mirror of
https://github.com/awalsh128/cache-apt-pkgs-action.git
synced 2026-07-06 17:14:36 +00:00
242 lines
5.8 KiB
JavaScript
242 lines
5.8 KiB
JavaScript
#!/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();
|
||
}
|
||
}
|