@ontrails/trails 0.2.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.
Files changed (121) hide show
  1. package/CHANGELOG.md +1906 -0
  2. package/README.md +48 -0
  3. package/bin/trails.ts +3 -0
  4. package/package.json +57 -0
  5. package/src/app.ts +167 -0
  6. package/src/clack.ts +111 -0
  7. package/src/cli.ts +308 -0
  8. package/src/completions.ts +431 -0
  9. package/src/lifecycle-source-io.ts +33 -0
  10. package/src/load-app-mirror.ts +202 -0
  11. package/src/local-state-io.ts +129 -0
  12. package/src/mcp-app.ts +42 -0
  13. package/src/mcp-options.ts +92 -0
  14. package/src/mcp.ts +8 -0
  15. package/src/project-writes.ts +377 -0
  16. package/src/regrade/audit.ts +571 -0
  17. package/src/regrade/config.ts +152 -0
  18. package/src/regrade/history.ts +636 -0
  19. package/src/regrade/lifecycle.ts +76 -0
  20. package/src/regrade/live-api-preserve.ts +123 -0
  21. package/src/regrade/plan-artifact.ts +515 -0
  22. package/src/regrade/plan-derivation.ts +301 -0
  23. package/src/regrade/prepared-run.ts +259 -0
  24. package/src/regrade/receipt-history.ts +446 -0
  25. package/src/regrade/source-transaction.ts +185 -0
  26. package/src/release/bindings.ts +58 -0
  27. package/src/release/changeset-packages.ts +99 -0
  28. package/src/release/check.ts +1191 -0
  29. package/src/release/cli-bundle.ts +575 -0
  30. package/src/release/config.ts +73 -0
  31. package/src/release/contract-facts.ts +425 -0
  32. package/src/release/homebrew.ts +221 -0
  33. package/src/release/index.ts +180 -0
  34. package/src/release/lock-roundtrip-smoke.ts +255 -0
  35. package/src/release/lock-roundtrip-workspace.ts +107 -0
  36. package/src/release/native-bun-publish.ts +964 -0
  37. package/src/release/native-bun-registry.ts +848 -0
  38. package/src/release/notes-cli.ts +171 -0
  39. package/src/release/notes.ts +390 -0
  40. package/src/release/pack-coherence.ts +455 -0
  41. package/src/release/package-route-facts.ts +146 -0
  42. package/src/release/packed-artifacts-smoke.ts +236 -0
  43. package/src/release/policy.ts +1780 -0
  44. package/src/release/semver.ts +104 -0
  45. package/src/release/smoke.ts +56 -0
  46. package/src/release/stable-version-release.ts +80 -0
  47. package/src/release/wayfinder-dogfood-smoke.ts +762 -0
  48. package/src/release/zero-line-transition.ts +68 -0
  49. package/src/retired-topo-command.ts +36 -0
  50. package/src/run-adapter-check.ts +76 -0
  51. package/src/run-argv.ts +133 -0
  52. package/src/run-collision.ts +126 -0
  53. package/src/run-completions-install.ts +179 -0
  54. package/src/run-example.ts +149 -0
  55. package/src/run-examples.ts +148 -0
  56. package/src/run-quiet.ts +75 -0
  57. package/src/run-regrade-progress.ts +47 -0
  58. package/src/run-release-check.ts +74 -0
  59. package/src/run-schema.ts +74 -0
  60. package/src/run-trace.ts +273 -0
  61. package/src/run-warden.ts +39 -0
  62. package/src/run-watch-project.ts +52 -0
  63. package/src/run-watch.ts +381 -0
  64. package/src/run-wayfind-outline.ts +170 -0
  65. package/src/scaffold-version-sync.ts +183 -0
  66. package/src/scaffold-versions.generated.ts +12 -0
  67. package/src/trails/adapter-check.ts +244 -0
  68. package/src/trails/add-surface.ts +816 -0
  69. package/src/trails/add-trail.ts +141 -0
  70. package/src/trails/add-verify.ts +252 -0
  71. package/src/trails/compile.ts +118 -0
  72. package/src/trails/completions-complete.ts +236 -0
  73. package/src/trails/completions.ts +47 -0
  74. package/src/trails/config-explain.ts +43 -0
  75. package/src/trails/create-adapter.ts +785 -0
  76. package/src/trails/create-scaffold.ts +1215 -0
  77. package/src/trails/create-versions.ts +62 -0
  78. package/src/trails/create.ts +652 -0
  79. package/src/trails/deprecate.ts +59 -0
  80. package/src/trails/dev-clean.ts +80 -0
  81. package/src/trails/dev-reset.ts +48 -0
  82. package/src/trails/dev-stats.ts +71 -0
  83. package/src/trails/dev-support.ts +360 -0
  84. package/src/trails/doctor.ts +77 -0
  85. package/src/trails/draft-promote.ts +949 -0
  86. package/src/trails/guide.ts +106 -0
  87. package/src/trails/load-app.ts +1145 -0
  88. package/src/trails/operator-context.ts +66 -0
  89. package/src/trails/project-context-output.ts +304 -0
  90. package/src/trails/project-context.ts +613 -0
  91. package/src/trails/project.ts +65 -0
  92. package/src/trails/regrade.ts +4951 -0
  93. package/src/trails/release-check.ts +113 -0
  94. package/src/trails/release-smoke.ts +49 -0
  95. package/src/trails/revise.ts +53 -0
  96. package/src/trails/root-dir.ts +21 -0
  97. package/src/trails/run-example.ts +592 -0
  98. package/src/trails/run-examples.ts +149 -0
  99. package/src/trails/run.ts +496 -0
  100. package/src/trails/scaffold-json.ts +60 -0
  101. package/src/trails/scaffold-topo-identity.ts +479 -0
  102. package/src/trails/survey.ts +990 -0
  103. package/src/trails/topo-activation.ts +14 -0
  104. package/src/trails/topo-constants.ts +2 -0
  105. package/src/trails/topo-history.ts +47 -0
  106. package/src/trails/topo-output-schemas.ts +259 -0
  107. package/src/trails/topo-pin.ts +38 -0
  108. package/src/trails/topo-read-support.ts +368 -0
  109. package/src/trails/topo-reports.ts +809 -0
  110. package/src/trails/topo-store-support.ts +323 -0
  111. package/src/trails/topo-support.ts +247 -0
  112. package/src/trails/topo-unpin.ts +61 -0
  113. package/src/trails/topo.ts +92 -0
  114. package/src/trails/validate.ts +348 -0
  115. package/src/trails/version-lifecycle-support.ts +936 -0
  116. package/src/trails/warden-guide.ts +134 -0
  117. package/src/trails/warden.ts +598 -0
  118. package/src/trails/wayfind-diff.ts +716 -0
  119. package/src/trails/wayfind-outline.ts +876 -0
  120. package/src/trails/wayfind.ts +1319 -0
  121. package/src/versions.ts +31 -0
@@ -0,0 +1,575 @@
1
+ import {
2
+ chmodSync,
3
+ cpSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ readFileSync,
8
+ readdirSync,
9
+ rmSync,
10
+ writeFileSync,
11
+ } from 'node:fs';
12
+ import { tmpdir } from 'node:os';
13
+ import { dirname, join, relative, resolve } from 'node:path';
14
+
15
+ export const trailsCliReleasePlatformValues = [
16
+ 'darwin-arm64',
17
+ 'darwin-x64',
18
+ 'linux-arm64',
19
+ 'linux-x64',
20
+ ] as const;
21
+
22
+ export type TrailsCliReleasePlatform =
23
+ (typeof trailsCliReleasePlatformValues)[number];
24
+
25
+ export interface TrailsCliReleasePlatformDescriptor {
26
+ readonly oxcParserBinding: string;
27
+ readonly oxcResolverBinding: string;
28
+ readonly platform: TrailsCliReleasePlatform;
29
+ }
30
+
31
+ export const trailsCliReleasePlatforms = [
32
+ {
33
+ oxcParserBinding: '@oxc-parser/binding-darwin-arm64',
34
+ oxcResolverBinding: '@oxc-resolver/binding-darwin-arm64',
35
+ platform: 'darwin-arm64',
36
+ },
37
+ {
38
+ oxcParserBinding: '@oxc-parser/binding-darwin-x64',
39
+ oxcResolverBinding: '@oxc-resolver/binding-darwin-x64',
40
+ platform: 'darwin-x64',
41
+ },
42
+ {
43
+ oxcParserBinding: '@oxc-parser/binding-linux-arm64-gnu',
44
+ oxcResolverBinding: '@oxc-resolver/binding-linux-arm64-gnu',
45
+ platform: 'linux-arm64',
46
+ },
47
+ {
48
+ oxcParserBinding: '@oxc-parser/binding-linux-x64-gnu',
49
+ oxcResolverBinding: '@oxc-resolver/binding-linux-x64-gnu',
50
+ platform: 'linux-x64',
51
+ },
52
+ ] as const satisfies readonly TrailsCliReleasePlatformDescriptor[];
53
+
54
+ export interface BuildTrailsCliReleaseOptions {
55
+ readonly outDir: string;
56
+ readonly platform?: TrailsCliReleasePlatform;
57
+ readonly repoRoot?: string;
58
+ readonly version?: string;
59
+ }
60
+
61
+ export interface TrailsCliReleaseArtifact {
62
+ readonly archiveName: string;
63
+ readonly archivePath: string;
64
+ readonly checksum: string;
65
+ readonly checksumPath: string;
66
+ readonly platform: TrailsCliReleasePlatform;
67
+ readonly version: string;
68
+ }
69
+
70
+ const DEFAULT_REPO_ROOT = resolve(import.meta.dir, '../../../..');
71
+ const textDecoder = new TextDecoder();
72
+ const BUNDLE_EXTERNALS = ['oxc-parser', 'oxc-resolver'] as const;
73
+
74
+ const platformDescriptor = (
75
+ platform: TrailsCliReleasePlatform
76
+ ): TrailsCliReleasePlatformDescriptor => {
77
+ const descriptor = trailsCliReleasePlatforms.find(
78
+ (candidate) => candidate.platform === platform
79
+ );
80
+ if (!descriptor) {
81
+ throw new Error(`Unsupported Trails CLI release platform: ${platform}`);
82
+ }
83
+ return descriptor;
84
+ };
85
+
86
+ export const currentTrailsCliReleasePlatform = (
87
+ platform = process.platform,
88
+ architecture = process.arch
89
+ ): TrailsCliReleasePlatform => {
90
+ const candidate = `${platform}-${architecture}`;
91
+ if (
92
+ trailsCliReleasePlatformValues.includes(
93
+ candidate as TrailsCliReleasePlatform
94
+ )
95
+ ) {
96
+ return candidate as TrailsCliReleasePlatform;
97
+ }
98
+ throw new Error(`Unsupported Trails CLI release host: ${candidate}`);
99
+ };
100
+
101
+ export const trailsCliArchiveName = (
102
+ version: string,
103
+ platform: TrailsCliReleasePlatform
104
+ ): string => `trails-v${version}-${platform}.tar.gz`;
105
+
106
+ export const trailsCliChecksumName = (version: string): string =>
107
+ `trails-v${version}-SHA256SUMS`;
108
+
109
+ export const expectedTrailsCliReleaseAssetNames = (
110
+ version: string
111
+ ): readonly string[] => [
112
+ ...trailsCliReleasePlatformValues.map((platform) =>
113
+ trailsCliArchiveName(version, platform)
114
+ ),
115
+ trailsCliChecksumName(version),
116
+ ];
117
+
118
+ const findPackageRoot = (entryPath: string, expectedName: string): string => {
119
+ let candidate = dirname(entryPath);
120
+ while (candidate !== dirname(candidate)) {
121
+ const packagePath = join(candidate, 'package.json');
122
+ if (existsSync(packagePath)) {
123
+ const packageJson = JSON.parse(readFileSync(packagePath, 'utf8')) as {
124
+ name?: string;
125
+ };
126
+ if (packageJson.name === expectedName) {
127
+ return candidate;
128
+ }
129
+ }
130
+ candidate = dirname(candidate);
131
+ }
132
+ throw new Error(`Could not locate package root for ${expectedName}`);
133
+ };
134
+
135
+ const resolvePackageRoot = (
136
+ packageName: string,
137
+ fromDirectory: string
138
+ ): string =>
139
+ findPackageRoot(Bun.resolveSync(packageName, fromDirectory), packageName);
140
+
141
+ const copyPackage = (
142
+ packageName: string,
143
+ sourceRoot: string,
144
+ bundleRoot: string
145
+ ): { readonly name: string; readonly version: string } => {
146
+ const scope = packageName.startsWith('@') ? packageName.split('/')[0] : null;
147
+ const target = join(bundleRoot, 'node_modules', packageName);
148
+ if (scope) {
149
+ mkdirSync(join(bundleRoot, 'node_modules', scope), { recursive: true });
150
+ }
151
+ cpSync(sourceRoot, target, { dereference: true, recursive: true });
152
+ const packageJson = JSON.parse(
153
+ readFileSync(join(sourceRoot, 'package.json'), 'utf8')
154
+ ) as { name?: string; version?: string };
155
+ if (packageJson.name !== packageName || !packageJson.version) {
156
+ throw new Error(`Invalid package metadata for ${packageName}`);
157
+ }
158
+ return { name: packageName, version: packageJson.version };
159
+ };
160
+
161
+ const launcherSource = `#!/bin/sh
162
+ set -eu
163
+
164
+ source_path=$0
165
+ while [ -L "$source_path" ]; do
166
+ source_dir=$(CDPATH= cd -P -- "$(dirname -- "$source_path")" && pwd)
167
+ source_path=$(readlink "$source_path")
168
+ case $source_path in
169
+ /*) ;;
170
+ *) source_path=$source_dir/$source_path ;;
171
+ esac
172
+ done
173
+ source_dir=$(CDPATH= cd -P -- "$(dirname -- "$source_path")" && pwd)
174
+ exec bun "$source_dir/../lib/trails.js" "$@"
175
+ `;
176
+
177
+ interface ArchiveEntry {
178
+ readonly bytes: Buffer;
179
+ readonly mode: number;
180
+ readonly path: string;
181
+ }
182
+
183
+ const collectArchiveEntries = (
184
+ rootDir: string,
185
+ archiveRoot: string
186
+ ): readonly ArchiveEntry[] => {
187
+ const entries: ArchiveEntry[] = [];
188
+ const visit = (directory: string): void => {
189
+ for (const entry of readdirSync(directory, {
190
+ withFileTypes: true,
191
+ }).toSorted((left, right) => left.name.localeCompare(right.name))) {
192
+ const absolutePath = join(directory, entry.name);
193
+ if (entry.isDirectory()) {
194
+ visit(absolutePath);
195
+ continue;
196
+ }
197
+ if (!entry.isFile()) {
198
+ throw new Error(
199
+ `Release bundles cannot contain links: ${absolutePath}`
200
+ );
201
+ }
202
+ const relativePath = relative(rootDir, absolutePath).replaceAll(
203
+ '\\',
204
+ '/'
205
+ );
206
+ entries.push({
207
+ bytes: readFileSync(absolutePath),
208
+ mode: relativePath === 'bin/trails' ? 0o755 : 0o644,
209
+ path: `${archiveRoot}/${relativePath}`,
210
+ });
211
+ }
212
+ };
213
+ visit(rootDir);
214
+ return entries;
215
+ };
216
+
217
+ const writeText = (
218
+ target: Buffer,
219
+ offset: number,
220
+ length: number,
221
+ value: string
222
+ ): void => {
223
+ const encoded = Buffer.from(value);
224
+ if (encoded.length > length) {
225
+ throw new Error(`Tar header field is too long: ${value}`);
226
+ }
227
+ encoded.copy(target, offset);
228
+ };
229
+
230
+ const writeOctal = (
231
+ target: Buffer,
232
+ offset: number,
233
+ length: number,
234
+ value: number
235
+ ): void => {
236
+ const encoded = value.toString(8).padStart(length - 1, '0');
237
+ writeText(target, offset, length - 1, encoded);
238
+ };
239
+
240
+ const splitTarPath = (
241
+ path: string
242
+ ): { readonly name: string; readonly prefix: string } => {
243
+ if (Buffer.byteLength(path) <= 100) {
244
+ return { name: path, prefix: '' };
245
+ }
246
+ for (
247
+ let index = path.lastIndexOf('/');
248
+ index > 0;
249
+ index = path.lastIndexOf('/', index - 1)
250
+ ) {
251
+ const prefix = path.slice(0, index);
252
+ const name = path.slice(index + 1);
253
+ if (Buffer.byteLength(prefix) <= 155 && Buffer.byteLength(name) <= 100) {
254
+ return { name, prefix };
255
+ }
256
+ }
257
+ throw new Error(`Tar path is too long: ${path}`);
258
+ };
259
+
260
+ const tarHeader = (entry: ArchiveEntry): Buffer => {
261
+ const header = Buffer.alloc(512);
262
+ const { name, prefix } = splitTarPath(entry.path);
263
+ writeText(header, 0, 100, name);
264
+ writeOctal(header, 100, 8, entry.mode);
265
+ writeOctal(header, 108, 8, 0);
266
+ writeOctal(header, 116, 8, 0);
267
+ writeOctal(header, 124, 12, entry.bytes.length);
268
+ writeOctal(header, 136, 12, 0);
269
+ header.fill(0x20, 148, 156);
270
+ header[156] = 0x30;
271
+ writeText(header, 257, 6, 'ustar');
272
+ writeText(header, 263, 2, '00');
273
+ writeText(header, 345, 155, prefix);
274
+ const checksum = header.reduce((sum, byte) => sum + byte, 0);
275
+ writeText(header, 148, 8, `${checksum.toString(8).padStart(6, '0')}\0 `);
276
+ return header;
277
+ };
278
+
279
+ export const createDeterministicTarGzip = (
280
+ rootDir: string,
281
+ archiveRoot: string
282
+ ): Uint8Array => {
283
+ const chunks: Buffer[] = [];
284
+ for (const entry of collectArchiveEntries(rootDir, archiveRoot)) {
285
+ chunks.push(tarHeader(entry), entry.bytes);
286
+ const padding = (512 - (entry.bytes.length % 512)) % 512;
287
+ if (padding > 0) {
288
+ chunks.push(Buffer.alloc(padding));
289
+ }
290
+ }
291
+ chunks.push(Buffer.alloc(1024));
292
+ return Bun.gzipSync(Buffer.concat(chunks), { level: 9 });
293
+ };
294
+
295
+ const sha256 = (bytes: Uint8Array): string =>
296
+ new Bun.CryptoHasher('sha256').update(bytes).digest('hex');
297
+
298
+ const readVersion = (repoRoot: string): string => {
299
+ const packageJson = JSON.parse(
300
+ readFileSync(join(repoRoot, 'apps/trails/package.json'), 'utf8')
301
+ ) as { version?: string };
302
+ if (!packageJson.version) {
303
+ throw new Error('apps/trails/package.json does not declare a version');
304
+ }
305
+ return packageJson.version;
306
+ };
307
+
308
+ export const verifyTrailsCliArchive = (
309
+ artifact: Pick<
310
+ TrailsCliReleaseArtifact,
311
+ 'archivePath' | 'platform' | 'version'
312
+ >
313
+ ): void => {
314
+ const host = currentTrailsCliReleasePlatform();
315
+ if (artifact.platform !== host) {
316
+ throw new Error(
317
+ `Cannot cold-start ${artifact.platform} on the ${host} release host`
318
+ );
319
+ }
320
+ const extractRoot = mkdtempSync(join(tmpdir(), 'trails-cli-cold-'));
321
+ try {
322
+ const extract = Bun.spawnSync([
323
+ 'tar',
324
+ '-xzf',
325
+ artifact.archivePath,
326
+ '-C',
327
+ extractRoot,
328
+ ]);
329
+ if (extract.exitCode !== 0) {
330
+ throw new Error(textDecoder.decode(extract.stderr));
331
+ }
332
+ const rootName = artifact.archivePath
333
+ .split('/')
334
+ .at(-1)
335
+ ?.replace(/\.tar\.gz$/u, '');
336
+ if (!rootName) {
337
+ throw new Error(`Invalid archive path: ${artifact.archivePath}`);
338
+ }
339
+ const launcher = join(extractRoot, rootName, 'bin/trails');
340
+ const bundleRoot = join(extractRoot, rootName);
341
+ for (const [argument, expected] of [
342
+ ['--version', artifact.version],
343
+ ['--help', 'Usage: trails'],
344
+ ] as const) {
345
+ const result = Bun.spawnSync([launcher, argument], {
346
+ cwd: extractRoot,
347
+ env: {
348
+ HOME: extractRoot,
349
+ PATH: process.env['PATH'] ?? '',
350
+ },
351
+ });
352
+ const output = textDecoder.decode(result.stdout);
353
+ if (result.exitCode !== 0 || !output.includes(expected)) {
354
+ throw new Error(
355
+ `Cold Trails CLI ${argument} check failed: ${textDecoder.decode(result.stderr) || output}`
356
+ );
357
+ }
358
+ }
359
+ const example = Bun.spawnSync(
360
+ [
361
+ launcher,
362
+ 'run',
363
+ 'example',
364
+ 'survey.brief',
365
+ 'Brief capability report',
366
+ '--root-dir',
367
+ '.',
368
+ '--module',
369
+ 'lib/app.js',
370
+ '--permit',
371
+ '{"id":"release-cold-verifier","scopes":["trails:run"]}',
372
+ ],
373
+ {
374
+ cwd: bundleRoot,
375
+ env: {
376
+ HOME: extractRoot,
377
+ PATH: process.env['PATH'] ?? '',
378
+ },
379
+ }
380
+ );
381
+ const exampleOutput = textDecoder.decode(example.stdout);
382
+ const expectedExampleOutput = 'OK survey.brief :: Brief capability report';
383
+ if (
384
+ example.exitCode !== 0 ||
385
+ !exampleOutput.split(/\r?\n/u).includes(expectedExampleOutput)
386
+ ) {
387
+ throw new Error(
388
+ `Cold Trails CLI authored example check failed: ${textDecoder.decode(example.stderr) || exampleOutput}`
389
+ );
390
+ }
391
+ } finally {
392
+ rmSync(extractRoot, { force: true, recursive: true });
393
+ }
394
+ };
395
+
396
+ export const buildTrailsCliReleaseArtifact = async (
397
+ options: BuildTrailsCliReleaseOptions
398
+ ): Promise<TrailsCliReleaseArtifact> => {
399
+ const repoRoot = resolve(options.repoRoot ?? DEFAULT_REPO_ROOT);
400
+ const platform = options.platform ?? currentTrailsCliReleasePlatform();
401
+ const host = currentTrailsCliReleasePlatform();
402
+ if (platform !== host) {
403
+ throw new Error(
404
+ `Build ${platform} on its matching host so Bun installs the correct optional bindings; current host is ${host}`
405
+ );
406
+ }
407
+ const descriptor = platformDescriptor(platform);
408
+ const packageVersion = readVersion(repoRoot);
409
+ const version = options.version ?? packageVersion;
410
+ if (version !== packageVersion) {
411
+ throw new Error(
412
+ `Requested version ${version} does not match apps/trails/package.json ${packageVersion}`
413
+ );
414
+ }
415
+
416
+ const outputDir = resolve(options.outDir);
417
+ mkdirSync(outputDir, { recursive: true });
418
+ const stagingDir = mkdtempSync(join(tmpdir(), 'trails-cli-release-'));
419
+ const archiveName = trailsCliArchiveName(version, platform);
420
+ const archiveRoot = archiveName.replace(/\.tar\.gz$/u, '');
421
+ const bundleRoot = join(stagingDir, archiveRoot);
422
+ try {
423
+ mkdirSync(join(bundleRoot, 'bin'), { recursive: true });
424
+ mkdirSync(join(bundleRoot, 'lib'), { recursive: true });
425
+ for (const [entrypoint, output, label] of [
426
+ ['apps/trails/bin/trails.ts', 'trails.js', 'CLI'],
427
+ ['apps/trails/src/app.ts', 'app.js', 'app module'],
428
+ ] as const) {
429
+ const build = Bun.spawnSync(
430
+ [
431
+ process.execPath,
432
+ 'build',
433
+ entrypoint,
434
+ '--target=bun',
435
+ ...BUNDLE_EXTERNALS.flatMap((external) => ['--external', external]),
436
+ '--outfile',
437
+ join(bundleRoot, 'lib', output),
438
+ ],
439
+ { cwd: repoRoot }
440
+ );
441
+ if (build.exitCode !== 0) {
442
+ throw new Error(
443
+ `Could not bundle the Trails ${label}: ${textDecoder.decode(build.stderr)}`
444
+ );
445
+ }
446
+ }
447
+
448
+ const parserRoot = resolvePackageRoot(
449
+ 'oxc-parser',
450
+ join(repoRoot, 'packages/source/src')
451
+ );
452
+ const resolverRoot = resolvePackageRoot(
453
+ 'oxc-resolver',
454
+ join(repoRoot, 'packages/warden/src')
455
+ );
456
+ const includedPackages = [
457
+ copyPackage('oxc-parser', parserRoot, bundleRoot),
458
+ copyPackage('oxc-resolver', resolverRoot, bundleRoot),
459
+ copyPackage(
460
+ descriptor.oxcParserBinding,
461
+ resolvePackageRoot(descriptor.oxcParserBinding, parserRoot),
462
+ bundleRoot
463
+ ),
464
+ copyPackage(
465
+ descriptor.oxcResolverBinding,
466
+ resolvePackageRoot(descriptor.oxcResolverBinding, resolverRoot),
467
+ bundleRoot
468
+ ),
469
+ ];
470
+ const dependencies: Record<string, string> = {};
471
+ for (const {
472
+ name,
473
+ version: dependencyVersion,
474
+ } of includedPackages.toSorted((left, right) =>
475
+ left.name.localeCompare(right.name)
476
+ )) {
477
+ dependencies[name] = dependencyVersion;
478
+ }
479
+ writeFileSync(
480
+ join(bundleRoot, 'package.json'),
481
+ `${JSON.stringify(
482
+ {
483
+ dependencies,
484
+ name: '@ontrails/trails-homebrew-bundle',
485
+ private: true,
486
+ type: 'module',
487
+ version,
488
+ },
489
+ null,
490
+ 2
491
+ )}\n`
492
+ );
493
+ const launcherPath = join(bundleRoot, 'bin/trails');
494
+ writeFileSync(launcherPath, launcherSource);
495
+ chmodSync(launcherPath, 0o755);
496
+
497
+ const archiveBytes = createDeterministicTarGzip(bundleRoot, archiveRoot);
498
+ const archivePath = join(outputDir, archiveName);
499
+ const checksum = sha256(archiveBytes);
500
+ const checksumPath = `${archivePath}.sha256`;
501
+ writeFileSync(archivePath, archiveBytes);
502
+ writeFileSync(checksumPath, `${checksum} ${archiveName}\n`);
503
+ const artifact = {
504
+ archiveName,
505
+ archivePath,
506
+ checksum,
507
+ checksumPath,
508
+ platform,
509
+ version,
510
+ } satisfies TrailsCliReleaseArtifact;
511
+ verifyTrailsCliArchive(artifact);
512
+ return artifact;
513
+ } finally {
514
+ rmSync(stagingDir, { force: true, recursive: true });
515
+ }
516
+ };
517
+
518
+ const parseCliArgs = (
519
+ args: readonly string[]
520
+ ): BuildTrailsCliReleaseOptions => {
521
+ let outDir: string | undefined;
522
+ let platform: TrailsCliReleasePlatform | undefined;
523
+ let repoRoot: string | undefined;
524
+ let version: string | undefined;
525
+ for (let index = 0; index < args.length; index += 1) {
526
+ const argument = args[index];
527
+ const value = args[index + 1];
528
+ if (!value) {
529
+ throw new Error(`Missing value for ${argument}`);
530
+ }
531
+ if (argument === '--out-dir') {
532
+ outDir = value;
533
+ } else if (argument === '--platform') {
534
+ if (
535
+ !trailsCliReleasePlatformValues.includes(
536
+ value as TrailsCliReleasePlatform
537
+ )
538
+ ) {
539
+ throw new Error(`Unsupported Trails CLI release platform: ${value}`);
540
+ }
541
+ platform = value as TrailsCliReleasePlatform;
542
+ } else if (argument === '--repo-root') {
543
+ repoRoot = value;
544
+ } else if (argument === '--version') {
545
+ version = value;
546
+ } else {
547
+ throw new Error(`Unknown Trails CLI release argument: ${argument}`);
548
+ }
549
+ index += 1;
550
+ }
551
+ if (!outDir) {
552
+ throw new Error('Missing required argument: --out-dir');
553
+ }
554
+ return {
555
+ outDir,
556
+ ...(platform ? { platform } : {}),
557
+ ...(repoRoot ? { repoRoot } : {}),
558
+ ...(version ? { version } : {}),
559
+ };
560
+ };
561
+
562
+ export const runTrailsCliReleaseCli = async (
563
+ args: readonly string[]
564
+ ): Promise<number> => {
565
+ try {
566
+ const artifact = await buildTrailsCliReleaseArtifact(parseCliArgs(args));
567
+ console.log(
568
+ `Built and cold-verified ${artifact.archiveName} (${artifact.checksum})`
569
+ );
570
+ return 0;
571
+ } catch (error) {
572
+ console.error(error instanceof Error ? error.message : String(error));
573
+ return 1;
574
+ }
575
+ };
@@ -0,0 +1,73 @@
1
+ import { z } from 'zod';
2
+
3
+ export const releaseFactTypeValues = [
4
+ 'package-content',
5
+ 'public-package-route',
6
+ 'public-trail-contract',
7
+ ] as const;
8
+
9
+ export const releaseIntentSourceValues = [
10
+ 'changeset',
11
+ 'no-release-override',
12
+ ] as const;
13
+
14
+ export const releaseRuleSeverityValues = ['error', 'warning'] as const;
15
+
16
+ export const releaseFactTypeSchema = z.enum(releaseFactTypeValues);
17
+ export const releaseIntentSourceSchema = z.enum(releaseIntentSourceValues);
18
+ export const releaseRuleSeveritySchema = z.enum(releaseRuleSeverityValues);
19
+
20
+ export const releaseRuleSchema = z.object({
21
+ description: z.string().optional(),
22
+ enabled: z.boolean().default(true),
23
+ facts: z.array(releaseFactTypeSchema).min(1),
24
+ id: z.string().min(1),
25
+ intent: z.array(releaseIntentSourceSchema).min(1).default(['changeset']),
26
+ severity: releaseRuleSeveritySchema.default('error'),
27
+ });
28
+
29
+ export type ReleaseFactType = z.output<typeof releaseFactTypeSchema>;
30
+ export type ReleaseIntentSource = z.output<typeof releaseIntentSourceSchema>;
31
+ export type ReleaseRule = z.output<typeof releaseRuleSchema>;
32
+ export type ReleaseRuleInput = z.input<typeof releaseRuleSchema>;
33
+
34
+ export const defaultReleaseRules = [
35
+ {
36
+ description:
37
+ 'Public package removals require an exact governed Regrade route or classified multi-owner fold plus branch-local release intent.',
38
+ enabled: true,
39
+ facts: ['public-package-route'],
40
+ id: 'public-package-route-requires-regrade',
41
+ intent: ['changeset'],
42
+ severity: 'error',
43
+ },
44
+ {
45
+ description:
46
+ 'Publishable package content changes require positive release intent.',
47
+ enabled: true,
48
+ facts: ['package-content'],
49
+ id: 'package-content-requires-intent',
50
+ intent: ['changeset', 'no-release-override'],
51
+ severity: 'error',
52
+ },
53
+ {
54
+ description:
55
+ 'Public trail contract changes require positive release intent.',
56
+ enabled: true,
57
+ facts: ['public-trail-contract'],
58
+ id: 'public-trail-contract-requires-intent',
59
+ intent: ['changeset', 'no-release-override'],
60
+ severity: 'error',
61
+ },
62
+ ] as const satisfies readonly ReleaseRuleInput[];
63
+
64
+ export const releaseConfigSchema = z
65
+ .object({
66
+ rules: z.array(releaseRuleSchema).default(() => [...defaultReleaseRules]),
67
+ })
68
+ .default({ rules: [...defaultReleaseRules] });
69
+
70
+ export type ReleaseConfig = z.output<typeof releaseConfigSchema>;
71
+ export type ReleaseConfigInput = z.input<typeof releaseConfigSchema>;
72
+
73
+ export const defaultReleaseConfig = releaseConfigSchema.parse({});