scripts/chickpea-node.mjs
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env node
import path from 'node:path';
import process from 'node:process';
import { realpathSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import {
authenticateNgrok,
defaultNodeInstallationHome,
initInstallation,
installLaunchAgent,
installationStatus,
openSetup,
renewSetup,
restartInstallation,
runStart,
stopInstallation,
UnsafeRuntimeOperationLockError,
uninstallLaunchAgent,
} from './lib/node-installation.mjs';
const RELEASE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
export function parseArguments(argv, environment = process.env) {
const args = [...argv];
let home = defaultNodeInstallationHome(environment);
if (args[0] === '--home') {
if (!args[1] || args[1].startsWith('--')) throw new Error('--home requires a directory.');
home = path.resolve(args[1]);
args.splice(0, 2);
}
const command = args.shift();
if (!command) throw new Error(usage());
return { home, command, args };
}
export async function main(argv = process.argv.slice(2)) {
if (argv.length === 1 && ['--help', 'help'].includes(argv[0])) {
process.stdout.write(`${usage()}\n`);
return 0;
}
const parsed = parseArguments(argv);
if (parsed.command === 'init') {
const options = parseInitOptions(parsed.args);
const result = await initInstallation({ home: parsed.home, releaseRoot: RELEASE_ROOT, ...options });
process.stdout.write(result.changed
? `[chickpea] Initialized ${result.sourceCommit.slice(0, 12)}.\n`
: `[chickpea] Installation already initialized.\n`);
return 0;
}
if (parsed.command === 'start') {
const options = parseStartOptions(parsed.args);
try {
return await runStart(parsed.home, options);
} catch (error) {
if (options.service && error instanceof UnsafeRuntimeOperationLockError) {
process.stderr.write(`[chickpea] ${error.message}\n[chickpea] Service start will remain stopped until the lock is inspected and the service is installed again.\n`);
return 0;
}
throw error;
}
}
if (parsed.command === 'stop') {
requireNoArguments(parsed.args, 'stop');
const result = await stopInstallation(parsed.home);
process.stdout.write(result.stopped
? '[chickpea] Stopped.\n'
: result.serviceUnloaded
? '[chickpea] LaunchAgent unloaded; no managed process answered the control socket.\n'
: '[chickpea] No managed process is running.\n');
return 0;
}
if (parsed.command === 'restart') {
requireNoArguments(parsed.args, 'restart');
return restartInstallation(parsed.home);
}
if (parsed.command === 'tunnel') {
if (parsed.args.length !== 3 || parsed.args[0] !== 'authenticate' || parsed.args[1] !== '--token-file') {
throw new Error('Use tunnel authenticate --token-file /absolute/private/token-file.');
}
await authenticateNgrok(parsed.home, path.resolve(parsed.args[2]));
process.stdout.write('[chickpea] ngrok authentication saved. Start Chickpea to verify it. The public URL is unchanged.\n');
return 0;
}
if (parsed.command === 'status') {
requireNoArguments(parsed.args, 'status');
const status = await installationStatus(parsed.home);
process.stdout.write([
`Home: ${status.home}`,
`Installed version: ${status.installedVersion}`,
`Source commit: ${status.sourceCommit}`,
`Managed process: ${yesNo(status.managedProcess)}`,
`Local HTTP: ${yesNo(status.localReachable)}`,
`Public HTTPS: ${yesNo(status.publicReachable)}`,
...(status.publicHint ? [status.publicHint] : []),
`Tunnel configured: ${yesNo(status.tunnelConfigured)}`,
`Tunnel process: ${status.tunnelConfigured ? yesNo(status.tunnelRunning) : 'not configured'}`,
`Start-at-login service installed: ${yesNo(status.launchAgentInstalled)}`,
`Start-at-login service loaded: ${yesNo(status.launchAgentLoaded)}`,
'Slack delivery: not verified by this command',
'',
].join('\n'));
return status.managedProcess && status.localReachable ? 0 : 1;
}
if (parsed.command === 'setup') {
const renew = parseFlagOnly(parsed.args, '--renew');
if (renew) {
await renewSetup(parsed.home);
process.stdout.write('[chickpea] Setup access renewed. Run `chickpea-node setup` to open it.\n');
} else {
await openSetup(parsed.home);
process.stdout.write('[chickpea] Opened setup in the browser.\n');
}
return 0;
}
if (parsed.command === 'service') {
const action = parsed.args.shift();
requireNoArguments(parsed.args, `service ${action ?? ''}`.trim());
if (action === 'install') {
const result = await installLaunchAgent(parsed.home);
process.stdout.write(`[chickpea] Start-at-login service installed (${result.label}).\n`);
return 0;
}
if (action === 'uninstall') {
const result = uninstallLaunchAgent(parsed.home);
process.stdout.write(result.removed
? `[chickpea] Start-at-login service removed (${result.label}).\n`
: '[chickpea] No start-at-login service was installed.\n');
return 0;
}
throw new Error('service requires install or uninstall.');
}
throw new Error(`Unknown command: ${parsed.command}\n${usage()}`);
}
function parseInitOptions(args) {
const values = new Map();
const allowed = new Set(['--origin', '--port', '--tunnel', '--tunnel-token-file', '--cloudflared', '--ngrok']);
for (let index = 0; index < args.length; index += 2) {
const option = args[index];
const value = args[index + 1];
if (!allowed.has(option)) throw new Error(`Unknown init option: ${option ?? '(missing)'}`);
if (!value || value.startsWith('--')) throw new Error(`${option} requires a value.`);
if (values.has(option)) throw new Error(`${option} may only be provided once.`);
values.set(option, value);
}
if (!values.has('--origin') || !values.has('--port')) {
throw new Error('init requires --origin and --port.');
}
return {
origin: values.get('--origin'),
port: Number(values.get('--port')),
tunnelMode: values.get('--tunnel') ?? 'external',
...(values.has('--tunnel-token-file') ? { tunnelTokenFile: path.resolve(values.get('--tunnel-token-file')) } : {}),
...(values.has('--cloudflared') ? { cloudflared: path.resolve(values.get('--cloudflared')) } : {}),
...(values.has('--ngrok') ? { ngrok: path.resolve(values.get('--ngrok')) } : {}),
};
}
function parseFlagOnly(args, flag) {
if (args.length === 0) return false;
if (args.length === 1 && args[0] === flag) return true;
throw new Error(`Only ${flag} is accepted for this command.`);
}
function parseStartOptions(args) {
const allowed = new Set(['--open', '--service']);
const seen = new Set();
for (const argument of args) {
if (!allowed.has(argument)) throw new Error(`Unknown start option: ${argument}`);
if (seen.has(argument)) throw new Error(`${argument} may only be provided once.`);
seen.add(argument);
}
if (seen.has('--open') && seen.has('--service')) throw new Error('--open is not available for service starts.');
return { open: seen.has('--open'), service: seen.has('--service') };
}
function requireNoArguments(args, command) {
if (args.length > 0) throw new Error(`${command} does not accept arguments.`);
}
function yesNo(value) {
return value ? 'yes' : 'no';
}
function usage() {
return `Usage: chickpea-node [--home DIR] <command>
init --origin URL --port PORT [--tunnel ngrok|external|cloudflare] [--tunnel-token-file FILE] [--ngrok FILE | --cloudflared FILE]
start [--open]
restart
stop
status
setup [--renew]
service install|uninstall
tunnel authenticate --token-file FILE`;
}
export function isMainModule(argvEntry = process.argv[1], moduleUrl = import.meta.url) {
if (!argvEntry) return false;
try {
return realpathSync(argvEntry) === realpathSync(fileURLToPath(moduleUrl));
} catch { return false; }
}
if (isMainModule()) {
main().then(
(code) => { process.exitCode = code; },
(error) => {
process.stderr.write(`[chickpea] ${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
},
);
}