Ctrl K
ComunidadGuías…

scripts/chickpea-local-worker.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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);
}