scripts/chickpea-environment.mjs
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env node
import { readFileSync } from 'node:fs';
import { attestEnvironment } from './lib/environment-attestation.mjs';
import {
EnvironmentRegistryError,
activeEnvironmentTargets,
claimEnvironment,
migrateEnvironmentProviderAuthConfigsFromFile,
readEnvironmentStatus,
reclaimEnvironment,
reconcileEnvironment,
releaseEnvironment,
withEnvironmentInstallationClaim,
} from './lib/environment-registry.mjs';
import { EnvironmentWaitError, waitForEnvironmentClaim } from './lib/environment-wait.mjs';
import { assertInstallationNodeRuntime, reserveEnvironmentInstallation, restoreEnvironmentInstallation } from './lib/environment-installation.mjs';
import { InstallationNodeError, reconcileNodeInstallationProcess, runNodeInstallation } from './lib/environment-installation-node.mjs';
import { targetEnvironment } from './lib/environment-target.mjs';
import {
readEnvironmentCapabilities,
renderCapabilityTable,
writeCapabilityMatrix,
} from './lib/environment-capabilities.mjs';
import {
reconcileEnvironmentDeployment,
adoptEnvironmentFromFile,
carryEnvironmentSchemaAdvancementIntent,
readLocalEnvironmentContract,
withEnvironmentReleaseFence,
writeEnvironmentSchemaAdvancementIntent,
} from './lib/environment-preflight.mjs';
export const USAGE = `Usage: npm run env -- <command> [lane] [options]
Lanes: ${activeEnvironmentTargets.join(', ')}. Every command takes --worktree <absolute path> (default: the current directory).
status [lane|--all] Claims, health, serving version and schema generation.
capabilities <lane|all> [--json] [--write]
Read-only lane capability matrix: profile, keys, models, schema.
claim [lane] Claim a free lane for this worktree at its current HEAD.
wait-claim <lane|any> --timeout-ms MS --poll-ms MS
Wait for a lane, then claim it (0 timeout = try once).
restamp <lane> Move this worktree's own claim to its current HEAD after a commit,
rebase or branch switch. Refused while a deploy is open; never
takes another worktree's lane. Carries a pending schema intent.
schema-advance <lane> Record this worktree's intent to advance the lane's Durable Object
schema to the candidate's generation (one recorded step). The next
guarded deploy applies it. Permanent: Cloudflare cannot roll back.
release <lane> Give back this worktree's claim from any branch HEAD.
reclaim <lane> [--adopt-orphan]
Renew an expired claim, or adopt one whose worktree is gone.
target <lane> | attest <lane> Doctor inputs for the legacy coordinator.
reconciliation [lane] Adopt an uploaded version after an interrupted deploy.
register --registration FILE | migrate-provider-auth --bindings FILE
install-reserve|install-start|install-reconcile|install-restore <lane> ...
Borrow a lane for a fresh install (qa/live/operator/environments.md).
Output is JSON on stdout; errors are JSON on stderr with an "error" code.
`;
/**
* One advisory line on stderr after a claim: whether the worktree's HEAD
* already contains remote main. The guarded deploy refuses a candidate behind
* main (QA_SOURCE_BEHIND_MAIN); saying so now saves a build and a host wait.
*/
async function sourceAdvisory(worktreePath, stderr, io) {
if (io.sourceAdvisory === false || process.env.CHICKPEA_ENV_SOURCE_ADVISORY === 'off') return;
try {
const { admitQaCandidate } = await import('./lib/qa-candidate.mjs');
const admission = admitQaCandidate(worktreePath);
stderr(`source: contains remote main ${admission.approvedTip.slice(0, 8)}; ready for a guarded deploy\n`);
} catch (error) {
const code = error?.code ?? 'UNKNOWN';
stderr(code === 'QA_SOURCE_BEHIND_MAIN'
? 'source: behind remote main; rebase or merge origin/main, then `npm run env -- restamp <lane>`, before deploying\n'
: `source: admission not confirmed (${code}); the guarded deploy will check again\n`);
}
}
/**
* The schema contract belongs to the claimed checkout. Without this, a
* `--worktree` pointing at another checkout would be judged by the runner's own
* migrations.
*/
function contractOptions(options, parsed) {
return parsed.flags.worktree ? { ...options, projectRoot: parsed.flags.worktree } : options;
}
export async function runEnvironmentCli(argv, io = {}) {
const stdout = io.stdout ?? ((value) => process.stdout.write(value));
const stderr = io.stderr ?? ((value) => process.stderr.write(value));
try {
if (argv.length === 0 || argv.includes('--help') || argv[0] === 'help') {
(argv.length === 0 ? stderr : stdout)(USAGE);
return argv.length === 0 ? 2 : 0;
}
const parsed = parseArgs(argv);
const options = {
...(parsed.flags.root ? { root: parsed.flags.root } : {}),
...(parsed.flags.worktree ? { worktreePath: parsed.flags.worktree } : {}),
...(parsed.flags.leaseMs ? { leaseDurationMs: numberFlag(parsed.flags.leaseMs) } : {}),
...((parsed.flags.profile || parsed.flags.environment) ? {
providerContext: [
...(parsed.flags.profile ? ['--profile', parsed.flags.profile] : []),
...(parsed.flags.environment ? ['--env', parsed.flags.environment] : []),
],
} : {}),
// Direct injection is intentionally available only to unit harnesses.
// The executable never accepts a host identity from argv or env.
...(io.hostFingerprint ? { hostFingerprint: io.hostFingerprint } : {}),
// Test harnesses must opt in explicitly; this is never derived from argv,
// environment variables, or the injected machine identity.
...(io.allowSuppliedObservation === true ? { allowSuppliedObservation: true } : {}),
};
let result;
if (['install-start', 'install-reconcile'].includes(parsed.command)) {
requireTarget(parsed.target);
if (Object.keys(parsed.flags).some((flag) => !['root', 'worktree', 'runtimeEnv'].includes(flag))
|| (parsed.command === 'install-start' ? !parsed.flags.runtimeEnv : parsed.flags.runtimeEnv)) {
throw new EnvironmentRegistryError('INVALID_ARGUMENT');
}
if (parsed.command === 'install-start') {
const installation = assertInstallationNodeRuntime(parsed.target, options);
result = await runNodeInstallation(installation, parsed.flags.runtimeEnv,
(start) => withEnvironmentInstallationClaim(parsed.target, options, start));
stdout(`${JSON.stringify(result)}\n`);
return result.code ?? 1;
}
result = withEnvironmentInstallationClaim(parsed.target, options, (installation) => {
if (installation.runtime !== 'node') throw new EnvironmentRegistryError('INSTALLATION_RUNTIME_MISMATCH');
return reconcileNodeInstallationProcess(installation);
});
} else if (['install-reserve', 'install-restore'].includes(parsed.command)) {
requireTarget(parsed.target);
if (!parsed.flags.installation || Object.keys(parsed.flags).some((flag) => ![
'root', 'worktree', 'installation', 'profile', 'environment',
].includes(flag))) throw new EnvironmentRegistryError('INVALID_ARGUMENT');
result = await (parsed.command === 'install-reserve' ? reserveEnvironmentInstallation : restoreEnvironmentInstallation)(
parsed.target, parsed.flags.installation, options,
);
} else if (parsed.command === 'register') {
if (parsed.target || !parsed.flags.registration
|| Object.keys(parsed.flags).some((flag) => !['root', 'registration'].includes(flag))) {
throw new EnvironmentRegistryError('INVALID_ARGUMENT');
}
result = await adoptEnvironmentFromFile(parsed.flags.registration, options);
} else if (parsed.command === 'migrate-provider-auth') {
if (parsed.target || !parsed.flags.bindings
|| Object.keys(parsed.flags).some((flag) => !['root', 'bindings'].includes(flag))) {
throw new EnvironmentRegistryError('INVALID_ARGUMENT');
}
result = migrateEnvironmentProviderAuthConfigsFromFile(parsed.flags.bindings, options);
} else if (parsed.command === 'claim') {
result = claimEnvironment(parsed.target, options);
await sourceAdvisory(result.canonicalWorktreePath, stderr, io);
} else if (parsed.command === 'restamp') {
requireTarget(parsed.target);
if (Object.keys(parsed.flags).some((flag) => !['root', 'worktree', 'leaseMs'].includes(flag))) {
throw new EnvironmentRegistryError('INVALID_ARGUMENT');
}
let previous;
const claim = reclaimEnvironment(parsed.target, {
...options,
requireSameWorktree: true,
onReclaimed: (change) => { previous = change.previous; },
});
const schemaIntent = carryEnvironmentSchemaAdvancementIntent(parsed.target, previous?.leaseNonce, contractOptions(options, parsed));
result = {
target: parsed.target,
restamped: true,
branch: claim.branch,
previousRevision: previous?.claimedRevision ?? null,
claimedRevision: claim.claimedRevision,
expiresAt: claim.expiresAt,
schemaIntent,
...(schemaIntent.startsWith('stale:') ? { next: 'The pending schema intent no longer matches this HEAD; run `env schema-advance` again if the candidate still needs it.' } : {}),
};
await sourceAdvisory(claim.canonicalWorktreePath, stderr, io);
} else if (parsed.command === 'schema-advance') {
requireTarget(parsed.target);
if (Object.keys(parsed.flags).some((flag) => !['root', 'worktree'].includes(flag))) {
throw new EnvironmentRegistryError('INVALID_ARGUMENT');
}
const local = contractOptions(options, parsed);
result = writeEnvironmentSchemaAdvancementIntent(parsed.target, readLocalEnvironmentContract(local).schemaGeneration, local);
} else if (parsed.command === 'wait-claim') {
if (!parsed.target || !['any', ...activeEnvironmentTargets].includes(parsed.target)
|| parsed.flags.timeoutMs === undefined || parsed.flags.pollMs === undefined
|| Object.keys(parsed.flags).some((flag) => ![
'root', 'worktree', 'leaseMs', 'timeoutMs', 'pollMs',
].includes(flag))) {
throw new EnvironmentRegistryError('INVALID_ARGUMENT');
}
const controller = new AbortController();
const signal = io.signal ?? controller.signal;
const interrupt = () => controller.abort();
if (!io.signal) {
process.once('SIGINT', interrupt);
process.once('SIGTERM', interrupt);
}
try {
result = await waitForEnvironmentClaim(parsed.target, {
...options,
timeoutMs: numberFlag(parsed.flags.timeoutMs),
pollMs: numberFlag(parsed.flags.pollMs),
signal,
...(io.waitOptions ?? {}),
onStatusChange: (status) => stderr(
`wait-claim: ${status.map((lane) => `${lane.target}=${lane.claimed ? 'claimed' : lane.verifierLock === 'live' ? 'locked' : lane.health}`).join(' ')}\n`,
),
});
} finally {
if (!io.signal) {
process.removeListener('SIGINT', interrupt);
process.removeListener('SIGTERM', interrupt);
}
}
if (result?.claim?.canonicalWorktreePath) await sourceAdvisory(result.claim.canonicalWorktreePath, stderr, io);
} else if (parsed.command === 'capabilities') {
requireTarget(parsed.target);
if (Object.keys(parsed.flags).some((flag) => ![
'root', 'profile', 'environment', 'json', 'write',
].includes(flag)) || (parsed.flags.write && parsed.target !== 'all')) {
throw new EnvironmentRegistryError('INVALID_ARGUMENT');
}
const report = await readEnvironmentCapabilities(parsed.target, {
...options,
// Test harnesses inject fake Wrangler and registry readers here only.
...(io.capabilityOptions ?? {}),
});
const written = parsed.flags.write
? writeCapabilityMatrix(report, { ...options, ...(io.capabilityOptions ?? {}) })
: undefined;
if (parsed.flags.json) {
stdout(`${JSON.stringify(written ? { ...report, matrix: written } : report, null, 2)}\n`);
} else {
stdout(renderCapabilityTable(report));
if (written) stdout(`\nUpdated the generated section of ${written.path}.\n`);
}
return 0;
} else if (parsed.command === 'status') {
result = readEnvironmentStatus({
...options,
worktreePath: options.worktreePath ?? process.cwd(),
...(!parsed.flags.all && parsed.target ? { target: parsed.target } : {}),
});
} else if (parsed.command === 'target') {
requireTarget(parsed.target);
result = targetEnvironment(parsed.target, options);
} else if (parsed.command === 'attest') {
requireTarget(parsed.target);
if (parsed.flags.observation && io.allowSuppliedObservation !== true) {
throw new EnvironmentRegistryError('CALLER_OBSERVATION_REFUSED');
}
if (!parsed.flags.observation && io.allowSuppliedObservation === true) {
throw new EnvironmentRegistryError('OBSERVATION_REQUIRED');
}
const observation = parsed.flags.observation
? JSON.parse(readFileSync(parsed.flags.observation, 'utf8'))
: undefined;
result = await attestEnvironment(parsed.target, observation, options);
} else if (parsed.command === 'release') {
requireTarget(parsed.target);
const releaseOptions = { ...options, ownerHeadMayMove: true };
result = withEnvironmentReleaseFence(
parsed.target,
releaseOptions,
(fence) => releaseEnvironment(parsed.target, {
...releaseOptions,
expectedTargetLockRunId: fence.runId,
}),
);
} else if (parsed.command === 'reclaim') {
requireTarget(parsed.target);
result = reclaimEnvironment(parsed.target, {
...options,
...(parsed.flags.adoptOrphan ? { adoptOrphan: true } : {}),
});
} else if (parsed.command === 'reconciliation') {
const recoveredDeployment = parsed.target
? await reconcileEnvironmentDeployment(parsed.target, options)
: null;
result = {
...reconcileEnvironment(parsed.target, options),
deploymentRecovered: recoveredDeployment !== null,
};
} else {
throw new EnvironmentRegistryError('INVALID_COMMAND');
}
stdout(`${JSON.stringify(result, null, 2)}\n`);
return result?.kind === 'timeout' ? 3 : 0;
} catch (error) {
const code = error instanceof EnvironmentRegistryError || error instanceof EnvironmentWaitError || error instanceof InstallationNodeError
? error.code
: 'ENVIRONMENT_COMMAND_FAILED';
// A non-registry failure used to surface as a bare code, which hid the
// actual cause (a missing host variable, an unreachable Worker, a parse
// error). Name it, bounded and without any token-shaped content.
const message = error instanceof EnvironmentRegistryError || error instanceof EnvironmentWaitError || error instanceof InstallationNodeError
? undefined
: redactCommandFailure(error instanceof Error ? error.message : String(error));
const body = {
error: code,
...((error instanceof EnvironmentRegistryError || error instanceof EnvironmentWaitError) && error.details
? { details: error.details }
: {}),
...(message ? { message } : {}),
};
stderr(`${JSON.stringify(body)}\n`);
return error instanceof EnvironmentWaitError && error.code === 'WAIT_CANCELLED' ? 130 : 2;
}
}
function redactCommandFailure(message) {
return String(message)
.replace(/[A-Za-z0-9_-]{43}/g, '<redacted>')
.replace(/xox[a-z]-[A-Za-z0-9-]+/g, '<redacted>')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 240);
}
function parseArgs(argv) {
const positional = [];
const flags = {};
for (let index = 0; index < argv.length; index += 1) {
const value = argv[index];
if (value === '--all') {
flags.all = true;
continue;
}
if (value === '--json' || value === '--write') {
flags[value.slice(2)] = true;
continue;
}
if (value === '--adopt-orphan') {
flags.adoptOrphan = true;
continue;
}
const field = {
'--root': 'root',
'--worktree': 'worktree',
'--lease-ms': 'leaseMs',
'--timeout-ms': 'timeoutMs',
'--poll-ms': 'pollMs',
'--observation': 'observation',
'--bindings': 'bindings',
'--registration': 'registration',
'--installation': 'installation',
'--runtime-env': 'runtimeEnv',
'--profile': 'profile',
'--env': 'environment',
}[value];
if (field) {
const next = argv[index + 1];
if (!next || next.startsWith('--')) throw new EnvironmentRegistryError('INVALID_ARGUMENT');
flags[field] = next;
index += 1;
continue;
}
if (value.startsWith('--')) throw new EnvironmentRegistryError('INVALID_ARGUMENT');
positional.push(value);
}
if (positional.length < 1 || positional.length > 2) {
throw new EnvironmentRegistryError('INVALID_COMMAND');
}
if (flags.bindings && positional[0] !== 'migrate-provider-auth') {
throw new EnvironmentRegistryError('INVALID_ARGUMENT');
}
if ((flags.json || flags.write) && positional[0] !== 'capabilities') throw new EnvironmentRegistryError('INVALID_ARGUMENT');
if (flags.registration && positional[0] !== 'register') throw new EnvironmentRegistryError('INVALID_ARGUMENT');
if (flags.installation && !['install-reserve', 'install-restore'].includes(positional[0])) throw new EnvironmentRegistryError('INVALID_ARGUMENT');
if (positional[0] !== 'wait-claim'
&& (flags.timeoutMs !== undefined || flags.pollMs !== undefined)) {
throw new EnvironmentRegistryError('INVALID_ARGUMENT');
}
return { command: positional[0], target: positional[1], flags };
}
function requireTarget(target) {
if (!target) throw new EnvironmentRegistryError('TARGET_REQUIRED');
}
function numberFlag(input) {
const value = Number(input);
if (!Number.isSafeInteger(value)) throw new EnvironmentRegistryError('INVALID_ARGUMENT');
return value;
}
if (import.meta.url === `file://${process.argv[1]}`) {
process.exitCode = await runEnvironmentCli(process.argv.slice(2));
}