@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.
- package/CHANGELOG.md +647 -0
- package/README.md +26 -0
- package/package.json +28 -7
- package/src/app.ts +86 -2
- package/src/clack.ts +22 -0
- package/src/cli.ts +330 -11
- package/src/completions.ts +240 -0
- package/src/lifecycle-source-io.ts +33 -0
- package/src/load-app-mirror.ts +202 -0
- package/src/local-state-io.ts +153 -0
- package/src/mcp-app.ts +30 -0
- package/src/mcp-options.ts +77 -0
- package/src/mcp.ts +8 -0
- package/src/project-writes.ts +377 -0
- package/src/release/bindings.ts +39 -0
- package/src/release/check.ts +818 -0
- package/src/release/config.ts +63 -0
- package/src/release/contract-facts.ts +425 -0
- package/src/release/index.ts +85 -0
- package/src/release/native-bun-publish.ts +651 -0
- package/src/release/native-bun-registry.ts +350 -0
- package/src/release/packed-artifacts-smoke.ts +236 -0
- package/src/release/smoke.ts +46 -0
- package/src/release/wayfinder-dogfood-smoke.ts +226 -0
- package/src/retired-topo-command.ts +36 -0
- package/src/run-adapter-check.ts +76 -0
- package/src/run-collision.ts +126 -0
- package/src/run-completions-install.ts +179 -0
- package/src/run-example.ts +149 -0
- package/src/run-examples.ts +148 -0
- package/src/run-quiet.ts +75 -0
- package/src/run-release-check.ts +74 -0
- package/src/run-trace.ts +273 -0
- package/src/run-warden.ts +39 -0
- package/src/run-watch.ts +432 -0
- package/src/scaffold-version-sync.ts +183 -0
- package/src/scaffold-versions.generated.ts +12 -0
- package/src/trails/adapter-check.ts +244 -0
- package/src/trails/add-surface.ts +94 -40
- package/src/trails/add-trail.ts +79 -41
- package/src/trails/add-verify.ts +95 -25
- package/src/trails/compile.ts +67 -0
- package/src/trails/completions-complete.ts +165 -0
- package/src/trails/completions.ts +47 -0
- package/src/trails/create-adapter.ts +1084 -0
- package/src/trails/create-scaffold.ts +399 -104
- package/src/trails/create-versions.ts +62 -0
- package/src/trails/create.ts +185 -71
- package/src/trails/deprecate.ts +59 -0
- package/src/trails/dev-clean.ts +82 -0
- package/src/trails/dev-reset.ts +50 -0
- package/src/trails/dev-stats.ts +72 -0
- package/src/trails/dev-support.ts +340 -0
- package/src/trails/doctor.ts +56 -0
- package/src/trails/draft-promote.ts +949 -0
- package/src/trails/guide.ts +74 -68
- package/src/trails/load-app.ts +1143 -15
- package/src/trails/project.ts +17 -3
- package/src/trails/release-check.ts +104 -0
- package/src/trails/release-smoke.ts +48 -0
- package/src/trails/revise.ts +53 -0
- package/src/trails/root-dir.ts +21 -0
- package/src/trails/run-example.ts +491 -0
- package/src/trails/run-examples.ts +145 -0
- package/src/trails/run.ts +410 -0
- package/src/trails/scaffold-json.ts +58 -0
- package/src/trails/survey.ts +881 -226
- package/src/trails/topo-activation.ts +385 -0
- package/src/trails/topo-constants.ts +2 -0
- package/src/trails/topo-history.ts +47 -0
- package/src/trails/topo-output-schemas.ts +248 -0
- package/src/trails/topo-pin.ts +52 -0
- package/src/trails/topo-read-support.ts +313 -0
- package/src/trails/topo-reports.ts +807 -0
- package/src/trails/topo-store-support.ts +174 -0
- package/src/trails/topo-support.ts +220 -0
- package/src/trails/topo-unpin.ts +61 -0
- package/src/trails/topo.ts +106 -0
- package/src/trails/validate.ts +38 -0
- package/src/trails/version-lifecycle-support.ts +945 -0
- package/src/trails/warden-guide.ts +129 -0
- package/src/trails/warden.ts +165 -58
- package/src/versions.ts +31 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/__tests__/examples.test.ts +0 -6
- package/dist/bin/trails.d.ts +0 -3
- package/dist/bin/trails.d.ts.map +0 -1
- package/dist/bin/trails.js +0 -4
- package/dist/bin/trails.js.map +0 -1
- package/dist/src/app.d.ts +0 -2
- package/dist/src/app.d.ts.map +0 -1
- package/dist/src/app.js +0 -11
- package/dist/src/app.js.map +0 -1
- package/dist/src/clack.d.ts +0 -9
- package/dist/src/clack.d.ts.map +0 -1
- package/dist/src/clack.js +0 -62
- package/dist/src/clack.js.map +0 -1
- package/dist/src/cli.d.ts +0 -2
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js +0 -13
- package/dist/src/cli.js.map +0 -1
- package/dist/src/trails/add-surface.d.ts +0 -13
- package/dist/src/trails/add-surface.d.ts.map +0 -1
- package/dist/src/trails/add-surface.js +0 -88
- package/dist/src/trails/add-surface.js.map +0 -1
- package/dist/src/trails/add-trail.d.ts +0 -11
- package/dist/src/trails/add-trail.d.ts.map +0 -1
- package/dist/src/trails/add-trail.js +0 -85
- package/dist/src/trails/add-trail.js.map +0 -1
- package/dist/src/trails/add-verify.d.ts +0 -10
- package/dist/src/trails/add-verify.d.ts.map +0 -1
- package/dist/src/trails/add-verify.js +0 -67
- package/dist/src/trails/add-verify.js.map +0 -1
- package/dist/src/trails/create-scaffold.d.ts +0 -15
- package/dist/src/trails/create-scaffold.d.ts.map +0 -1
- package/dist/src/trails/create-scaffold.js +0 -288
- package/dist/src/trails/create-scaffold.js.map +0 -1
- package/dist/src/trails/create.d.ts +0 -22
- package/dist/src/trails/create.d.ts.map +0 -1
- package/dist/src/trails/create.js +0 -121
- package/dist/src/trails/create.js.map +0 -1
- package/dist/src/trails/guide.d.ts +0 -11
- package/dist/src/trails/guide.d.ts.map +0 -1
- package/dist/src/trails/guide.js +0 -80
- package/dist/src/trails/guide.js.map +0 -1
- package/dist/src/trails/load-app.d.ts +0 -4
- package/dist/src/trails/load-app.d.ts.map +0 -1
- package/dist/src/trails/load-app.js +0 -24
- package/dist/src/trails/load-app.js.map +0 -1
- package/dist/src/trails/project.d.ts +0 -8
- package/dist/src/trails/project.d.ts.map +0 -1
- package/dist/src/trails/project.js +0 -43
- package/dist/src/trails/project.js.map +0 -1
- package/dist/src/trails/survey.d.ts +0 -33
- package/dist/src/trails/survey.d.ts.map +0 -1
- package/dist/src/trails/survey.js +0 -225
- package/dist/src/trails/survey.js.map +0 -1
- package/dist/src/trails/warden.d.ts +0 -19
- package/dist/src/trails/warden.d.ts.map +0 -1
- package/dist/src/trails/warden.js +0 -88
- package/dist/src/trails/warden.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/__tests__/create.test.ts +0 -349
- package/src/__tests__/guide.test.ts +0 -91
- package/src/__tests__/load-app.test.ts +0 -15
- package/src/__tests__/survey.test.ts +0 -161
- package/src/__tests__/warden.test.ts +0 -74
- 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
|
+
};
|