scripts/chickpea-local-worker.mjs
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env node
import { createHash } from 'node:crypto';
import { spawn, spawnSync } from 'node:child_process';
import {
closeSync,
existsSync,
openSync,
readFileSync,
unlinkSync,
watch,
writeFileSync,
} from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import { experimental_readRawConfig } from 'wrangler';
import {
bindLocalLaneSlack,
createLocalTimingObserver,
ensureWorktreeDevVarsLink,
initializeLocalLane,
localLaneLockPath,
readLocalLaneManifest,
relocateUnboundLocalLane,
renewLocalLaneSetup,
resolveLocalLanePaths,
validateLocalLaneName,
worktreeDevVarsLinkStatus,
} from './lib/local-worker-lane.mjs';
import {
assertRunnerRootReadOnly,
inspectLocalWorkerRunner,
} from './lib/local-worker-inspection.mjs';
const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const WRANGLER = join(PROJECT_ROOT, 'node_modules', '.bin', 'wrangler');
const VITE = join(PROJECT_ROOT, 'node_modules', '.bin', 'vite');
const [command = 'help', ...rawArgs] = process.argv.slice(2);
let args;
try {
args = parseArgs(rawArgs);
assertRunnerRootReadOnly(command, args.get('runner-root'));
if (command === 'init') await init();
else if (command === 'bind-slack') bindSlack();
else if (command === 'status') status();
else if (command === 'renew-setup') await renewSetup();
else if (command === 'relocate-unbound') await relocateUnbound();
else if (command === 'setup-link') setupLink();
else if (command === 'start') await start();
else if (command === 'schedule') await schedule();
else if (command === 'help' || command === '--help' || command === '-h') usage(0);
else throw new Error(`Unknown command ${JSON.stringify(command)}.`);
} catch (error) {
console.error(`local-worker: ${error instanceof Error ? error.message : String(error)}`);
process.exitCode = 1;
}
async function init() {
const result = await initializeLocalLane({
projectRoot: PROJECT_ROOT,
lane: option('lane'),
publicUrl: option('public-url'),
tunnelName: option('tunnel'),
port: args.get('port') ?? 8787,
});
console.log(result.created ? `Initialized local Worker lane ${result.manifest.lane}.` : `Local Worker lane ${result.manifest.lane} was already initialized.`);
printManifest(result.manifest);
console.log(`Private setup link: npm run dev:cf -- setup-link --lane ${result.manifest.lane}`);
}
function bindSlack() {
const lane = option('lane');
const manifest = bindLocalLaneSlack(PROJECT_ROOT, lane, {
workspaceId: option('workspace-id'),
workspaceLabel: option('workspace-label'),
appId: option('app-id'),
appLabel: args.get('app-label') ?? 'Chickpea',
});
console.log(`Bound ${manifest.lane} to its immutable Slack workspace/app pair.`);
printManifest(manifest);
}
function status() {
const runnerRoot = args.get('runner-root');
if (runnerRoot) {
console.log(JSON.stringify(inspectLocalWorkerRunner({
runnerRoot,
candidateRoot: PROJECT_ROOT,
lane: option('lane'),
}), null, 2));
return;
}
const manifest = laneManifest();
const paths = resolveLocalLanePaths(PROJECT_ROOT, manifest.lane);
const lock = readLock(localLaneLockPath(manifest.publicUrl));
const live = lock ? pidIsLive(lock.pid) : false;
const owner = lock ? {
...lock,
live,
status: live ? 'running' : 'stale-lock',
} : null;
printManifest(manifest, owner, worktreeDevVarsLinkStatus(paths));
}
async function renewSetup() {
const lane = option('lane');
const result = await renewLocalLaneSetup(PROJECT_ROOT, lane);
console.log(`Renewed the 24-hour setup capability for ${result.manifest.lane}.`);
console.log('Restart the local Worker before opening the new private setup link.');
console.log(`Private setup link: npm run dev:cf -- setup-link --lane ${result.manifest.lane}`);
}
async function relocateUnbound() {
const result = await relocateUnboundLocalLane(PROJECT_ROOT, option('lane'), {
publicUrl: option('public-url'),
tunnelName: args.get('tunnel'),
});
console.log(`Relocated unbound local Worker lane ${result.manifest.lane}.`);
printManifest(result.manifest);
console.log('Restart the local Worker before opening its rotated private setup link.');
}
function setupLink() {
const manifest = laneManifest();
const paths = resolveLocalLanePaths(PROJECT_ROOT, manifest.lane);
console.log(readFileSync(paths.setupLink, 'utf8').trim());
}
async function start() {
const manifest = laneManifest();
const paths = resolveLocalLanePaths(PROJECT_ROOT, manifest.lane);
ensureWorktreeDevVarsLink(paths);
const sourceSha = gitValue(['rev-parse', 'HEAD']);
const dirty = gitValue(['status', '--porcelain']).length > 0;
const lockPath = localLaneLockPath(manifest.publicUrl);
acquireLock(lockPath, manifest, sourceSha);
process.once('exit', () => releaseOwnedLock(lockPath));
const startedAt = Date.now();
try {
console.log('Preparing local AUTH_DB migrations…');
const { rawConfig } = await experimental_readRawConfig({
config: join(PROJECT_ROOT, 'wrangler.jsonc'),
});
const localConfig = structuredClone(rawConfig);
const authDb = localConfig.d1_databases?.find((database) => database.binding === 'AUTH_DB');
if (!authDb) throw new Error('root Cloudflare config does not define AUTH_DB.');
authDb.database_id = manifest.d1DatabaseId;
authDb.migrations_dir = resolve(PROJECT_ROOT, authDb.migrations_dir ?? 'migrations');
writeFileSync(paths.wranglerConfig, `${JSON.stringify(localConfig, null, 2)}\n`, { mode: 0o600 });
const migration = spawnSync(WRANGLER, [
'd1', 'migrations', 'apply', 'AUTH_DB', '--local',
'--persist-to', manifest.statePath,
'--config', paths.wranglerConfig,
], {
cwd: PROJECT_ROOT,
env: { ...process.env, CI: '1' },
stdio: 'inherit',
});
if (migration.status !== 0) throw new Error(`local AUTH_DB migrations failed (exit ${migration.status}).`);
console.log('\nLocal Worker identity');
printManifest(manifest, { pid: process.pid, sourceSha, dirty });
console.log('Slack delivery uses normal HTTP Events and interactivity through the named tunnel.');
console.log('Cloudflare dashboard telemetry does not contain this local run; inspect this terminal and local state.\n');
const child = spawn(VITE, ['dev', '--config', 'vite.config.ts'], {
cwd: PROJECT_ROOT,
env: {
...process.env,
CHICKPEA_LOCAL_LANE: manifest.lane,
CHICKPEA_LOCAL_PUBLIC_URL: manifest.publicUrl,
CHICKPEA_LOCAL_TUNNEL_NAME: manifest.tunnelName,
CHICKPEA_LOCAL_STATE_PATH: manifest.statePath,
CHICKPEA_LOCAL_D1_DATABASE_ID: manifest.d1DatabaseId,
CHICKPEA_LOCAL_PORT: String(manifest.port),
CHICKPEA_LOCAL_SOURCE_SHA: sourceSha,
},
stdio: ['inherit', 'pipe', 'pipe'],
});
const observeOutput = createLocalTimingObserver(paths.metrics, manifest.lane, sourceSha, startedAt);
const sourceWatchers = ['src', 'agents', 'skills']
.map((directory) => join(PROJECT_ROOT, directory))
.filter(existsSync)
.map((directory) => watch(directory, { recursive: true }, () => observeOutput.noteSourceChange()));
child.stdout.on('data', (chunk) => {
process.stdout.write(chunk);
observeOutput(chunk);
});
child.stderr.on('data', (chunk) => {
process.stderr.write(chunk);
observeOutput(chunk);
});
const forward = (signal) => {
if (child.exitCode === null && child.signalCode === null) child.kill(signal);
};
process.once('SIGINT', () => forward('SIGINT'));
process.once('SIGTERM', () => forward('SIGTERM'));
const code = await new Promise((resolveExit) => child.once('exit', (exitCode) => resolveExit(exitCode ?? 1)));
for (const watcher of sourceWatchers) watcher.close();
appendMetric(paths.metrics, {
kind: 'session',
lane: manifest.lane,
sourceSha,
dirty,
startedAt: new Date(startedAt).toISOString(),
durationMs: Date.now() - startedAt,
exitCode: code,
});
process.exitCode = code;
} finally {
releaseOwnedLock(lockPath);
}
}
async function schedule() {
const manifest = laneManifest();
const url = new URL('/cdn-cgi/local/scheduled', `http://127.0.0.1:${manifest.port}`);
url.searchParams.set('cron', '* * * * *');
const startedAt = Date.now();
const response = await fetch(url, { method: 'POST' });
const elapsedMs = Date.now() - startedAt;
if (!response.ok) throw new Error(`simulated scheduled trigger returned HTTP ${response.status}.`);
appendMetric(resolveLocalLanePaths(PROJECT_ROOT, manifest.lane).metrics, {
kind: 'simulated-schedule',
lane: manifest.lane,
at: new Date().toISOString(),
elapsedMs,
});
console.log(`Simulated local cron dispatch completed in ${elapsedMs} ms.`);
console.log('This exercises schedule logic only; it is not deployed due-time delivery evidence.');
}
function laneManifest() {
return readLocalLaneManifest(PROJECT_ROOT, option('lane'));
}
function printManifest(manifest, lock = null, devVarsLink = undefined) {
console.log(JSON.stringify({
lane: manifest.lane,
runtime: manifest.runtime,
transport: manifest.transport,
publicUrl: manifest.publicUrl,
tunnelName: manifest.tunnelName,
port: manifest.port,
owningWorktree: manifest.owningWorktree,
statePath: manifest.statePath,
d1DatabaseId: manifest.d1DatabaseId,
model: manifest.model,
slack: manifest.slack ?? 'not-bound',
...(devVarsLink === undefined ? {} : { devVarsLink }),
process: lock ?? 'stopped',
}, null, 2));
}
function acquireLock(lockPath, manifest, sourceSha) {
if (existsSync(lockPath)) {
const current = readLock(lockPath);
if (current && pidIsLive(current.pid)) {
throw new Error(`local endpoint ${manifest.publicUrl} is already owned by PID ${current.pid} in ${current.owningWorktree}.`);
}
unlinkSync(lockPath);
}
const fd = openSync(lockPath, 'wx', 0o600);
try {
writeFileSync(fd, `${JSON.stringify({
schemaVersion: 'chickpea-local-worker-lock/v1',
pid: process.pid,
lane: manifest.lane,
publicUrl: manifest.publicUrl,
owningWorktree: PROJECT_ROOT,
sourceSha,
worktreeFingerprint: createHash('sha256').update(PROJECT_ROOT).digest('hex').slice(0, 12),
acquiredAt: new Date().toISOString(),
}, null, 2)}\n`);
} finally {
closeSync(fd);
}
}
function releaseOwnedLock(lockPath) {
const lock = readLock(lockPath);
if (lock?.pid === process.pid) unlinkSync(lockPath);
}
function readLock(lockPath) {
if (!existsSync(lockPath)) return null;
try {
return JSON.parse(readFileSync(lockPath, 'utf8'));
} catch {
throw new Error(`Local endpoint lock is unreadable: ${lockPath}`);
}
}
function pidIsLive(pid) {
if (!Number.isSafeInteger(pid) || pid <= 0) return false;
try {
process.kill(pid, 0);
return true;
} catch (error) {
if (error?.code === 'ESRCH') return false;
throw error;
}
}
function appendMetric(file, value) {
writeFileSync(file, `${JSON.stringify(value)}\n`, { flag: 'a', mode: 0o600 });
}
function gitValue(gitArgs) {
const result = spawnSync('git', gitArgs, { cwd: PROJECT_ROOT, encoding: 'utf8' });
if (result.status !== 0) throw new Error(`git ${gitArgs.join(' ')} failed.`);
return result.stdout.trim();
}
function option(name) {
const value = args.get(name);
if (value === undefined) throw new Error(`--${name} is required.`);
return value;
}
function parseArgs(values) {
const parsed = new Map();
for (let index = 0; index < values.length; index += 1) {
const raw = values[index];
if (!raw?.startsWith('--')) throw new Error(`Unexpected argument ${JSON.stringify(raw)}.`);
const name = raw.slice(2);
const value = values[index + 1];
if (!name || !value || value.startsWith('--')) throw new Error(`${raw} requires a value.`);
if (parsed.has(name)) throw new Error(`${raw} may be provided only once.`);
parsed.set(name, value);
index += 1;
}
return parsed;
}
function usage(exitCode) {
console.log(`Usage:
npm run dev:cf -- init --lane local-a --public-url https://HOST --tunnel TUNNEL [--port 8787]
npm run dev:cf -- bind-slack --lane local-a --workspace-id T... --workspace-label "Chickpea Local A" --app-id A... [--app-label Chickpea]
npm run dev:cf -- status --lane local-a
npm run dev:cf -- status --lane local-a --runner-root /absolute/runner
npm run dev:cf -- renew-setup --lane local-a
npm run dev:cf -- relocate-unbound --lane local-a --public-url https://HOST [--tunnel TUNNEL]
npm run dev:cf -- setup-link --lane local-a
npm run dev:cf -- start --lane local-a
npm run dev:cf -- schedule --lane local-a`);
process.exit(exitCode);
}