blob: acf0adb113237ab64db3aeefb9ac192de000c6f7 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import assert from 'node:assert/strict';
import { createHash, randomUUID } from 'node:crypto';
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
import { stripTypeScriptTypes } from 'node:module';
import { dirname } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
// The projection lives in @maka/core because the Runtime Host runs it too.
// `prepare` fires before any workspace builds, so read the TypeScript source
// rather than a dist build that does not exist yet.
const projection = await loadTypeScriptModule(
await readFile(
fileURLToPath(new URL('../packages/core/src/models-dev-projection.ts', import.meta.url)),
'utf8',
),
);
const { selectModelsDevCatalog, assertModelsDevProvider, collectProjectionRemovals } = projection;
const SOURCE_URL = projection.MODELS_DEV_SOURCE_URL;
export const PROVIDERS = projection.MODELS_DEV_PROVIDERS;
export const toMetadata = projection.projectModelsDevModel;
const DEFAULT_SNAPSHOT = 'scripts/model-metadata/models-dev-api.snapshot.json';
const DEFAULT_OUTPUT = 'packages/core/src/model-metadata.generated.ts';
const DEFAULT_PRICING_OUTPUT = 'packages/runtime/src/telemetry/model-pricing.generated.ts';
// models.dev cost fields describe the catalog provider's public API. They are
// not automatically the rate a user pays on an OAuth, free, subscription, or
// plan access path. Such paths stay unpriced unless builtin-pricing.ts carries
// an explicit rate for that exact path.
export const PRICING_EXCLUDED_PROVIDER_TYPES = new Set([
'alibaba-coding-plan-cn',
'alibaba-coding-plan',
'alibaba-token-plan-cn',
'alibaba-token-plan',
'github-copilot',
'kimi-coding-plan',
'minimax-coding-plan',
'MiniMax-cn',
'opencode-free',
'opencode-go',
'stepfun-ai-step-plan',
'stepfun-step-plan',
'tencent-coding-plan',
'tencent-token-plan',
'volcengine-agent-plan',
'volcengine-coding-plan',
'xiaomi-token-plan-ams',
'xiaomi-token-plan-cn',
'xiaomi-token-plan-sgp',
'zai-coding-plan',
]);
export async function main(argv = process.argv) {
const refreshInputPath = option('--refresh-input', argv);
const snapshotPath = option('--snapshot', argv) ?? DEFAULT_SNAPSHOT;
const outputPath = option('--output', argv) ?? DEFAULT_OUTPUT;
const pricingOutputPath =
option('--pricing-output', argv) ??
(outputPath === DEFAULT_OUTPUT ? DEFAULT_PRICING_OUTPUT : undefined);
const refresh = argv.includes('--refresh');
const check = argv.includes('--check');
const drift = argv.includes('--drift');
const acceptUpstreamRemovals = argv.includes('--accept-upstream-removals');
if (drift && (refresh || check)) {
throw new Error('--drift reports without writing and cannot combine with --refresh or --check');
}
if (refreshInputPath && !refresh && !drift) {
throw new Error('--refresh-input requires --refresh or --drift');
}
if (acceptUpstreamRemovals && !refresh) {
throw new Error('--accept-upstream-removals requires --refresh');
}
if (drift) {
const report = await collectDrift(await loadSnapshot(snapshotPath), refreshInputPath);
process.stdout.write(`${formatDrift(report)}\n`);
return report;
}
const source = refresh
? await refreshSnapshot(snapshotPath, refreshInputPath, { acceptUpstreamRemovals })
: await loadSnapshot(snapshotPath);
const {
metadata: generated,
pricing: generatedPricing,
providerFacts: generatedProviders,
providerOverrides: generatedModelProviderOverrides,
} = source.projection;
if (check) {
await assertGeneratedOutputs(outputPath, pricingOutputPath, source);
if (source.snapshotWrite) await replaceFilesTransactionally([source.snapshotWrite]);
return;
}
const providerTypeUnion = Object.keys(PROVIDERS).map(JSON.stringify).join(' | ');
const lines = [
...snapshotHeader(
'// Do not edit by hand; put access-path-specific facts in model-metadata.ts.',
source,
),
"import type { ModelMetadata } from './model-metadata.js';",
'',
`export const GENERATED_MODELS_DEV_METADATA: Record<${providerTypeUnion}, Record<string, ModelMetadata>> = {`,
];
for (const [provider, models] of Object.entries(generated)) {
lines.push(` ${JSON.stringify(provider)}: {`);
for (const [id, metadata] of Object.entries(models)) {
lines.push(` ${JSON.stringify(id)}: ${JSON.stringify(metadata)},`);
}
lines.push(' },');
}
lines.push('};', '');
lines.push(
`export const GENERATED_MODELS_DEV_MODEL_PROVIDER_OVERRIDES: Record<${providerTypeUnion}, Record<string, { adapter: import('./provider-registry.js').ProviderRuntimeAdapter; baseUrl?: string }>> = {`,
);
for (const [provider, overrides] of Object.entries(generatedModelProviderOverrides)) {
lines.push(
` ${JSON.stringify(provider)}: ${JSON.stringify(normalizeRuntimeOverrides(overrides))},`,
);
}
lines.push('};', '');
lines.push(
`export const GENERATED_MODELS_DEV_PROVIDER_FACTS: Record<${providerTypeUnion}, { id: string; name: string; api?: string; doc: string }> = {`,
);
for (const [provider, facts] of Object.entries(generatedProviders)) {
lines.push(` ${JSON.stringify(provider)}: ${JSON.stringify(facts)},`);
}
lines.push('};', '');
const metadataText = completeGeneratedModule(lines.join('\n'));
const writes = [{ path: outputPath, text: metadataText }];
if (pricingOutputPath) {
const pricingText = completeGeneratedModule(buildPricingModule(generatedPricing, source));
writes.push({ path: pricingOutputPath, text: pricingText });
}
if (source.snapshotWrite) writes.push(source.snapshotWrite);
await replaceFilesTransactionally(writes);
}
// `options.onReject` decides what an unprojectable provider or model costs. A
// refresh has none, so the first bad shape aborts the whole snapshot rather
// than silently committing a catalog with a hole in it. The drift report
// passes one, because there a bad shape is the finding it exists to print and
// must not stop it comparing everything else.
function buildProjection(catalog, options = {}) {
const onReject = options.onReject;
const metadata = {};
const pricing = [];
const providerFacts = {};
const providerOverrides = {};
for (const [providerType, sourceId] of Object.entries(PROVIDERS)) {
const provider = catalog[sourceId];
try {
assertModelsDevProvider(sourceId, provider);
} catch (error) {
if (!onReject) throw error;
onReject('provider', providerType, error);
continue;
}
providerFacts[providerType] = {
id: provider.id,
name: provider.name,
...(typeof provider.api === 'string' ? { api: provider.api } : {}),
doc: provider.doc,
};
metadata[providerType] = {};
providerOverrides[providerType] = {};
const priced = !PRICING_EXCLUDED_PROVIDER_TYPES.has(providerType);
const models = Object.entries(provider.models).sort(([left], [right]) =>
left.localeCompare(right),
);
for (const [id, model] of models) {
let projected;
try {
projected = {
metadata: toMetadata(sourceId, id, provider, model),
override:
model.provider === undefined
? undefined
: toModelProviderOverride(sourceId, id, model.provider),
pricing: priced ? toPricing(providerType, id, model) : undefined,
};
} catch (error) {
if (!onReject) throw error;
onReject('model', `${providerType}/${id}`, error);
continue;
}
metadata[providerType][id] = projected.metadata;
if (projected.override !== undefined)
providerOverrides[providerType][id] = projected.override;
if (projected.pricing !== undefined) pricing.push(projected.pricing);
}
}
return { metadata, pricing, providerFacts, providerOverrides };
}
async function readUpstream(refreshInputPath) {
if (refreshInputPath) {
return {
text: await readFile(refreshInputPath, 'utf8'),
etag: null,
retrievedAt: new Date().toISOString(),
};
}
const response = await fetch(SOURCE_URL, { signal: AbortSignal.timeout(10_000) });
if (!response.ok) throw new Error(`models.dev returned HTTP ${response.status}`);
return {
text: await response.text(),
etag: response.headers.get('etag'),
retrievedAt: new Date(response.headers.get('date') ?? Date.now()).toISOString(),
};
}
async function refreshSnapshot(snapshotPath, refreshInputPath, options = {}) {
const { text: sourceText, etag: sourceEtag, retrievedAt } = await readUpstream(refreshInputPath);
const projection = buildProjection(selectModelsDevCatalog(JSON.parse(sourceText)));
if (!options.acceptUpstreamRemovals) {
const previous = await loadSnapshotIfPresent(snapshotPath);
if (previous) assertProjectionDoesNotShrink(previous.projection, projection);
}
const projectionText = JSON.stringify(projection);
const snapshot = {
formatVersion: 1,
sourceUrl: SOURCE_URL,
origin: {
kind: 'models-dev-response',
retrievedAt,
etag: sourceEtag,
responseSha256: sha256(sourceText),
},
projectionSha256: sha256(projectionText),
projection,
};
await mkdir(dirname(snapshotPath), { recursive: true });
return {
projection,
snapshotDigest: snapshot.projectionSha256,
snapshotLabel: snapshotPath,
snapshotWrite: { path: snapshotPath, text: `${JSON.stringify(snapshot, null, 2)}\n` },
};
}
function assertProjectionDoesNotShrink(previous, next) {
const removals = collectProjectionRemovals(previous, next);
if (removals.length === 0) return;
throw new Error(
`models.dev refresh would remove committed projection paths: ${removals.join(', ')}; inspect the upstream change and rerun with --accept-upstream-removals to acknowledge it`,
);
}
// `--check` only proves the generated modules match the committed snapshot.
// Nothing compared that snapshot against models.dev, which is how it stayed
// weeks behind upstream without anything reporting it. This walks the two one
// model at a time, so a shape the projector rejects becomes its own finding
// instead of aborting the whole comparison the way a refresh does.
const DRIFT_LIST_LIMIT = 20;
// Every section of the projection, flattened to one value per entity. The
// report reads sections through this table instead of naming them itself,
// which is how it went out comparing only two of the four. A section added to
// buildProjection is compared here without touching the comparison.
const PROJECTION_SECTIONS = {
metadata: entitiesByProviderAndModel,
// modelKey is `${providerType}:${id}`, so replacing the first colon yields
// the label every other section already uses.
pricing: (section) => new Map(section.map((entry) => [entry.modelKey.replace(':', '/'), entry])),
providerFacts: (section) => new Map(Object.entries(section)),
providerOverrides: entitiesByProviderAndModel,
};
async function collectDrift(snapshot, refreshInputPath) {
const rejectedProviders = [];
const rejectedModels = [];
const rejected = new Set();
// The raw catalog, not the selected one: a provider that vanished upstream
// is the report's most important finding, and selection throws on it.
const upstream = buildProjection(JSON.parse((await readUpstream(refreshInputPath)).text), {
onReject: (kind, label, error) => {
rejected.add(label);
(kind === 'provider' ? rejectedProviders : rejectedModels).push(`${label}: ${error.message}`);
},
});
const previous = projectionEntities(snapshot.projection);
const next = projectionEntities(upstream);
const report = { rejectedProviders, rejectedModels, added: [], removed: [], changed: [] };
for (const label of [...new Set([...previous.keys(), ...next.keys()])].sort()) {
const before = previous.get(label);
const after = next.get(label);
if (before === undefined) {
report.added.push(label);
continue;
}
if (after === undefined) {
// A shape the projector rejected already has its own finding above. It
// is not upstream saying the entity is gone.
if (!isRejected(label, rejected)) report.removed.push(label);
continue;
}
const fields = driftedFields(before, after);
if (fields.length > 0) report.changed.push(`${label}: ${fields.join(', ')}`);
}
const drifted = Object.values(report).some((entries) => entries.length > 0);
return { ...report, drifted };
}
function entitiesByProviderAndModel(section) {
const entities = new Map();
for (const [providerType, models] of Object.entries(section)) {
for (const [id, value] of Object.entries(models)) entities.set(`${providerType}/${id}`, value);
}
return entities;
}
function projectionEntities(projection) {
const entities = new Map();
for (const [section, flatten] of Object.entries(PROJECTION_SECTIONS)) {
for (const [label, value] of flatten(projection[section])) {
const entity = entities.get(label);
if (entity) entity[section] = value;
else entities.set(label, { [section]: value });
}
}
return entities;
}
function isRejected(label, rejected) {
if (rejected.has(label)) return true;
const slash = label.indexOf('/');
return slash !== -1 && rejected.has(label.slice(0, slash));
}
function driftedFields(previous, next) {
const fields = [];
for (const section of Object.keys(PROJECTION_SECTIONS)) {
const before = previous[section];
const after = next[section];
if (sameValue(before, after)) continue;
if (isPlainObject(before) && isPlainObject(after)) {
const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort();
for (const key of keys) {
if (!sameValue(before[key], after[key])) fields.push(`${section}.${key}`);
}
continue;
}
fields.push(section);
}
return fields;
}
function isPlainObject(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
// Both sides are projector output, so their keys are already in one order.
function sameValue(left, right) {
return JSON.stringify(left) === JSON.stringify(right);
}
function formatDrift(report) {
const lines = [];
for (const [label, entries] of [
['providers the projector rejects', report.rejectedProviders],
['models the projector rejects', report.rejectedModels],
['entries upstream has and the snapshot does not', report.added],
['entries the snapshot has and upstream does not', report.removed],
['entries whose projection changed', report.changed],
]) {
if (entries.length === 0) continue;
lines.push(`${label}: ${entries.length}`);
for (const entry of entries.slice(0, DRIFT_LIST_LIMIT)) lines.push(` ${entry}`);
if (entries.length > DRIFT_LIST_LIMIT) {
lines.push(` ... and ${entries.length - DRIFT_LIST_LIMIT} more`);
}
}
if (!report.drifted) return `${SOURCE_URL} matches the committed snapshot.`;
return [`${SOURCE_URL} has drifted from the committed snapshot.`, ...lines].join('\n');
}
async function replaceFilesTransactionally(writes) {
if (new Set(writes.map((write) => write.path)).size !== writes.length) {
throw new Error('model metadata outputs must use distinct paths');
}
// Later workspace prebuilds may sync after an earlier workspace has already
// compiled. Preserve byte-identical targets so their mtimes cannot make that
// already-built workspace look stale.
const changedWrites = (
await Promise.all(
writes.map(async (write) => {
try {
return (await readFile(write.path)).equals(Buffer.from(write.text)) ? undefined : write;
} catch (error) {
if (error?.code === 'ENOENT') return write;
throw error;
}
}),
)
).filter(Boolean);
if (changedWrites.length === 0) return;
const transactionId = `${process.pid}-${randomUUID()}`;
const entries = changedWrites.map((write) => ({
...write,
stagedPath: `${write.path}.tmp-${transactionId}`,
backupPath: `${write.path}.bak-${transactionId}`,
hadOriginal: false,
installed: false,
}));
let committed = false;
try {
// Stage every byte before replacing any target. Missing/unwritable output
// directories therefore leave the existing snapshot and outputs intact.
const stageResults = await Promise.allSettled(
entries.map((entry) => writeFile(entry.stagedPath, entry.text, { flag: 'wx' })),
);
const stageFailure = stageResults.find((result) => result.status === 'rejected');
if (stageFailure) throw stageFailure.reason;
for (const entry of entries) {
try {
await rename(entry.path, entry.backupPath);
entry.hadOriginal = true;
} catch (error) {
if (error?.code !== 'ENOENT') throw error;
}
await rename(entry.stagedPath, entry.path);
entry.installed = true;
}
committed = true;
} catch (error) {
let rollbackError;
for (const entry of [...entries].reverse()) {
try {
if (entry.installed) await rm(entry.path, { force: true });
if (entry.hadOriginal) await rename(entry.backupPath, entry.path);
} catch (candidate) {
rollbackError ??= candidate;
}
}
if (rollbackError) {
throw new AggregateError(
[error, rollbackError],
'model metadata transaction rollback failed',
);
}
throw error;
} finally {
const cleanupPaths = entries.flatMap((entry) => [
entry.stagedPath,
...(committed ? [entry.backupPath] : []),
]);
await Promise.all(cleanupPaths.map((path) => rm(path, { force: true }).catch(() => {})));
}
}
async function loadSnapshot(snapshotPath) {
const snapshot = JSON.parse(await readFile(snapshotPath, 'utf8'));
if (
snapshot?.formatVersion !== 1 ||
snapshot.sourceUrl !== SOURCE_URL ||
!snapshot.origin ||
typeof snapshot.origin !== 'object' ||
Array.isArray(snapshot.origin) ||
typeof snapshot.projectionSha256 !== 'string' ||
!snapshot.projection ||
typeof snapshot.projection !== 'object' ||
Array.isArray(snapshot.projection)
) {
throw new Error(`models.dev snapshot ${snapshotPath} has an unsupported shape`);
}
const actualDigest = sha256(JSON.stringify(snapshot.projection));
if (actualDigest !== snapshot.projectionSha256) {
throw new Error(
`models.dev snapshot ${snapshotPath} digest mismatch: expected ${snapshot.projectionSha256}, got ${actualDigest}`,
);
}
return {
projection: snapshot.projection,
snapshotDigest: actualDigest,
snapshotLabel: snapshotPath,
};
}
async function loadSnapshotIfPresent(snapshotPath) {
try {
return await loadSnapshot(snapshotPath);
} catch (error) {
if (error?.code === 'ENOENT') return undefined;
throw error;
}
}
function sha256(value) {
return createHash('sha256').update(value).digest('hex');
}
function completeGeneratedModule(source) {
return source.endsWith('\n') ? source : `${source}\n`;
}
async function assertGeneratedOutputs(metadataPath, pricingPath, source) {
const metadataSource = await readFile(metadataPath, 'utf8');
assert.ok(
metadataSource.includes(
`// Snapshot: ${source.snapshotLabel} (SHA-256 ${source.snapshotDigest}).`,
),
`${metadataPath} is stale; run npm run sync:model-metadata`,
);
const metadataModule = await loadTypeScriptModule(metadataSource);
assert.deepEqual(
metadataModule.GENERATED_MODELS_DEV_METADATA,
source.projection.metadata,
`${metadataPath} is stale; run npm run sync:model-metadata`,
);
assert.deepEqual(
metadataModule.GENERATED_MODELS_DEV_MODEL_PROVIDER_OVERRIDES,
Object.fromEntries(
Object.entries(source.projection.providerOverrides).map(([provider, overrides]) => [
provider,
normalizeRuntimeOverrides(overrides),
]),
),
`${metadataPath} is stale; run npm run sync:model-metadata`,
);
assert.deepEqual(
metadataModule.GENERATED_MODELS_DEV_PROVIDER_FACTS,
source.projection.providerFacts,
`${metadataPath} is stale; run npm run sync:model-metadata`,
);
if (!pricingPath) return;
const pricingSource = await readFile(pricingPath, 'utf8');
assert.ok(
pricingSource.includes(
`// Snapshot: ${source.snapshotLabel} (SHA-256 ${source.snapshotDigest}).`,
),
`${pricingPath} is stale; run npm run sync:model-metadata`,
);
const pricingModule = await loadTypeScriptModule(pricingSource);
assert.deepEqual(
pricingModule.GENERATED_MODEL_PRICING,
source.projection.pricing,
`${pricingPath} is stale; run npm run sync:model-metadata`,
);
}
export async function loadTypeScriptModule(source) {
const javascript = stripTypeScriptTypes(source, { mode: 'strip' });
return import(`data:text/javascript;base64,${Buffer.from(javascript).toString('base64')}`);
}
function toModelProviderOverride(providerId, modelId, override) {
if (
!override ||
typeof override !== 'object' ||
typeof override.npm !== 'string' ||
(override.api !== undefined && typeof override.api !== 'string')
) {
throw new Error(
`models.dev model ${providerId}/${modelId} has an unsupported provider override`,
);
}
return { npm: override.npm, ...(override.api ? { api: override.api } : {}) };
}
function normalizeRuntimeOverrides(overrides) {
const adapters = {
'@ai-sdk/anthropic': { kind: 'anthropic', auth: 'api-key', normalizeBaseUrl: true },
'@ai-sdk/google': { kind: 'google', normalizeBaseUrl: false },
'@ai-sdk/openai': { kind: 'openai' },
'@ai-sdk/openai-compatible': { kind: 'openai-compatible', name: 'provider' },
};
return Object.fromEntries(
Object.entries(overrides).map(([modelId, override]) => {
if (!Object.hasOwn(adapters, override.npm)) {
throw new Error(`models.dev model ${modelId} uses unsupported SDK ${override.npm}`);
}
return [
modelId,
{ adapter: adapters[override.npm], ...(override.api ? { baseUrl: override.api } : {}) },
];
}),
);
}
export function toPricing(providerType, modelId, model) {
const cost = model?.cost;
if (cost === undefined) return undefined;
if (!cost || typeof cost !== 'object' || Array.isArray(cost)) {
throw new Error(`models.dev model ${providerType}/${modelId} has an unsupported cost shape`);
}
// PricingConfig and computeCost() currently represent one flat rate. Do
// not publish a base rate for a model whose actual price depends on input
// volume or an explicit tier table; that would systematically undercharge
// long-context requests until the runtime can select a tier by usage.
if (
Object.prototype.hasOwnProperty.call(cost, 'context_over_200k') ||
Object.prototype.hasOwnProperty.call(cost, 'tiers')
) {
return undefined;
}
const inputUsdPer1M = priceNumber(providerType, modelId, cost.input, 'input');
const outputUsdPer1M = priceNumber(providerType, modelId, cost.output, 'output');
const cacheReadUsdPer1M = optionalPriceNumber(
providerType,
modelId,
cost.cache_read,
'cache_read',
);
const cacheWriteUsdPer1M = optionalPriceNumber(
providerType,
modelId,
cost.cache_write,
'cache_write',
);
return {
modelKey: `${providerType}:${modelId}`,
inputUsdPer1M,
outputUsdPer1M,
...(cacheReadUsdPer1M !== undefined ? { cacheReadUsdPer1M } : {}),
...(cacheWriteUsdPer1M !== undefined ? { cacheWriteUsdPer1M } : {}),
};
}
function priceNumber(providerType, modelId, value, field) {
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
throw new Error(`models.dev model ${providerType}/${modelId} has an unsupported cost.${field}`);
}
return value;
}
function optionalPriceNumber(providerType, modelId, value, field) {
return value === undefined ? undefined : priceNumber(providerType, modelId, value, field);
}
// Every generated file names the exact repository-contained input projection.
// The copyright line is required by models.dev's MIT license and is repeated
// in LICENSE under THIRD-PARTY COMPONENTS. The digest binds generated output
// to the snapshot bytes that ship in the same source archive.
function snapshotHeader(handEditLine, source) {
return [
`// Generated by scripts/sync-model-metadata.mjs from ${SOURCE_URL}.`,
'// models.dev is the upstream refresh source; this committed snapshot is the build input.',
`// Snapshot: ${source.snapshotLabel} (SHA-256 ${source.snapshotDigest}).`,
'// Upstream: anomalyco/models.dev (https://github.com/anomalyco/models.dev), MIT,',
'// Copyright (c) 2025 models.dev. See LICENSE, THIRD-PARTY COMPONENTS.',
handEditLine,
];
}
function buildPricingModule(pricing, source) {
const lines = [
...snapshotHeader(
'// Do not edit by hand; special access-path pricing belongs in builtin-pricing.ts.',
source,
),
"import type { PricingConfig } from '@maka/core/usage-stats/types';",
'',
'export const GENERATED_MODEL_PRICING: readonly PricingConfig[] = [',
];
for (const entry of pricing) lines.push(` ${JSON.stringify(entry)},`);
lines.push('];', '');
return lines.join('\n');
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
// Drift is a finding, not a crash. Exit 2 so a caller can tell "upstream
// moved" from "this command failed"; both reported 1 before, which made the
// difference unreadable to the one job that has to act on it.
if ((await main())?.drifted) process.exitCode = 2;
}
function option(name, argv) {
const index = argv.indexOf(name);
if (index === -1) return undefined;
const value = argv[index + 1];
if (!value || value.startsWith('--')) throw new Error(`${name} requires a value`);
return value;
}