cache-apt-pkgs-action/dev_scripts/lib.mjs

242 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
import {
accessSync,
constants,
mkdirSync,
readFileSync,
writeFileSync,
} from "node:fs";
import { execFileSync, spawnSync } from "node:child_process";
import path from "node:path";
import process from "node:process";
import readline from "node:readline/promises";
import { fileURLToPath } from "node:url";
export { fileURLToPath, path, process };
export class Paths {
constructor(importMetaUrl) {
this.importMetaUrl = importMetaUrl;
}
scriptDir() {
return path.dirname(fileURLToPath(this.importMetaUrl));
}
repoRootDir() {
return path.resolve(this.scriptDir(), "..");
}
}
export const ROOT_DIR = new Paths(import.meta.url).repoRootDir();
export const REPO_OWNER = "awalsh128";
export const REPO_NAME = "cache-apt-pkgs-action";
export const REPO_SLUG = `${REPO_OWNER}/${REPO_NAME}`;
export const REPO_URL = `https://github.com/${REPO_SLUG}.git`;
export const GITHUB_API_BASE_URL = "https://api.github.com";
export const GITHUB_USER_AGENT = `${REPO_NAME}-dev-scripts`;
export const VSCODE_SETTINGS_RELPATH = ".vscode/settings.json";
export const VSCODE_SETTINGS_DEFAULTS = {
"chat.tools.terminal.autoApprove": {
"*": true,
},
"chat.useAgentsMdFile": true,
};
export const CURRENT_NODE_VERSION = "24";
export function usage(scriptPath, params) {
return `usage: ${path.basename(scriptPath)} ${params}`;
}
export function fail(message, exitCode = 1) {
logError(message);
process.exit(exitCode);
}
export function logInfo(message) {
console.log(` ${message}`);
}
export function logSuccess(message) {
console.log(`${message}`);
}
export function logWarn(message) {
console.log(`⚠️ ${message}`);
}
export function logError(message) {
console.error(`${message}`);
}
export function assertNonEmpty(value, message, usageMessage) {
if (!value || value.trim().length === 0) {
if (usageMessage) {
console.error(usageMessage);
}
fail(message);
}
}
export function commandExists(command) {
const isExecutable = (candidatePath) => {
try {
accessSync(candidatePath, constants.X_OK);
return true;
} catch {
return false;
}
};
if (command.includes("/")) {
return isExecutable(command);
}
const pathValue = process.env.PATH ?? "";
const dirs = pathValue.split(path.delimiter).filter(Boolean);
return dirs.some((dirPath) => isExecutable(path.join(dirPath, command)));
}
export function ensureDirExists(dirPath) {
mkdirSync(dirPath, { recursive: true });
}
export function readJsonFile(filePath) {
return JSON.parse(readFileSync(filePath, "utf8"));
}
export function writeJsonFile(filePath, value) {
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
export function readNodeMajorVersion(rootDir = ROOT_DIR) {
const nodeVersionPath = path.join(rootDir, ".node_ver");
let fileText;
try {
fileText = readFileSync(nodeVersionPath, "utf8");
} catch (error) {
fail(
`Missing or unreadable version file at ${nodeVersionPath}: ${error instanceof Error ? error.message : String(error)}`,
);
}
const lines = fileText
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0);
if (lines.length !== 1) {
fail(".node_ver must contain exactly one non-empty line.");
}
const rawVersion = lines[0];
if (!/^\d+$/.test(rawVersion)) {
fail(
".node_ver must contain only the Node.js major version (e.g. 20, 24).",
);
}
return rawVersion;
}
function normalizeRunOptions(optionsOrQuiet) {
if (typeof optionsOrQuiet === "boolean") {
return {
cwd: ROOT_DIR,
env: process.env,
encoding: "utf8",
stdio: optionsOrQuiet ? "pipe" : "inherit",
};
}
const options = optionsOrQuiet ?? {};
return {
cwd: options.cwd ?? ROOT_DIR,
env: options.env ?? process.env,
encoding: options.encoding ?? "utf8",
stdio:
options.stdio ?? (options.stdout || options.stderr ? "pipe" : "inherit"),
};
}
export function run(command, args = [], optionsOrQuiet = {}) {
const options = normalizeRunOptions(optionsOrQuiet);
try {
return execFileSync(command, args, options);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to execute '${command} ${args.join(" ")}'. ${message}`,
);
}
}
export function tryRun(command, args = [], optionsOrQuiet = {}) {
const options = normalizeRunOptions(optionsOrQuiet);
const result = spawnSync(command, args, options);
if (result.error) {
throw new Error(
`Failed to spawn '${command} ${args.join(" ")}'. ${result.error.message}`,
);
}
return {
ok: result.status === 0,
status: result.status,
stdout: result.stdout ?? "",
stderr: result.stderr ?? "",
};
}
export function runCaptureOutput(command, args = [], options = {}) {
return run(command, args, {
...options,
stdout: "pipe",
stderr: "pipe",
}).trim();
}
export async function confirmPrompt(
message,
yesPattern = /^[yY]$/,
noPattern = /^[nN]$/,
) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
try {
const getText = (pattern) =>
pattern.source
.trim()
.replace(/^\^/, "")
.replace(/\$$/, "")
.replace(/\\([^\\])/g, "$1");
while (true) {
const answer = await rl
.question(
`${message} [${getText(yesPattern)} | ${getText(noPattern)}] `,
)
.then((ans) => ans.trim().toLowerCase());
if (yesPattern.test(answer) || answer.trim() === "") {
return;
}
if (noPattern.test(answer)) {
fail("Aborted.", 0);
}
logError(
`Invalid option '${answer}' selected. Options are: ${getText(yesPattern)} or ${getText(noPattern)}`,
);
}
} finally {
rl.close();
}
}