@ontrails/trails 1.0.0-beta.2 → 1.0.0-beta.22

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 (150) hide show
  1. package/CHANGELOG.md +647 -0
  2. package/README.md +26 -0
  3. package/package.json +28 -7
  4. package/src/app.ts +86 -2
  5. package/src/clack.ts +22 -0
  6. package/src/cli.ts +330 -11
  7. package/src/completions.ts +240 -0
  8. package/src/lifecycle-source-io.ts +33 -0
  9. package/src/load-app-mirror.ts +202 -0
  10. package/src/local-state-io.ts +153 -0
  11. package/src/mcp-app.ts +30 -0
  12. package/src/mcp-options.ts +77 -0
  13. package/src/mcp.ts +8 -0
  14. package/src/project-writes.ts +377 -0
  15. package/src/release/bindings.ts +39 -0
  16. package/src/release/check.ts +818 -0
  17. package/src/release/config.ts +63 -0
  18. package/src/release/contract-facts.ts +425 -0
  19. package/src/release/index.ts +85 -0
  20. package/src/release/native-bun-publish.ts +651 -0
  21. package/src/release/native-bun-registry.ts +350 -0
  22. package/src/release/packed-artifacts-smoke.ts +236 -0
  23. package/src/release/smoke.ts +46 -0
  24. package/src/release/wayfinder-dogfood-smoke.ts +226 -0
  25. package/src/retired-topo-command.ts +36 -0
  26. package/src/run-adapter-check.ts +76 -0
  27. package/src/run-collision.ts +126 -0
  28. package/src/run-completions-install.ts +179 -0
  29. package/src/run-example.ts +149 -0
  30. package/src/run-examples.ts +148 -0
  31. package/src/run-quiet.ts +75 -0
  32. package/src/run-release-check.ts +74 -0
  33. package/src/run-trace.ts +273 -0
  34. package/src/run-warden.ts +39 -0
  35. package/src/run-watch.ts +432 -0
  36. package/src/scaffold-version-sync.ts +183 -0
  37. package/src/scaffold-versions.generated.ts +12 -0
  38. package/src/trails/adapter-check.ts +244 -0
  39. package/src/trails/add-surface.ts +94 -40
  40. package/src/trails/add-trail.ts +79 -41
  41. package/src/trails/add-verify.ts +95 -25
  42. package/src/trails/compile.ts +67 -0
  43. package/src/trails/completions-complete.ts +165 -0
  44. package/src/trails/completions.ts +47 -0
  45. package/src/trails/create-adapter.ts +1084 -0
  46. package/src/trails/create-scaffold.ts +399 -104
  47. package/src/trails/create-versions.ts +62 -0
  48. package/src/trails/create.ts +185 -71
  49. package/src/trails/deprecate.ts +59 -0
  50. package/src/trails/dev-clean.ts +82 -0
  51. package/src/trails/dev-reset.ts +50 -0
  52. package/src/trails/dev-stats.ts +72 -0
  53. package/src/trails/dev-support.ts +340 -0
  54. package/src/trails/doctor.ts +56 -0
  55. package/src/trails/draft-promote.ts +949 -0
  56. package/src/trails/guide.ts +74 -68
  57. package/src/trails/load-app.ts +1143 -15
  58. package/src/trails/project.ts +17 -3
  59. package/src/trails/release-check.ts +104 -0
  60. package/src/trails/release-smoke.ts +48 -0
  61. package/src/trails/revise.ts +53 -0
  62. package/src/trails/root-dir.ts +21 -0
  63. package/src/trails/run-example.ts +491 -0
  64. package/src/trails/run-examples.ts +145 -0
  65. package/src/trails/run.ts +410 -0
  66. package/src/trails/scaffold-json.ts +58 -0
  67. package/src/trails/survey.ts +881 -226
  68. package/src/trails/topo-activation.ts +385 -0
  69. package/src/trails/topo-constants.ts +2 -0
  70. package/src/trails/topo-history.ts +47 -0
  71. package/src/trails/topo-output-schemas.ts +248 -0
  72. package/src/trails/topo-pin.ts +52 -0
  73. package/src/trails/topo-read-support.ts +313 -0
  74. package/src/trails/topo-reports.ts +807 -0
  75. package/src/trails/topo-store-support.ts +174 -0
  76. package/src/trails/topo-support.ts +220 -0
  77. package/src/trails/topo-unpin.ts +61 -0
  78. package/src/trails/topo.ts +106 -0
  79. package/src/trails/validate.ts +38 -0
  80. package/src/trails/version-lifecycle-support.ts +945 -0
  81. package/src/trails/warden-guide.ts +129 -0
  82. package/src/trails/warden.ts +165 -58
  83. package/src/versions.ts +31 -0
  84. package/.turbo/turbo-build.log +0 -1
  85. package/.turbo/turbo-lint.log +0 -3
  86. package/.turbo/turbo-typecheck.log +0 -1
  87. package/__tests__/examples.test.ts +0 -6
  88. package/dist/bin/trails.d.ts +0 -3
  89. package/dist/bin/trails.d.ts.map +0 -1
  90. package/dist/bin/trails.js +0 -4
  91. package/dist/bin/trails.js.map +0 -1
  92. package/dist/src/app.d.ts +0 -2
  93. package/dist/src/app.d.ts.map +0 -1
  94. package/dist/src/app.js +0 -11
  95. package/dist/src/app.js.map +0 -1
  96. package/dist/src/clack.d.ts +0 -9
  97. package/dist/src/clack.d.ts.map +0 -1
  98. package/dist/src/clack.js +0 -62
  99. package/dist/src/clack.js.map +0 -1
  100. package/dist/src/cli.d.ts +0 -2
  101. package/dist/src/cli.d.ts.map +0 -1
  102. package/dist/src/cli.js +0 -13
  103. package/dist/src/cli.js.map +0 -1
  104. package/dist/src/trails/add-surface.d.ts +0 -13
  105. package/dist/src/trails/add-surface.d.ts.map +0 -1
  106. package/dist/src/trails/add-surface.js +0 -88
  107. package/dist/src/trails/add-surface.js.map +0 -1
  108. package/dist/src/trails/add-trail.d.ts +0 -11
  109. package/dist/src/trails/add-trail.d.ts.map +0 -1
  110. package/dist/src/trails/add-trail.js +0 -85
  111. package/dist/src/trails/add-trail.js.map +0 -1
  112. package/dist/src/trails/add-verify.d.ts +0 -10
  113. package/dist/src/trails/add-verify.d.ts.map +0 -1
  114. package/dist/src/trails/add-verify.js +0 -67
  115. package/dist/src/trails/add-verify.js.map +0 -1
  116. package/dist/src/trails/create-scaffold.d.ts +0 -15
  117. package/dist/src/trails/create-scaffold.d.ts.map +0 -1
  118. package/dist/src/trails/create-scaffold.js +0 -288
  119. package/dist/src/trails/create-scaffold.js.map +0 -1
  120. package/dist/src/trails/create.d.ts +0 -22
  121. package/dist/src/trails/create.d.ts.map +0 -1
  122. package/dist/src/trails/create.js +0 -121
  123. package/dist/src/trails/create.js.map +0 -1
  124. package/dist/src/trails/guide.d.ts +0 -11
  125. package/dist/src/trails/guide.d.ts.map +0 -1
  126. package/dist/src/trails/guide.js +0 -80
  127. package/dist/src/trails/guide.js.map +0 -1
  128. package/dist/src/trails/load-app.d.ts +0 -4
  129. package/dist/src/trails/load-app.d.ts.map +0 -1
  130. package/dist/src/trails/load-app.js +0 -24
  131. package/dist/src/trails/load-app.js.map +0 -1
  132. package/dist/src/trails/project.d.ts +0 -8
  133. package/dist/src/trails/project.d.ts.map +0 -1
  134. package/dist/src/trails/project.js +0 -43
  135. package/dist/src/trails/project.js.map +0 -1
  136. package/dist/src/trails/survey.d.ts +0 -33
  137. package/dist/src/trails/survey.d.ts.map +0 -1
  138. package/dist/src/trails/survey.js +0 -225
  139. package/dist/src/trails/survey.js.map +0 -1
  140. package/dist/src/trails/warden.d.ts +0 -19
  141. package/dist/src/trails/warden.d.ts.map +0 -1
  142. package/dist/src/trails/warden.js +0 -88
  143. package/dist/src/trails/warden.js.map +0 -1
  144. package/dist/tsconfig.tsbuildinfo +0 -1
  145. package/src/__tests__/create.test.ts +0 -349
  146. package/src/__tests__/guide.test.ts +0 -91
  147. package/src/__tests__/load-app.test.ts +0 -15
  148. package/src/__tests__/survey.test.ts +0 -161
  149. package/src/__tests__/warden.test.ts +0 -74
  150. package/tsconfig.json +0 -9
@@ -0,0 +1,350 @@
1
+ /* oxlint-disable max-statements -- release preflight CLI with explicit reporting */
2
+ import { readdir } from 'node:fs/promises';
3
+ import { join, relative, resolve } from 'node:path';
4
+
5
+ const REPO_ROOT = resolve(process.cwd());
6
+ const SUMMARY_DIST_TAGS = ['latest', 'beta'] as const;
7
+
8
+ export interface RegistryPreflightOptions {
9
+ readonly requirePublished: boolean;
10
+ readonly tag: string | undefined;
11
+ }
12
+
13
+ interface PackageJson {
14
+ readonly name?: string;
15
+ readonly private?: boolean;
16
+ readonly version?: string;
17
+ }
18
+
19
+ export interface RegistryWorkspace {
20
+ readonly name: string;
21
+ readonly path: string;
22
+ readonly version: string;
23
+ }
24
+
25
+ interface NpmView {
26
+ readonly name?: string;
27
+ readonly version?: string;
28
+ readonly 'dist-tags'?: Record<string, string>;
29
+ }
30
+
31
+ export type RegistryResult =
32
+ | {
33
+ readonly distTags: Record<string, string>;
34
+ readonly expectedTagVersion: string | undefined;
35
+ readonly name: string;
36
+ readonly status: 'published';
37
+ readonly version: string;
38
+ readonly workspaceVersion: string;
39
+ }
40
+ | {
41
+ readonly name: string;
42
+ readonly status: 'missing';
43
+ readonly workspaceVersion: string;
44
+ }
45
+ | {
46
+ readonly error: string;
47
+ readonly name: string;
48
+ readonly status: 'inaccessible';
49
+ readonly workspaceVersion: string;
50
+ };
51
+
52
+ const USAGE = `Usage: bun scripts/check-registry-preflight.ts [options]
53
+
54
+ Read-only npm registry preflight for public @ontrails/* workspaces.
55
+
56
+ Options:
57
+ --tag <tag> Expected npm dist-tag. Defaults to .changeset/pre.json
58
+ tag while in prerelease mode, otherwise "latest".
59
+ --require-published Fail when any workspace package is missing from npm.
60
+ Use after publication to verify every package exists.
61
+ -h, --help Show this help and exit.
62
+
63
+ Exit codes: 0 success, 1 registry posture failure, 2 arg-parse error.`;
64
+
65
+ const parseArgs = (argv: readonly string[]): RegistryPreflightOptions => {
66
+ let requirePublished = false;
67
+ let tag: string | undefined;
68
+
69
+ const needsValue = (flag: string, value: string | undefined): string => {
70
+ if (value === undefined || value.startsWith('--')) {
71
+ console.error(`${flag} requires a value`);
72
+ console.error(USAGE);
73
+ process.exit(2);
74
+ }
75
+ return value;
76
+ };
77
+
78
+ let i = 0;
79
+ while (i < argv.length) {
80
+ const arg = argv[i] as string;
81
+ if (arg === '--require-published') {
82
+ requirePublished = true;
83
+ } else if (arg === '--tag') {
84
+ i += 1;
85
+ tag = needsValue('--tag', argv[i]);
86
+ } else if (arg === '-h' || arg === '--help') {
87
+ console.log(USAGE);
88
+ process.exit(0);
89
+ } else {
90
+ console.error(`Unknown argument: ${arg}`);
91
+ console.error(USAGE);
92
+ process.exit(2);
93
+ }
94
+ i += 1;
95
+ }
96
+
97
+ return { requirePublished, tag };
98
+ };
99
+
100
+ const readJson = async <T>(path: string): Promise<T> => {
101
+ const file = Bun.file(path);
102
+ if (!(await file.exists())) {
103
+ throw new Error(`File not found: ${path}`);
104
+ }
105
+ return (await file.json()) as T;
106
+ };
107
+
108
+ const errorCode = (error: unknown): string | undefined => {
109
+ if (typeof error !== 'object' || error === null || !('code' in error)) {
110
+ return undefined;
111
+ }
112
+ const { code } = error as { readonly code?: unknown };
113
+ return typeof code === 'string' ? code : undefined;
114
+ };
115
+
116
+ const resolveDefaultTag = async (): Promise<string> => {
117
+ const prePath = join(REPO_ROOT, '.changeset', 'pre.json');
118
+ if (!(await Bun.file(prePath).exists())) {
119
+ return 'latest';
120
+ }
121
+ const pre = await readJson<{ mode?: string; tag?: string }>(prePath);
122
+ if (pre.mode !== 'pre') {
123
+ return 'latest';
124
+ }
125
+ if (typeof pre.tag === 'string' && pre.tag.length > 0) {
126
+ return pre.tag;
127
+ }
128
+ throw new Error(`${prePath} is in prerelease mode but has no tag`);
129
+ };
130
+
131
+ const discoverWorkspaceDirs = async (
132
+ repoRoot: string,
133
+ patterns: readonly string[]
134
+ ): Promise<string[]> => {
135
+ const dirs: string[] = [];
136
+ for (const pattern of patterns) {
137
+ if (pattern.endsWith('/*')) {
138
+ const parent = join(repoRoot, pattern.slice(0, -2));
139
+ let names: string[] = [];
140
+ try {
141
+ const entries = await readdir(parent, { withFileTypes: true });
142
+ names = entries.filter((e) => e.isDirectory()).map((e) => e.name);
143
+ } catch (error) {
144
+ if (errorCode(error) === 'ENOENT') {
145
+ continue;
146
+ }
147
+ throw new Error(
148
+ `Unable to read workspace directory ${relative(repoRoot, parent)}: ${error instanceof Error ? error.message : String(error)}`,
149
+ { cause: error }
150
+ );
151
+ }
152
+ for (const name of names) {
153
+ const dir = join(parent, name);
154
+ if (await Bun.file(join(dir, 'package.json')).exists()) {
155
+ dirs.push(dir);
156
+ }
157
+ }
158
+ } else {
159
+ const dir = join(repoRoot, pattern);
160
+ if (await Bun.file(join(dir, 'package.json')).exists()) {
161
+ dirs.push(dir);
162
+ }
163
+ }
164
+ }
165
+ return dirs;
166
+ };
167
+
168
+ export const discoverRegistryWorkspaces = async (
169
+ repoRoot = REPO_ROOT
170
+ ): Promise<RegistryWorkspace[]> => {
171
+ const root = await readJson<{ workspaces?: string[] }>(
172
+ join(repoRoot, 'package.json')
173
+ );
174
+ const dirs = await discoverWorkspaceDirs(repoRoot, root.workspaces ?? []);
175
+ const workspaces: RegistryWorkspace[] = [];
176
+
177
+ for (const dir of dirs) {
178
+ const pkg = await readJson<PackageJson>(join(dir, 'package.json'));
179
+ if (
180
+ pkg.private === true ||
181
+ typeof pkg.name !== 'string' ||
182
+ !pkg.name.startsWith('@ontrails/') ||
183
+ typeof pkg.version !== 'string'
184
+ ) {
185
+ continue;
186
+ }
187
+ workspaces.push({
188
+ name: pkg.name,
189
+ path: relative(repoRoot, dir),
190
+ version: pkg.version,
191
+ });
192
+ }
193
+
194
+ return workspaces.toSorted((a, b) => a.name.localeCompare(b.name));
195
+ };
196
+
197
+ export type RegistryView = (name: string) => Promise<NpmView | null>;
198
+
199
+ export const npmRegistryView: RegistryView = async (name) => {
200
+ const proc = Bun.spawn(
201
+ ['npm', 'view', name, 'name', 'version', 'dist-tags', '--json'],
202
+ { stderr: 'pipe', stdin: 'ignore', stdout: 'pipe' }
203
+ );
204
+ const [stdout, stderr, exitCode] = await Promise.all([
205
+ new Response(proc.stdout).text(),
206
+ new Response(proc.stderr).text(),
207
+ proc.exited,
208
+ ]);
209
+
210
+ if (exitCode === 0) {
211
+ return JSON.parse(stdout) as NpmView;
212
+ }
213
+ const combined = `${stdout}\n${stderr}`;
214
+ if (combined.includes('E404') || combined.includes('404 Not Found')) {
215
+ return null;
216
+ }
217
+ throw new Error(stderr.trim() || `npm view failed for ${name}`);
218
+ };
219
+
220
+ const checkWorkspaceRegistryPosture = async (
221
+ workspace: RegistryWorkspace,
222
+ view: RegistryView,
223
+ expectedTag: string
224
+ ): Promise<RegistryResult> => {
225
+ try {
226
+ const registry = await view(workspace.name);
227
+ if (!registry) {
228
+ return {
229
+ name: workspace.name,
230
+ status: 'missing',
231
+ workspaceVersion: workspace.version,
232
+ };
233
+ }
234
+ const distTags = registry['dist-tags'] ?? {};
235
+ return {
236
+ distTags,
237
+ expectedTagVersion: distTags[expectedTag],
238
+ name: workspace.name,
239
+ status: 'published',
240
+ version: registry.version ?? '(unknown)',
241
+ workspaceVersion: workspace.version,
242
+ };
243
+ } catch (error) {
244
+ return {
245
+ error: error instanceof Error ? error.message : String(error),
246
+ name: workspace.name,
247
+ status: 'inaccessible',
248
+ workspaceVersion: workspace.version,
249
+ };
250
+ }
251
+ };
252
+
253
+ export const checkRegistryPosture = async (
254
+ workspaces: readonly RegistryWorkspace[],
255
+ view: RegistryView,
256
+ expectedTag: string
257
+ ): Promise<RegistryResult[]> =>
258
+ Promise.all(
259
+ workspaces.map((workspace) =>
260
+ checkWorkspaceRegistryPosture(workspace, view, expectedTag)
261
+ )
262
+ );
263
+
264
+ export const registryPostureErrors = (
265
+ results: readonly RegistryResult[],
266
+ expectedTag: string,
267
+ requirePublished: boolean
268
+ ): string[] => {
269
+ const errors: string[] = [];
270
+ for (const result of results) {
271
+ if (result.status === 'inaccessible') {
272
+ errors.push(`${result.name}: registry probe failed: ${result.error}`);
273
+ } else if (result.status === 'missing') {
274
+ if (requirePublished) {
275
+ errors.push(`${result.name}: package is missing from the registry`);
276
+ }
277
+ } else if (result.expectedTagVersion !== result.workspaceVersion) {
278
+ errors.push(
279
+ `${result.name}: dist-tag ${expectedTag} points to ${result.expectedTagVersion ?? '(missing)'}, expected ${result.workspaceVersion}`
280
+ );
281
+ }
282
+ }
283
+ return errors;
284
+ };
285
+
286
+ export const formatDistTagSummary = (
287
+ distTags: Readonly<Record<string, string>>
288
+ ): string =>
289
+ SUMMARY_DIST_TAGS.map((tag) => `${tag}=${distTags[tag] ?? 'missing'}`).join(
290
+ ', '
291
+ );
292
+
293
+ const printResults = (
294
+ results: readonly RegistryResult[],
295
+ expectedTag: string
296
+ ): void => {
297
+ console.log(`Registry preflight for dist-tag "${expectedTag}"`);
298
+ for (const result of results) {
299
+ if (result.status === 'published') {
300
+ console.log(
301
+ `✓ ${result.name}@${result.workspaceVersion}: published (registry version ${result.version}, expected ${expectedTag}=${result.expectedTagVersion ?? 'missing'}, tags ${formatDistTagSummary(result.distTags)})`
302
+ );
303
+ } else if (result.status === 'missing') {
304
+ console.log(
305
+ `• ${result.name}@${result.workspaceVersion}: first-time package candidate (not found on registry)`
306
+ );
307
+ } else {
308
+ console.log(`✗ ${result.name}: registry probe failed: ${result.error}`);
309
+ }
310
+ }
311
+ };
312
+
313
+ export const runRegistryPreflight = async (
314
+ options: RegistryPreflightOptions,
315
+ view: RegistryView = npmRegistryView
316
+ ): Promise<number> => {
317
+ const expectedTag = options.tag ?? (await resolveDefaultTag());
318
+ const workspaces = await discoverRegistryWorkspaces();
319
+ const results = await checkRegistryPosture(workspaces, view, expectedTag);
320
+ printResults(results, expectedTag);
321
+ const errors = registryPostureErrors(
322
+ results,
323
+ expectedTag,
324
+ options.requirePublished
325
+ );
326
+ if (errors.length > 0) {
327
+ console.error('\nRegistry preflight failed:');
328
+ for (const error of errors) {
329
+ console.error(`- ${error}`);
330
+ }
331
+ return 1;
332
+ }
333
+ console.log('\nRegistry preflight passed.');
334
+ return 0;
335
+ };
336
+
337
+ export const runRegistryPreflightCli = async (
338
+ args: readonly string[] = process.argv.slice(2)
339
+ ): Promise<number> => {
340
+ try {
341
+ return await runRegistryPreflight(parseArgs(args));
342
+ } catch (error) {
343
+ console.error(error instanceof Error ? error.message : String(error));
344
+ return 1;
345
+ }
346
+ };
347
+
348
+ if (import.meta.main) {
349
+ process.exit(await runRegistryPreflightCli(process.argv.slice(2)));
350
+ }
@@ -0,0 +1,236 @@
1
+ /* oxlint-disable eslint-plugin-jest/require-hook, max-statements -- end-to-end package smoke with temp consumer setup */
2
+ /**
3
+ * Packs public first-party packages into tarballs, installs them into a
4
+ * scratch consumer with first-party overrides, and runs the Warden/Trails CLI
5
+ * from the packed artifacts.
6
+ */
7
+
8
+ import { mkdir, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises';
9
+ import { tmpdir } from 'node:os';
10
+ import { isAbsolute, join, resolve } from 'node:path';
11
+
12
+ const REPO_ROOT = resolve(process.cwd());
13
+
14
+ interface PackedSmokePackageJson {
15
+ readonly name?: string;
16
+ readonly private?: boolean;
17
+ readonly version?: string;
18
+ readonly workspaces?: readonly string[];
19
+ }
20
+
21
+ interface PackedSmokeWorkspace {
22
+ readonly name: string;
23
+ readonly path: string;
24
+ }
25
+
26
+ export interface PackedArtifactsSmokeResult {
27
+ readonly check: 'packed-artifacts';
28
+ readonly message: string;
29
+ readonly packageCount: number;
30
+ readonly passed: true;
31
+ }
32
+
33
+ const commandText = (cmd: readonly string[]): string => cmd.join(' ');
34
+
35
+ const readJson = async <T>(path: string): Promise<T> =>
36
+ (await Bun.file(path).json()) as T;
37
+
38
+ const lastOutputLine = (output: string): string => {
39
+ const line = output
40
+ .split(/\r?\n/)
41
+ .map((item) => item.trim())
42
+ .findLast((item) => item.length > 0);
43
+ if (line === undefined) {
44
+ throw new Error('Expected command output, received none');
45
+ }
46
+ return line;
47
+ };
48
+
49
+ const runCapture = async (
50
+ cmd: readonly string[],
51
+ cwd: string
52
+ ): Promise<string> => {
53
+ const proc = Bun.spawn(cmd as string[], {
54
+ cwd,
55
+ stderr: 'pipe',
56
+ stdin: 'ignore',
57
+ stdout: 'pipe',
58
+ });
59
+ const [stdout, stderr, exitCode] = await Promise.all([
60
+ new Response(proc.stdout).text(),
61
+ new Response(proc.stderr).text(),
62
+ proc.exited,
63
+ ]);
64
+ if (exitCode !== 0) {
65
+ throw new Error(
66
+ [
67
+ `Command failed in ${cwd}: ${commandText(cmd)}`,
68
+ `exit: ${exitCode}`,
69
+ stdout.trim() ? `stdout:\n${stdout}` : undefined,
70
+ stderr.trim() ? `stderr:\n${stderr}` : undefined,
71
+ ]
72
+ .filter((line): line is string => typeof line === 'string')
73
+ .join('\n')
74
+ );
75
+ }
76
+ return stdout || stderr;
77
+ };
78
+
79
+ const workspaceDirs = async (): Promise<readonly string[]> => {
80
+ const rootPackage = await readJson<PackedSmokePackageJson>(
81
+ join(REPO_ROOT, 'package.json')
82
+ );
83
+ const dirs: string[] = [];
84
+ for (const pattern of rootPackage.workspaces ?? []) {
85
+ if (!pattern.endsWith('/*')) {
86
+ throw new Error(`Unsupported workspace pattern: ${pattern}`);
87
+ }
88
+ const base = join(REPO_ROOT, pattern.slice(0, -2));
89
+ for (const entry of await readdir(base, { withFileTypes: true })) {
90
+ const dir = join(base, entry.name);
91
+ if (
92
+ entry.isDirectory() &&
93
+ (await Bun.file(join(dir, 'package.json')).exists())
94
+ ) {
95
+ dirs.push(dir);
96
+ }
97
+ }
98
+ }
99
+ return dirs.toSorted((a, b) => a.localeCompare(b));
100
+ };
101
+
102
+ const publicFirstPartyWorkspaces = async (): Promise<
103
+ readonly PackedSmokeWorkspace[]
104
+ > => {
105
+ const workspaces: PackedSmokeWorkspace[] = [];
106
+ for (const path of await workspaceDirs()) {
107
+ const packageJson = await readJson<PackedSmokePackageJson>(
108
+ join(path, 'package.json')
109
+ );
110
+ if (
111
+ packageJson.private !== true &&
112
+ packageJson.name?.startsWith('@ontrails/') &&
113
+ packageJson.version !== undefined
114
+ ) {
115
+ workspaces.push({
116
+ name: packageJson.name,
117
+ path,
118
+ });
119
+ }
120
+ }
121
+ return workspaces.toSorted((a, b) => a.name.localeCompare(b.name));
122
+ };
123
+
124
+ const packWorkspace = async (
125
+ workspace: PackedSmokeWorkspace,
126
+ packRoot: string
127
+ ): Promise<string> => {
128
+ const output = await runCapture(
129
+ ['bun', 'pm', 'pack', '--destination', packRoot, '--quiet'],
130
+ workspace.path
131
+ );
132
+ const tarball = lastOutputLine(output);
133
+ const destinationPath = isAbsolute(tarball)
134
+ ? tarball
135
+ : join(packRoot, tarball);
136
+ if (await Bun.file(destinationPath).exists()) {
137
+ return destinationPath;
138
+ }
139
+ throw new Error(
140
+ `bun pm pack did not create expected tarball for ${workspace.name}: ${destinationPath} (destination: ${packRoot})`
141
+ );
142
+ };
143
+
144
+ const writeConsumerManifest = async (
145
+ consumerRoot: string,
146
+ tarballsByName: ReadonlyMap<string, string>
147
+ ): Promise<void> => {
148
+ const tarballDependencies = Object.fromEntries(
149
+ [...tarballsByName.entries()].map(([name, tarball]) => [
150
+ name,
151
+ `file:${tarball}`,
152
+ ])
153
+ );
154
+ await writeFile(
155
+ join(consumerRoot, 'package.json'),
156
+ `${JSON.stringify(
157
+ {
158
+ dependencies: tarballDependencies,
159
+ overrides: tarballDependencies,
160
+ private: true,
161
+ type: 'module',
162
+ },
163
+ null,
164
+ 2
165
+ )}\n`
166
+ );
167
+ };
168
+
169
+ const binPath = (consumerRoot: string, name: 'trails' | 'warden'): string =>
170
+ join(consumerRoot, 'node_modules', '.bin', name);
171
+
172
+ export const runPackedArtifactsSmoke =
173
+ async (): Promise<PackedArtifactsSmokeResult> => {
174
+ const tempRoot = await mkdtemp(join(tmpdir(), 'trails-packed-dogfood-'));
175
+ const packRoot = join(tempRoot, 'pack');
176
+ const consumerRoot = join(tempRoot, 'consumer');
177
+ let succeeded = false;
178
+
179
+ try {
180
+ await Promise.all([
181
+ mkdir(packRoot, { recursive: true }),
182
+ mkdir(consumerRoot, { recursive: true }),
183
+ ]);
184
+
185
+ const workspaces = await publicFirstPartyWorkspaces();
186
+ const tarballsByName = new Map<string, string>();
187
+ for (const workspace of workspaces) {
188
+ tarballsByName.set(
189
+ workspace.name,
190
+ await packWorkspace(workspace, packRoot)
191
+ );
192
+ }
193
+
194
+ await writeConsumerManifest(consumerRoot, tarballsByName);
195
+ await runCapture(['bun', 'install', '--silent'], consumerRoot);
196
+
197
+ await runCapture(
198
+ [
199
+ binPath(consumerRoot, 'warden'),
200
+ '--root-dir',
201
+ REPO_ROOT,
202
+ '--lock',
203
+ 'skip',
204
+ '--format',
205
+ 'summary',
206
+ ],
207
+ REPO_ROOT
208
+ );
209
+ await runCapture([binPath(consumerRoot, 'trails'), '--help'], REPO_ROOT);
210
+ await runCapture(
211
+ [binPath(consumerRoot, 'trails'), 'warden', '--lock', 'skip'],
212
+ REPO_ROOT
213
+ );
214
+
215
+ succeeded = true;
216
+ return {
217
+ check: 'packed-artifacts',
218
+ message: `Packed artifact smoke passed for ${workspaces.length} @ontrails/* packages.`,
219
+ packageCount: workspaces.length,
220
+ passed: true,
221
+ };
222
+ } finally {
223
+ if (succeeded) {
224
+ await rm(tempRoot, { force: true, recursive: true });
225
+ } else {
226
+ console.error(
227
+ `Packed dogfood temp root kept for inspection: ${tempRoot}`
228
+ );
229
+ }
230
+ }
231
+ };
232
+
233
+ if (import.meta.main) {
234
+ const result = await runPackedArtifactsSmoke();
235
+ console.log(result.message);
236
+ }
@@ -0,0 +1,46 @@
1
+ import { runPackedArtifactsSmoke } from './packed-artifacts-smoke.js';
2
+ import type { PackedArtifactsSmokeResult } from './packed-artifacts-smoke.js';
3
+ import { runWayfinderDogfoodSmoke } from './wayfinder-dogfood-smoke.js';
4
+ import type { WayfinderDogfoodSmokeResult } from './wayfinder-dogfood-smoke.js';
5
+
6
+ export const releaseSmokeCheckValues = [
7
+ 'all',
8
+ 'packed-artifacts',
9
+ 'wayfinder-dogfood',
10
+ ] as const;
11
+ export type ReleaseSmokeCheck = (typeof releaseSmokeCheckValues)[number];
12
+
13
+ export type ReleaseSmokeCheckResult =
14
+ | PackedArtifactsSmokeResult
15
+ | WayfinderDogfoodSmokeResult;
16
+
17
+ export interface ReleaseSmokeResult {
18
+ readonly checks: readonly ReleaseSmokeCheckResult[];
19
+ readonly message: string;
20
+ readonly passed: true;
21
+ }
22
+
23
+ const checksForInput = (
24
+ check: ReleaseSmokeCheck
25
+ ): readonly Exclude<ReleaseSmokeCheck, 'all'>[] =>
26
+ check === 'all' ? ['packed-artifacts', 'wayfinder-dogfood'] : [check];
27
+
28
+ export const runReleaseSmoke = async (
29
+ check: ReleaseSmokeCheck
30
+ ): Promise<ReleaseSmokeResult> => {
31
+ const results: ReleaseSmokeCheckResult[] = [];
32
+
33
+ for (const selectedCheck of checksForInput(check)) {
34
+ if (selectedCheck === 'packed-artifacts') {
35
+ results.push(await runPackedArtifactsSmoke());
36
+ continue;
37
+ }
38
+ results.push(await runWayfinderDogfoodSmoke());
39
+ }
40
+
41
+ return {
42
+ checks: results,
43
+ message: results.map((result) => result.message).join('\n'),
44
+ passed: true,
45
+ };
46
+ };