@xemahq/repo-build-tooling 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,491 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate the per-service `BiomeServiceDescriptor` bootstrap constant that
4
+ * `XemaServiceModule.forBiome(...)` consumes — from the AUTHORITATIVE source,
5
+ * each biome's `xema-biome.json`.
6
+ *
7
+ * Why: today a service's identity (name / version / dependencies) is declared
8
+ * in `xema-biome.json` AND re-typed as string literals inside `app.module.ts`
9
+ * (`ServiceRegistryModule` descriptor + `IdentityBootstrapModule`). That
10
+ * duplication drifts. This codegen makes the manifest the single source of
11
+ * truth: it emits `src/generated/<service>.bootstrap.generated.ts` next to each
12
+ * shipped API, and `app.module.ts` imports the constant instead of repeating
13
+ * the strings. A developer changes name/version/deps in ONE place — the manifest.
14
+ *
15
+ * Manifest fields read — the v5 COMPONENT contract (`xema.components[]`). The
16
+ * v4 `xema.ships.apis[]` spelling this used to read is RETIRED and matches
17
+ * nothing in the fleet.
18
+ *
19
+ * - top-level `version` → descriptor.semver / biomeVersion (required)
20
+ * - `components[].kind === 'service'` → the components this emits for
21
+ * - `components[].protocol.serviceName` → descriptor.name (falls back to `key`)
22
+ * - `components[].artifact.path` → where the file is written (required)
23
+ * - `components[].protocol.displayName` → descriptor.displayName
24
+ * (optional; falls back to `<xema.displayName> API`)
25
+ * - `components[].protocol.description` → descriptor.description (optional)
26
+ * — the ONE authored copy `main.ts` reads for
27
+ * the OpenAPI `info.description`
28
+ * - `components[].protocol.requiresServices` → descriptor.requiresServices
29
+ * (optional; default ['identity-api'])
30
+ * - `components[].protocol.capabilities` → descriptor.protocols (optional)
31
+ * - `components[].protocol.exposesCapabilities` → descriptor.exposesCapabilities
32
+ * (optional; falls back to `xema.exposesCapabilities`)
33
+ * - `components[].requirements.scaling.concurrency.maximumPerInstance`
34
+ * → descriptor.maximumConcurrentRequests (required positive
35
+ * integer; a service without one is refused)
36
+ * - `components[].kind === 'worker'` → a `XemaWorkerDescriptor` constant in
37
+ * `<package>/src/generated/<key>.worker-descriptor.generated.ts`
38
+ * (name = key, requiresServices = protocol.requiresServices,
39
+ * maximumConcurrentActivities = maximumPerInstance, and for a
40
+ * Nexus provider maximumConcurrentNexusOperations =
41
+ * maximumNexusOperationsPerInstance, required, no default). The package is
42
+ * the worker's `artifact.path`, or — for a `shared-oci-image`
43
+ * worker — the path of the component whose image it shares.
44
+ * Like services, only emitted into a package that depends on
45
+ * `@xemahq/xema-service-nest`.
46
+ * - `xema.id` → descriptor.biomeId (required)
47
+ * - `xema.capabilityDomain` → descriptor.capabilityDomain (optional)
48
+ *
49
+ * v4 also carried a per-API `serviceKind`; v5 carries none and needs none —
50
+ * every generated descriptor in the fleet is `ServiceKind.BiomeApi`, the
51
+ * default. `displayName` is the opposite case and is why this port needed a
52
+ * kernel change first: it is REGISTERED WITH identity-api, and 20 of the 50
53
+ * services declare one that differs from `<biome displayName> API`. Deriving it
54
+ * would have renamed them in the identity plane.
55
+ *
56
+ * `biomeId` + `biomeVersion` are NOT decoration: `isHttpCapabilityProjectionEnabled`
57
+ * in `@xemahq/xema-service-nest` gates HTTP capability projection on BOTH being
58
+ * present (a projected capability is stamped with `biome.id`/`biome.version`, and
59
+ * provenance cannot be guessed). A descriptor emitted without them silently
60
+ * projects nothing — so they are emitted unconditionally, and a manifest with no
61
+ * `xema.id` is a hard error rather than a descriptor that boots into a no-op.
62
+ * `capabilityDomain` is the `<domain>` segment of every projected ref; it is
63
+ * emitted only when the manifest declares it (the runtime defaults it to the
64
+ * biome id, so emitting the default would be redundant duplication).
65
+ *
66
+ * It runs in ONE repository: it walks every `xema-biome.json` under the
67
+ * repository root (the working directory, or `--root <dir>`) and writes next to
68
+ * the packages those manifests name. Every repository runs the same published
69
+ * copy, so no repository hand-writes a generated value.
70
+ *
71
+ * xema-service-bootstrap # write
72
+ * xema-service-bootstrap --check # CI: fail on drift
73
+ * xema-service-bootstrap --filter mailops-api # one component
74
+ * xema-service-bootstrap --root <repo-dir> # a repository other than cwd
75
+ */
76
+ import {
77
+ readdirSync,
78
+ readFileSync,
79
+ writeFileSync,
80
+ mkdirSync,
81
+ existsSync,
82
+ } from 'node:fs';
83
+ import { join, relative, dirname, resolve } from 'node:path';
84
+ import { renderServiceProtocols } from './service-bootstrap-protocols.mjs';
85
+ import { readMaximumPerInstance } from './service-bootstrap-concurrency.mjs';
86
+ import {
87
+ WORKER_DESCRIPTOR_FILE_SUFFIX,
88
+ renderWorkerDescriptorFile,
89
+ workerPackagePath,
90
+ workerRecord,
91
+ } from './worker-descriptor-generation.mjs';
92
+
93
+ const SKIP_DIRS = new Set(['node_modules', 'dist', '.git', '.turbo', 'coverage']);
94
+
95
+ const argv = process.argv.slice(2);
96
+ const CHECK = argv.includes('--check');
97
+ const optionValue = (flag) => {
98
+ const index = argv.indexOf(flag);
99
+ if (index < 0) return null;
100
+ const value = argv[index + 1];
101
+ if (value === undefined || value.startsWith('--')) {
102
+ console.error(`[xema-service-bootstrap] ${flag} needs a value.`);
103
+ process.exit(2);
104
+ }
105
+ return value;
106
+ };
107
+ const FILTER = optionValue('--filter');
108
+ const ROOT = resolve(optionValue('--root') ?? process.cwd());
109
+
110
+ /** Recursively collect files named `target` under `dir`. */
111
+ function walk(dir, target, out) {
112
+ let entries;
113
+ try {
114
+ entries = readdirSync(dir, { withFileTypes: true });
115
+ } catch {
116
+ return out;
117
+ }
118
+ for (const e of entries) {
119
+ if (e.isDirectory()) {
120
+ if (SKIP_DIRS.has(e.name)) continue;
121
+ walk(join(dir, e.name), target, out);
122
+ } else if (e.name === target) {
123
+ out.push(join(dir, e.name));
124
+ }
125
+ }
126
+ return out;
127
+ }
128
+
129
+ const CONST_KEY = (name) => name.toUpperCase().replace(/-/g, '_') + '_BOOTSTRAP';
130
+
131
+ const titleCase = (slug) =>
132
+ slug
133
+ .replace(/-/g, ' ')
134
+ .replace(/\b\w/g, (c) => c.toUpperCase());
135
+
136
+ /** Map a manifest into 0..n descriptor records (one per shipped API). */
137
+ function descriptorsFromManifest(manifestPath) {
138
+ const manifestDir = dirname(manifestPath);
139
+ let m;
140
+ try {
141
+ m = JSON.parse(readFileSync(manifestPath, 'utf8'));
142
+ } catch (error) {
143
+ throw new Error(
144
+ `[xema-service-bootstrap] cannot parse ${relative(ROOT, manifestPath)}: ${error.message}`,
145
+ { cause: error },
146
+ );
147
+ }
148
+ const xema = m?.xema ?? m;
149
+ const version = m?.version ?? xema?.version;
150
+ // v5: a service is a COMPONENT whose kind says so. `protocol.serviceName` is
151
+ // the wire name; `key` is its fallback, and the two agree fleet-wide today.
152
+ const apis = (xema?.components ?? [])
153
+ .filter((component) => component?.kind === 'service')
154
+ .map((component) => ({
155
+ name: component?.protocol?.serviceName ?? component?.key,
156
+ path: component?.artifact?.path,
157
+ displayName: component?.protocol?.displayName,
158
+ description: component?.protocol?.description,
159
+ requiresServices: component?.protocol?.requiresServices,
160
+ exposesCapabilities: component?.protocol?.exposesCapabilities,
161
+ protocols: component?.protocol?.capabilities,
162
+ component,
163
+ }));
164
+ const out = [];
165
+ for (const api of apis) {
166
+ if (!api?.name || !api?.path) continue;
167
+ if (FILTER && api.name !== FILTER) continue;
168
+ // Only emit for services that OPT IN by declaring the dependency. A biome
169
+ // may ship a worker / non-migrated API in the same manifest; generating a
170
+ // descriptor that imports `@xemahq/xema-service-nest` into a service that
171
+ // doesn't depend on it would break that service's build. The dep is the
172
+ // single signal that a service consumes `XemaServiceModule.forBiome`.
173
+ if (!packageOptsIn(join(manifestDir, api.path, 'package.json'))) continue;
174
+ if (!version) {
175
+ throw new Error(
176
+ `[xema-service-bootstrap] ${relative(ROOT, manifestPath)} ships api "${api.name}" but has no top-level "version".`,
177
+ );
178
+ }
179
+ const displayName =
180
+ api.displayName ??
181
+ (xema?.displayName ? `${xema.displayName} API` : `${titleCase(api.name)}`);
182
+ // v5 carries no per-component serviceKind, and needs none: every generated
183
+ // descriptor in the fleet is a biome API.
184
+ const serviceKind = 'biome-api';
185
+ const requiresServices =
186
+ Array.isArray(api.requiresServices) && api.requiresServices.length > 0
187
+ ? api.requiresServices
188
+ : ['identity-api'];
189
+ // SORTED, so the generated file is a function of the declaration SET rather
190
+ // than of the order someone happened to type it in. Without this, moving a
191
+ // ref inside the manifest churns a generated file for no semantic reason —
192
+ // and `--check` would fail on a diff that means nothing.
193
+ const exposesCapabilities = [
194
+ ...(Array.isArray(api.exposesCapabilities)
195
+ ? api.exposesCapabilities
196
+ : Array.isArray(xema?.exposesCapabilities)
197
+ ? xema.exposesCapabilities
198
+ : []),
199
+ ].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
200
+ const protocols = api.protocols;
201
+ const biomeId = xema?.id;
202
+ if (typeof biomeId !== 'string' || biomeId.trim() === '') {
203
+ throw new Error(
204
+ `[xema-service-bootstrap] ${relative(ROOT, manifestPath)} ships api "${api.name}" but has no "xema.id". ` +
205
+ 'The biome id is the provenance stamped on every projected capability — ' +
206
+ 'without it `XemaServiceModule.forBiome` disables HTTP capability projection entirely.',
207
+ );
208
+ }
209
+ const capabilityDomain =
210
+ typeof xema?.capabilityDomain === 'string' &&
211
+ xema.capabilityDomain.trim() !== ''
212
+ ? xema.capabilityDomain
213
+ : null;
214
+ const maximumConcurrentRequests = readMaximumPerInstance(
215
+ api.component,
216
+ relative(ROOT, manifestPath),
217
+ );
218
+ const apiSrcDir = join(manifestDir, api.path, 'src');
219
+ out.push({
220
+ name: api.name,
221
+ maximumConcurrentRequests,
222
+ semver: String(version),
223
+ serviceKind,
224
+ displayName,
225
+ description: api.description,
226
+ requiresServices,
227
+ exposesCapabilities,
228
+ protocols,
229
+ biomeId,
230
+ biomeVersion: String(version),
231
+ capabilityDomain,
232
+ outFile: join(
233
+ apiSrcDir,
234
+ 'generated',
235
+ `${api.name}.bootstrap.generated.ts`,
236
+ ),
237
+ manifestRel: relative(ROOT, manifestPath),
238
+ apiSrcExists: existsSync(apiSrcDir),
239
+ packageJsonPath: join(manifestDir, api.path, 'package.json'),
240
+ });
241
+ }
242
+ return out;
243
+ }
244
+
245
+ /** The package.json dependency that opts a package in to generation. */
246
+ function packageOptsIn(packageJsonPath) {
247
+ try {
248
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
249
+ return Boolean((pkg.dependencies ?? {})[SERVICE_NEST_PACKAGE]);
250
+ } catch {
251
+ return false;
252
+ }
253
+ }
254
+
255
+ /** Map a manifest into 0..n worker descriptor records (one per worker component). */
256
+ function workerDescriptorsFromManifest(manifestPath) {
257
+ const manifestDir = dirname(manifestPath);
258
+ let m;
259
+ try {
260
+ m = JSON.parse(readFileSync(manifestPath, 'utf8'));
261
+ } catch (error) {
262
+ throw new Error(
263
+ `[xema-service-bootstrap] cannot parse ${relative(ROOT, manifestPath)}: ${error.message}`,
264
+ { cause: error },
265
+ );
266
+ }
267
+ const components = (m?.xema ?? m)?.components ?? [];
268
+ const manifestRel = relative(ROOT, manifestPath);
269
+ const out = [];
270
+ for (const component of components) {
271
+ if (component?.kind !== 'worker' || !component?.key) continue;
272
+ if (FILTER && component.key !== FILTER) continue;
273
+ const packageDir = join(manifestDir, workerPackagePath(component, components, manifestRel));
274
+ // Same opt-in as services: no dependency on the SDK, no generated import. Checked
275
+ // BEFORE the record, so a worker that is not opted in never has to declare a limit.
276
+ if (!packageOptsIn(join(packageDir, 'package.json'))) continue;
277
+ const record = workerRecord(component, components, manifestRel);
278
+ const srcDir = join(packageDir, 'src');
279
+ out.push({
280
+ ...record,
281
+ manifestRel,
282
+ srcExists: existsSync(srcDir),
283
+ outFile: join(srcDir, 'generated', `${record.name}.${WORKER_DESCRIPTOR_FILE_SUFFIX}`),
284
+ render: () =>
285
+ renderWorkerDescriptorFile(record, {
286
+ manifestRel,
287
+ serviceNestPackage: SERVICE_NEST_PACKAGE,
288
+ }),
289
+ });
290
+ }
291
+ return out;
292
+ }
293
+
294
+ // THE TWO PACKAGES THIS GENERATOR EMITS AN IMPORT FOR, named once.
295
+ //
296
+ // The protocol import was hardcoded as `@xemahq/kernel-contracts/service-registry`
297
+ // and went stale when the kernel package was renamed to `@xemahq/contracts`.
298
+ // It stayed invisible because that import is emitted ONLY for a service that
299
+ // declares protocols, and for a long time none did.
300
+ //
301
+ // What it did when it finally fired is worse than emitting a dead import: the
302
+ // generator OVERWROTE resource-governance-api's committed, CORRECT file with
303
+ // the retired name, and `--check` therefore reported that correct file as
304
+ // "out of date" and told the reader to regenerate it. The gate was demanding a
305
+ // regression, and obeying it broke xema-base's build.
306
+ const SERVICE_NEST_PACKAGE = '@xemahq/xema-service-nest';
307
+ const KERNEL_CONTRACTS_PACKAGE = '@xemahq/contracts';
308
+ const SERVICE_REGISTRY_SUBPATH = `${KERNEL_CONTRACTS_PACKAGE}/service-registry`;
309
+
310
+ const SERVICE_KIND_ENUM = {
311
+ 'platform-service': 'ServiceKind.PlatformService',
312
+ 'biome-api': 'ServiceKind.BiomeApi',
313
+ cli: 'ServiceKind.Cli',
314
+ };
315
+
316
+ // A generator that emits an import nobody declares produces a file that does not
317
+ // compile, in a repository the generator does not build. This makes that a
318
+ // GENERATION-time failure instead, naming the service and the package.
319
+ //
320
+ // It grades ONLY the kernel-contracts import, and deliberately not the
321
+ // `@xemahq/xema-service-nest` one: a service reaches this function only by
322
+ // DECLARING that exact package — that is the opt-in signal above — so asserting
323
+ // it here could never fail, and a check that cannot fail is worse than no check
324
+ // because it reads as protection in review.
325
+ //
326
+ // It grades DECLARATIONS, not resolution: the constitution already requires that
327
+ // every package list every `@xemahq/*` it imports, so a declared dependency is
328
+ // the right thing to check and it needs no install to check it. Measured when
329
+ // this landed: all 53 generated services declare `@xemahq/xema-service-nest`,
330
+ // and the one service with a protocol import declares the kernel package — so
331
+ // this reddens nothing that was already correct.
332
+ function assertKernelContractsDeclared(d) {
333
+ if (!existsSync(d.packageJsonPath)) {
334
+ throw new Error(
335
+ `[xema-service-bootstrap] ${d.name}: no package.json at ${relative(ROOT, d.packageJsonPath)}, so the protocol import this generator emits cannot be checked against what the service declares.`,
336
+ );
337
+ }
338
+ const pkg = JSON.parse(readFileSync(d.packageJsonPath, 'utf8'));
339
+ const declared = new Set([
340
+ ...Object.keys(pkg.dependencies ?? {}),
341
+ ...Object.keys(pkg.peerDependencies ?? {}),
342
+ ]);
343
+ if (!declared.has(KERNEL_CONTRACTS_PACKAGE)) {
344
+ throw new Error(
345
+ `[xema-service-bootstrap] ${d.name} (${relative(ROOT, d.packageJsonPath)}) declares service protocols, so this generator emits an import from ` +
346
+ `'${SERVICE_REGISTRY_SUBPATH}' — but the service does not declare ${KERNEL_CONTRACTS_PACKAGE}. ` +
347
+ 'Either the kernel package was renamed and the constants at the top of this file are stale, or the service is missing a dependency. ' +
348
+ 'Both produce the same symptom — a generated file that will not compile — and both are fixed here, never in the generated file.',
349
+ );
350
+ }
351
+ }
352
+
353
+ function renderFile(d) {
354
+ const kindExpr = SERVICE_KIND_ENUM[d.serviceKind];
355
+ if (!kindExpr) {
356
+ throw new Error(
357
+ `[xema-service-bootstrap] ${d.manifestRel} api "${d.name}" has unknown serviceKind "${d.serviceKind}" (expected platform-service | biome-api | cli).`,
358
+ );
359
+ }
360
+ const arr = (xs) =>
361
+ xs.length === 0 ? '[]' : `[${xs.map((x) => `'${x}'`).join(', ')}]`;
362
+ const renderedProtocols = renderServiceProtocols(d.protocols);
363
+ if (renderedProtocols.imports.length > 0) {
364
+ assertKernelContractsDeclared(d);
365
+ }
366
+ const protocolImports =
367
+ renderedProtocols.imports.length === 0
368
+ ? ''
369
+ : `import {\n${renderedProtocols.imports.map((name) => ` ${name},`).join('\n')}\n} from '${SERVICE_REGISTRY_SUBPATH}';\n`;
370
+ return `// AUTO-GENERATED by xema-service-bootstrap (@xemahq/repo-build-tooling) — DO NOT EDIT.
371
+ // Source of truth: ${d.manifestRel} (xema.components[] + top-level version).
372
+ // Regenerate: \`pnpm exec xema-service-bootstrap\`.
373
+ ${protocolImports}import {
374
+ ServiceKind,
375
+ type BiomeServiceDescriptor,
376
+ } from '${SERVICE_NEST_PACKAGE}';
377
+
378
+ /**
379
+ * Bootstrap identity for ${d.name}, consumed by
380
+ * \`XemaServiceModule.forBiome(${CONST_KEY(d.name)})\`. Edit the biome manifest,
381
+ * not this file.
382
+ */
383
+ export const ${CONST_KEY(d.name)}: BiomeServiceDescriptor = {
384
+ name: '${d.name}',
385
+ semver: '${d.semver}',
386
+ serviceKind: ${kindExpr},
387
+ displayName: '${d.displayName.replace(/'/g, "\\'")}',
388
+ ${d.description ? ` // JSON.stringify, not a quoted literal: a lifted description carries real\n // newlines, and single quotes produced an unterminated string.\n description: ${JSON.stringify(d.description)},\n` : ''}\
389
+ maximumConcurrentRequests: ${d.maximumConcurrentRequests},
390
+ requiresServices: ${arr(d.requiresServices)},
391
+ exposesCapabilities: ${arr(d.exposesCapabilities)},${renderedProtocols.field}
392
+ biomeId: '${d.biomeId}',
393
+ biomeVersion: '${d.biomeVersion}',${
394
+ d.capabilityDomain === null
395
+ ? ''
396
+ : `\n capabilityDomain: '${d.capabilityDomain}',`
397
+ }
398
+ };
399
+ `;
400
+ }
401
+
402
+ const manifests = walk(ROOT, 'xema-biome.json', []);
403
+ const records = manifests.flatMap(descriptorsFromManifest);
404
+ const workerRecords = manifests.flatMap(workerDescriptorsFromManifest);
405
+
406
+ // A SCAN THAT FINDS NOTHING IS A FAILURE, NEVER AN "OK".
407
+ //
408
+ // This generator once read a retired manifest spelling, matched nothing in a
409
+ // fleet it walked, and reported "OK — 0 file(s) up to date": `--check` was a
410
+ // gate that could not fail. So an empty result exits non-zero: no manifest
411
+ // under the root means it ran in the wrong directory, and manifests with no
412
+ // opted-in service or worker mean the reader and the manifests disagree (or the
413
+ // repository has nothing to generate and must not run this).
414
+ if (manifests.length === 0) {
415
+ console.error(
416
+ `[xema-service-bootstrap] found no xema-biome.json under ${ROOT}. ` +
417
+ 'Run it from the repository root, or pass --root <repo-dir>.',
418
+ );
419
+ process.exit(1);
420
+ }
421
+ if (records.length === 0 && workerRecords.length === 0 && FILTER === null) {
422
+ console.error(
423
+ `[xema-service-bootstrap] walked ${manifests.length} xema-biome.json manifest(s) under ${ROOT} ` +
424
+ 'and produced ZERO service or worker descriptors. A descriptor is emitted for each ' +
425
+ '`xema.components[]` entry of kind `service` or `worker` whose package declares ' +
426
+ `${SERVICE_NEST_PACKAGE}; none here does, so this run would verify nothing.`,
427
+ );
428
+ process.exit(1);
429
+ }
430
+ if (FILTER !== null && records.length === 0 && workerRecords.length === 0) {
431
+ console.error(`[xema-service-bootstrap] --filter ${FILTER} matched no opted-in service or worker component.`);
432
+ process.exit(1);
433
+ }
434
+
435
+ let drift = 0;
436
+ let written = 0;
437
+ let skipped = 0;
438
+ let compared = 0;
439
+ const outputs = [
440
+ ...records.map((d) => ({
441
+ outFile: d.outFile,
442
+ srcExists: d.apiSrcExists,
443
+ render: () => renderFile(d),
444
+ })),
445
+ ...workerRecords,
446
+ ];
447
+ for (const d of outputs) {
448
+ if (!d.srcExists) {
449
+ // The manifest declares an api whose source isn't present in this
450
+ // checkout (carved-repo skew). Skip rather than emit into a phantom dir.
451
+ skipped++;
452
+ continue;
453
+ }
454
+ compared++;
455
+ const next = d.render();
456
+ const prev = existsSync(d.outFile) ? readFileSync(d.outFile, 'utf8') : null;
457
+ if (prev === next) continue;
458
+ if (CHECK) {
459
+ console.error(
460
+ `DRIFT: ${relative(ROOT, d.outFile)} is out of date (regenerate with \`pnpm exec xema-service-bootstrap\`).`,
461
+ );
462
+ drift++;
463
+ continue;
464
+ }
465
+ mkdirSync(dirname(d.outFile), { recursive: true });
466
+ writeFileSync(d.outFile, next, 'utf8');
467
+ console.log(`wrote ${relative(ROOT, d.outFile)}`);
468
+ written++;
469
+ }
470
+
471
+ // A RUN THAT COMPARES OR WRITES NOTHING IS A FAILURE: every record skipped because
472
+ // its source is missing would otherwise report OK (check) or "Done" (generate).
473
+ if (compared === 0) {
474
+ console.error(
475
+ `[xema-service-bootstrap] ${outputs.length} descriptor(s) found, but every one was skipped because its package has no src/ in this checkout; ` +
476
+ `${CHECK ? 'nothing was compared' : 'nothing was written'}. Run it in a checkout that contains the sources.`,
477
+ );
478
+ process.exit(1);
479
+ }
480
+
481
+ if (CHECK) {
482
+ if (drift > 0) {
483
+ console.error(`\n${drift} generated file(s) out of date.`);
484
+ process.exit(1);
485
+ }
486
+ console.log(`OK — ${records.length} service bootstrap and ${workerRecords.length} worker descriptor file(s) up to date.`);
487
+ } else {
488
+ console.log(
489
+ `Done — ${written} written, ${skipped} skipped (source not in checkout), ${outputs.length} total.`,
490
+ );
491
+ }