- 新增 PostgreSQL 备份、fresh DB 恢复和 server_data volume 备份脚本\n- 恢复默认拒绝覆盖,必须显式 --confirm-overwrite\n- 补充 package scripts、deploy verify 校验和部署文档\n\nCo-Authored-By: GPT-5 Codex <codex@openai.com>
117 lines
3.2 KiB
JavaScript
117 lines
3.2 KiB
JavaScript
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
import { dirname, resolve } from 'node:path';
|
|
import { spawn } from 'node:child_process';
|
|
|
|
export function parseArgs(argv) {
|
|
const parsed = { flags: new Set(), values: new Map(), positionals: [] };
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
if (arg === '--') continue;
|
|
if (!arg.startsWith('--')) {
|
|
parsed.positionals.push(arg);
|
|
continue;
|
|
}
|
|
|
|
const eq = arg.indexOf('=');
|
|
if (eq !== -1) {
|
|
parsed.values.set(arg.slice(2, eq), arg.slice(eq + 1));
|
|
continue;
|
|
}
|
|
|
|
const key = arg.slice(2);
|
|
const next = argv[index + 1];
|
|
if (next && !next.startsWith('--')) {
|
|
parsed.values.set(key, next);
|
|
index += 1;
|
|
} else {
|
|
parsed.flags.add(key);
|
|
}
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
export function option(args, name, fallback = undefined) {
|
|
return args.values.has(name) ? args.values.get(name) : fallback;
|
|
}
|
|
|
|
export function flag(args, name) {
|
|
return args.flags.has(name);
|
|
}
|
|
|
|
export function readEnvFile(filePath, { optional = false } = {}) {
|
|
if (!existsSync(filePath)) {
|
|
if (optional) return {};
|
|
throw new Error(`Env file not found: ${filePath}`);
|
|
}
|
|
|
|
const env = {};
|
|
const content = readFileSync(filePath, 'utf8');
|
|
for (const rawLine of content.split(/\r?\n/)) {
|
|
const line = rawLine.trim();
|
|
if (!line || line.startsWith('#')) continue;
|
|
const eq = line.indexOf('=');
|
|
if (eq === -1) continue;
|
|
const key = line.slice(0, eq).trim();
|
|
let value = line.slice(eq + 1).trim();
|
|
if (
|
|
(value.startsWith('"') && value.endsWith('"')) ||
|
|
(value.startsWith("'") && value.endsWith("'"))
|
|
) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
env[key] = value;
|
|
}
|
|
return env;
|
|
}
|
|
|
|
export function ensureParentDir(filePath) {
|
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
}
|
|
|
|
export function timestampSlug(date = new Date()) {
|
|
return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
|
}
|
|
|
|
export function shellQuote(value) {
|
|
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value;
|
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
}
|
|
|
|
export function commandToString(command, { stdin, stdout } = {}) {
|
|
const rendered = command.map((part) => shellQuote(part)).join(' ');
|
|
const withStdin = stdin ? `${rendered} < ${shellQuote(stdin)}` : rendered;
|
|
return stdout ? `${withStdin} > ${shellQuote(stdout)}` : withStdin;
|
|
}
|
|
|
|
export function composePrefix({ envFile, composeFile }) {
|
|
return ['docker', 'compose', '--env-file', envFile, '-f', composeFile];
|
|
}
|
|
|
|
export function resolvePath(path) {
|
|
return resolve(process.cwd(), path);
|
|
}
|
|
|
|
export function runCommand(command, options = {}) {
|
|
return new Promise((resolveRun, rejectRun) => {
|
|
const child = spawn(command[0], command.slice(1), {
|
|
stdio: options.stdio ?? 'inherit',
|
|
cwd: options.cwd ?? process.cwd(),
|
|
});
|
|
|
|
child.on('error', rejectRun);
|
|
child.on('close', (code) => {
|
|
if (code === 0) {
|
|
resolveRun();
|
|
} else {
|
|
rejectRun(new Error(`Command failed (${code}): ${commandToString(command)}`));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
export function quotePgLiteral(value) {
|
|
return `'${value.replace(/'/g, "''")}'`;
|
|
}
|