@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,651 @@
|
|
|
1
|
+
/* oxlint-disable eslint-plugin-jest/require-hook, max-statements, func-style -- release script with module-level flow */
|
|
2
|
+
/**
|
|
3
|
+
* Native Bun release binding for public `@ontrails/*` workspace publication.
|
|
4
|
+
*
|
|
5
|
+
* Auto-discovers workspaces from the root `package.json` `workspaces` field,
|
|
6
|
+
* topo-sorts them by `workspace:` dependency edges, enforces manifest-range
|
|
7
|
+
* cleanliness on the packed tarball (no `workspace:` / `catalog:` leakage),
|
|
8
|
+
* and respects the Changesets prerelease tag by default.
|
|
9
|
+
*
|
|
10
|
+
* @see docs/tenets.md for release posture.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { mkdtemp, readdir, rm } from 'node:fs/promises';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { join, relative, resolve } from 'node:path';
|
|
16
|
+
|
|
17
|
+
const REPO_ROOT = resolve(process.cwd());
|
|
18
|
+
|
|
19
|
+
/** ANSI color helpers, disabled when stdout is not a TTY or `NO_COLOR` is set. */
|
|
20
|
+
const useColor = Boolean(process.stdout.isTTY) && !process.env['NO_COLOR'];
|
|
21
|
+
const color = (code: string, text: string): string =>
|
|
22
|
+
useColor ? `\u001B[${code}m${text}\u001B[0m` : text;
|
|
23
|
+
const blue = (t: string) => color('0;34', t);
|
|
24
|
+
const green = (t: string) => color('0;32', t);
|
|
25
|
+
const red = (t: string) => color('0;31', t);
|
|
26
|
+
|
|
27
|
+
const info = (msg: string) => console.log(`${blue('▸')} ${msg}`);
|
|
28
|
+
const success = (msg: string) => console.log(`${green('✓')} ${msg}`);
|
|
29
|
+
const fail = (msg: string) => console.error(`${red('✗')} ${msg}`);
|
|
30
|
+
|
|
31
|
+
/** Parsed CLI options. */
|
|
32
|
+
export interface NativeBunPublishOptions {
|
|
33
|
+
readonly mode: 'check' | 'publish';
|
|
34
|
+
readonly tag: string | undefined;
|
|
35
|
+
readonly otp: string | undefined;
|
|
36
|
+
readonly only: readonly string[] | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Minimal shape of a workspace `package.json` we care about. */
|
|
40
|
+
export interface NativeBunPublishPackageJson {
|
|
41
|
+
name?: string;
|
|
42
|
+
version?: string;
|
|
43
|
+
private?: boolean;
|
|
44
|
+
dependencies?: Record<string, string>;
|
|
45
|
+
devDependencies?: Record<string, string>;
|
|
46
|
+
peerDependencies?: Record<string, string>;
|
|
47
|
+
optionalDependencies?: Record<string, string>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type DependencyField =
|
|
51
|
+
| 'dependencies'
|
|
52
|
+
| 'devDependencies'
|
|
53
|
+
| 'peerDependencies'
|
|
54
|
+
| 'optionalDependencies';
|
|
55
|
+
|
|
56
|
+
/** A discovered, publishable workspace. */
|
|
57
|
+
export interface NativeBunPublishWorkspace {
|
|
58
|
+
readonly name: string;
|
|
59
|
+
readonly version: string;
|
|
60
|
+
readonly path: string;
|
|
61
|
+
readonly isPrivate: boolean;
|
|
62
|
+
readonly workspaceDeps: readonly string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const DEPENDENCY_FIELDS: readonly DependencyField[] = [
|
|
66
|
+
'dependencies',
|
|
67
|
+
'devDependencies',
|
|
68
|
+
'peerDependencies',
|
|
69
|
+
'optionalDependencies',
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const USAGE = `Usage: bun scripts/publish.ts [options]
|
|
73
|
+
|
|
74
|
+
Publish all public @ontrails/* workspaces in dep order using \`bun publish\`.
|
|
75
|
+
|
|
76
|
+
Options:
|
|
77
|
+
--check Pre-publish verification only. Runs \`bun pm pack --dry-run\`
|
|
78
|
+
(required so \`catalog:\` resolves) and asserts the packed
|
|
79
|
+
manifest has no \`workspace:\` or \`catalog:\` ranges. No publishing.
|
|
80
|
+
--dry-run Alias for --check.
|
|
81
|
+
--tag <tag> npm dist-tag. Defaults to .changeset/pre.json tag when in
|
|
82
|
+
prerelease mode, otherwise "latest".
|
|
83
|
+
--otp <code> Two-factor code. Also read from BUN_PUBLISH_OTP.
|
|
84
|
+
--only <name[,name]> Restrict to the named packages (repeatable). Useful for
|
|
85
|
+
partial reruns after a mid-matrix failure.
|
|
86
|
+
-h, --help Show this help and exit.
|
|
87
|
+
|
|
88
|
+
Exit codes: 0 success, 1 publish/check failure, 2 arg-parse error.`;
|
|
89
|
+
|
|
90
|
+
/** Alphabetical sort helper, hoisted for reuse. */
|
|
91
|
+
const sortAlpha = (names: string[]): string[] =>
|
|
92
|
+
names.sort((a, b) => a.localeCompare(b));
|
|
93
|
+
|
|
94
|
+
/** Parse CLI args with a tiny hand-rolled parser. Exits with code 2 on error. */
|
|
95
|
+
const parseArgs = (argv: readonly string[]): NativeBunPublishOptions => {
|
|
96
|
+
let mode: NativeBunPublishOptions['mode'] = 'publish';
|
|
97
|
+
let tag: string | undefined;
|
|
98
|
+
let otp: string | undefined = process.env['BUN_PUBLISH_OTP'] || undefined;
|
|
99
|
+
const only: string[] = [];
|
|
100
|
+
|
|
101
|
+
const needsValue = (flag: string, value: string | undefined): string => {
|
|
102
|
+
if (value === undefined || value.startsWith('--')) {
|
|
103
|
+
fail(`${flag} requires a value`);
|
|
104
|
+
console.error(USAGE);
|
|
105
|
+
process.exit(2);
|
|
106
|
+
}
|
|
107
|
+
return value;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
let i = 0;
|
|
111
|
+
while (i < argv.length) {
|
|
112
|
+
const arg = argv[i] as string;
|
|
113
|
+
if (arg === '--check' || arg === '--dry-run') {
|
|
114
|
+
mode = 'check';
|
|
115
|
+
} else if (arg === '--tag') {
|
|
116
|
+
i += 1;
|
|
117
|
+
tag = needsValue('--tag', argv[i]);
|
|
118
|
+
} else if (arg === '--otp') {
|
|
119
|
+
i += 1;
|
|
120
|
+
otp = needsValue('--otp', argv[i]);
|
|
121
|
+
} else if (arg === '--only') {
|
|
122
|
+
i += 1;
|
|
123
|
+
const value = needsValue('--only', argv[i]);
|
|
124
|
+
for (const name of value
|
|
125
|
+
.split(',')
|
|
126
|
+
.map((s) => s.trim())
|
|
127
|
+
.filter(Boolean)) {
|
|
128
|
+
only.push(name);
|
|
129
|
+
}
|
|
130
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
131
|
+
console.log(USAGE);
|
|
132
|
+
process.exit(0);
|
|
133
|
+
} else {
|
|
134
|
+
fail(`Unknown argument: ${arg}`);
|
|
135
|
+
console.error(USAGE);
|
|
136
|
+
process.exit(2);
|
|
137
|
+
}
|
|
138
|
+
i += 1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
mode,
|
|
143
|
+
only: only.length > 0 ? only : undefined,
|
|
144
|
+
otp,
|
|
145
|
+
tag,
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/** Read and JSON-parse a file. Throws a readable error on failure. */
|
|
150
|
+
const readJson = async <T>(path: string): Promise<T> => {
|
|
151
|
+
const file = Bun.file(path);
|
|
152
|
+
if (!(await file.exists())) {
|
|
153
|
+
throw new Error(`File not found: ${path}`);
|
|
154
|
+
}
|
|
155
|
+
const text = await file.text();
|
|
156
|
+
try {
|
|
157
|
+
return JSON.parse(text) as T;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
throw new Error(`Invalid JSON in ${path}: ${(error as Error).message}`, {
|
|
160
|
+
cause: error,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Resolve the default dist-tag by inspecting `.changeset/pre.json`.
|
|
167
|
+
* Returns the prerelease tag when Changesets is in `pre` mode, else `"latest"`.
|
|
168
|
+
*/
|
|
169
|
+
const resolveDefaultTag = async (): Promise<string> => {
|
|
170
|
+
const prePath = join(REPO_ROOT, '.changeset', 'pre.json');
|
|
171
|
+
if (!(await Bun.file(prePath).exists())) {
|
|
172
|
+
return 'latest';
|
|
173
|
+
}
|
|
174
|
+
const pre = await readJson<{ mode?: string; tag?: string }>(prePath);
|
|
175
|
+
if (pre.mode !== 'pre') {
|
|
176
|
+
return 'latest';
|
|
177
|
+
}
|
|
178
|
+
if (typeof pre.tag === 'string' && pre.tag.length > 0) {
|
|
179
|
+
return pre.tag;
|
|
180
|
+
}
|
|
181
|
+
throw new Error(
|
|
182
|
+
`${prePath} has mode="pre" but no usable "tag" field. Set --tag explicitly or fix pre.json before publishing.`
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Expand `workspaces` globs from the root `package.json` into absolute
|
|
188
|
+
* directory paths that contain a `package.json`.
|
|
189
|
+
*
|
|
190
|
+
* Handles the simple `dir/*` pattern actually used in this repo plus bare
|
|
191
|
+
* `dir` entries. Directory listing is cheaper and more predictable here than
|
|
192
|
+
* a full glob implementation.
|
|
193
|
+
*/
|
|
194
|
+
const discoverWorkspaceDirs = async (
|
|
195
|
+
patterns: readonly string[]
|
|
196
|
+
): Promise<string[]> => {
|
|
197
|
+
const dirs: string[] = [];
|
|
198
|
+
for (const pattern of patterns) {
|
|
199
|
+
if (pattern.endsWith('/*')) {
|
|
200
|
+
const parent = join(REPO_ROOT, pattern.slice(0, -2));
|
|
201
|
+
let names: string[] = [];
|
|
202
|
+
try {
|
|
203
|
+
const entries = await readdir(parent, { withFileTypes: true });
|
|
204
|
+
names = entries
|
|
205
|
+
.filter((e) => e.isDirectory())
|
|
206
|
+
.map((e) => (typeof e.name === 'string' ? e.name : String(e.name)));
|
|
207
|
+
} catch {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
for (const name of names) {
|
|
211
|
+
const dir = join(parent, name);
|
|
212
|
+
if (await Bun.file(join(dir, 'package.json')).exists()) {
|
|
213
|
+
dirs.push(dir);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
const dir = join(REPO_ROOT, pattern);
|
|
218
|
+
if (await Bun.file(join(dir, 'package.json')).exists()) {
|
|
219
|
+
dirs.push(dir);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return dirs;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/** Collect all `workspace:`-referenced dep names from a package.json. */
|
|
227
|
+
const collectWorkspaceDeps = (pkg: NativeBunPublishPackageJson): string[] => {
|
|
228
|
+
const deps = new Set<string>();
|
|
229
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
230
|
+
const map = pkg[field];
|
|
231
|
+
if (!map || typeof map !== 'object') {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
for (const [name, range] of Object.entries(map)) {
|
|
235
|
+
if (typeof range === 'string' && range.startsWith('workspace:')) {
|
|
236
|
+
deps.add(name);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return [...deps];
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const expectedPackedWorkspaceRange = (
|
|
244
|
+
sourceRange: string,
|
|
245
|
+
dep: NativeBunPublishWorkspace
|
|
246
|
+
): string | undefined => {
|
|
247
|
+
if (!sourceRange.startsWith('workspace:')) {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
const protocolRange = sourceRange.slice('workspace:'.length);
|
|
251
|
+
if (protocolRange === '^') {
|
|
252
|
+
return `^${dep.version}`;
|
|
253
|
+
}
|
|
254
|
+
if (protocolRange === '~') {
|
|
255
|
+
return `~${dep.version}`;
|
|
256
|
+
}
|
|
257
|
+
if (protocolRange === '*' || protocolRange === '') {
|
|
258
|
+
return dep.version;
|
|
259
|
+
}
|
|
260
|
+
return protocolRange;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
export const findPackedFirstPartyDependencyMismatches = ({
|
|
264
|
+
packageName,
|
|
265
|
+
packagePath,
|
|
266
|
+
packedPackage,
|
|
267
|
+
sourcePackage,
|
|
268
|
+
workspacesByName,
|
|
269
|
+
}: {
|
|
270
|
+
readonly packageName: string;
|
|
271
|
+
readonly packagePath: string;
|
|
272
|
+
readonly packedPackage: NativeBunPublishPackageJson;
|
|
273
|
+
readonly sourcePackage: NativeBunPublishPackageJson;
|
|
274
|
+
readonly workspacesByName: ReadonlyMap<string, NativeBunPublishWorkspace>;
|
|
275
|
+
}): string[] => {
|
|
276
|
+
const mismatches: string[] = [];
|
|
277
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
278
|
+
const sourceDeps = sourcePackage[field];
|
|
279
|
+
if (!sourceDeps || typeof sourceDeps !== 'object') {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const packedDeps = packedPackage[field] ?? {};
|
|
283
|
+
for (const [depName, sourceRange] of Object.entries(sourceDeps)) {
|
|
284
|
+
const dep = workspacesByName.get(depName);
|
|
285
|
+
if (
|
|
286
|
+
!dep ||
|
|
287
|
+
!dep.name.startsWith('@ontrails/') ||
|
|
288
|
+
typeof sourceRange !== 'string'
|
|
289
|
+
) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const expected = expectedPackedWorkspaceRange(sourceRange, dep);
|
|
293
|
+
if (!expected) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const actual = packedDeps[depName] ?? '(missing)';
|
|
297
|
+
if (actual !== expected) {
|
|
298
|
+
const depPath = relative(REPO_ROOT, dep.path);
|
|
299
|
+
mismatches.push(
|
|
300
|
+
`${packageName} packed ${field} ${depName} resolved to ${actual}, expected ${expected} from ${depPath}/package.json`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (mismatches.length === 0) {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
const relPath = relative(REPO_ROOT, packagePath);
|
|
309
|
+
return [
|
|
310
|
+
`Packed manifest for ${packageName} (${relPath}) contains stale first-party workspace dependency ranges:`,
|
|
311
|
+
...mismatches.map((mismatch) => ` ${mismatch}`),
|
|
312
|
+
];
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/** Discover all workspace packages and enrich with dep edges. */
|
|
316
|
+
const discoverWorkspaces = async (): Promise<NativeBunPublishWorkspace[]> => {
|
|
317
|
+
const root = await readJson<{ workspaces?: string[] }>(
|
|
318
|
+
join(REPO_ROOT, 'package.json')
|
|
319
|
+
);
|
|
320
|
+
if (!root.workspaces || root.workspaces.length === 0) {
|
|
321
|
+
throw new Error('Root package.json has no "workspaces" field');
|
|
322
|
+
}
|
|
323
|
+
const dirs = await discoverWorkspaceDirs(root.workspaces);
|
|
324
|
+
|
|
325
|
+
const workspaces: NativeBunPublishWorkspace[] = [];
|
|
326
|
+
for (const dir of dirs) {
|
|
327
|
+
const pkg = await readJson<NativeBunPublishPackageJson>(
|
|
328
|
+
join(dir, 'package.json')
|
|
329
|
+
);
|
|
330
|
+
if (!pkg.name) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
workspaces.push({
|
|
334
|
+
isPrivate: pkg.private === true,
|
|
335
|
+
name: pkg.name,
|
|
336
|
+
path: dir,
|
|
337
|
+
version: pkg.version ?? '0.0.0',
|
|
338
|
+
workspaceDeps: collectWorkspaceDeps(pkg),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return workspaces;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Topologically sort workspaces so dependencies come before dependents.
|
|
346
|
+
* Ties broken alphabetically by name for deterministic output. Throws on cycles.
|
|
347
|
+
*/
|
|
348
|
+
const topoSort = (
|
|
349
|
+
workspaces: readonly NativeBunPublishWorkspace[]
|
|
350
|
+
): NativeBunPublishWorkspace[] => {
|
|
351
|
+
const byName = new Map(workspaces.map((w) => [w.name, w] as const));
|
|
352
|
+
const indegree = new Map<string, number>();
|
|
353
|
+
const reverseEdges = new Map<string, Set<string>>();
|
|
354
|
+
|
|
355
|
+
for (const w of workspaces) {
|
|
356
|
+
indegree.set(w.name, 0);
|
|
357
|
+
reverseEdges.set(w.name, new Set());
|
|
358
|
+
}
|
|
359
|
+
for (const w of workspaces) {
|
|
360
|
+
for (const dep of w.workspaceDeps) {
|
|
361
|
+
if (!byName.has(dep)) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
indegree.set(w.name, (indegree.get(w.name) ?? 0) + 1);
|
|
365
|
+
reverseEdges.get(dep)?.add(w.name);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const ready: string[] = sortAlpha(
|
|
370
|
+
[...indegree.entries()].filter(([, n]) => n === 0).map(([name]) => name)
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const out: NativeBunPublishWorkspace[] = [];
|
|
374
|
+
while (ready.length > 0) {
|
|
375
|
+
const name = ready.shift() as string;
|
|
376
|
+
const ws = byName.get(name);
|
|
377
|
+
if (ws) {
|
|
378
|
+
out.push(ws);
|
|
379
|
+
}
|
|
380
|
+
const dependents = sortAlpha([...(reverseEdges.get(name) ?? [])]);
|
|
381
|
+
for (const dep of dependents) {
|
|
382
|
+
const next = (indegree.get(dep) ?? 0) - 1;
|
|
383
|
+
indegree.set(dep, next);
|
|
384
|
+
if (next === 0) {
|
|
385
|
+
const idx = ready.findIndex((n) => n.localeCompare(dep) > 0);
|
|
386
|
+
if (idx === -1) {
|
|
387
|
+
ready.push(dep);
|
|
388
|
+
} else {
|
|
389
|
+
ready.splice(idx, 0, dep);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (out.length !== workspaces.length) {
|
|
396
|
+
const remaining = workspaces
|
|
397
|
+
.filter((w) => !out.includes(w))
|
|
398
|
+
.map((w) => w.name);
|
|
399
|
+
throw new Error(
|
|
400
|
+
`Dependency cycle detected among: ${remaining.toSorted().join(', ')}`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
return out;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
/** Spawn a child process inheriting stdio. Returns its exit code. */
|
|
407
|
+
const spawnInherit = async (
|
|
408
|
+
cmd: readonly string[],
|
|
409
|
+
cwd: string
|
|
410
|
+
): Promise<number> => {
|
|
411
|
+
const proc = Bun.spawn(cmd as string[], {
|
|
412
|
+
cwd,
|
|
413
|
+
stderr: 'inherit',
|
|
414
|
+
stdin: 'inherit',
|
|
415
|
+
stdout: 'inherit',
|
|
416
|
+
});
|
|
417
|
+
return await proc.exited;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
/** Spawn a child process and capture stdout (stderr inherited). */
|
|
421
|
+
const spawnCapture = async (
|
|
422
|
+
cmd: readonly string[],
|
|
423
|
+
cwd: string
|
|
424
|
+
): Promise<{ exitCode: number; stdout: string }> => {
|
|
425
|
+
const proc = Bun.spawn(cmd as string[], {
|
|
426
|
+
cwd,
|
|
427
|
+
stderr: 'inherit',
|
|
428
|
+
stdin: 'ignore',
|
|
429
|
+
stdout: 'pipe',
|
|
430
|
+
});
|
|
431
|
+
const stdout = await new Response(proc.stdout).text();
|
|
432
|
+
const exitCode = await proc.exited;
|
|
433
|
+
return { exitCode, stdout };
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Pack a package to a temp dir and assert the resulting tarball's
|
|
438
|
+
* `package/package.json` contains no `workspace:` or `catalog:` ranges.
|
|
439
|
+
*
|
|
440
|
+
* @throws When packing fails or forbidden ranges are found.
|
|
441
|
+
*/
|
|
442
|
+
const assertManifestClean = async (
|
|
443
|
+
ws: NativeBunPublishWorkspace,
|
|
444
|
+
workspacesByName: ReadonlyMap<string, NativeBunPublishWorkspace>
|
|
445
|
+
): Promise<void> => {
|
|
446
|
+
const tmp = await mkdtemp(join(tmpdir(), 'trails-publish-'));
|
|
447
|
+
try {
|
|
448
|
+
// Use `bun pm pack` so the packed manifest reflects what `bun publish`
|
|
449
|
+
// will upload: workspace: and catalog: ranges are resolved the same way.
|
|
450
|
+
// npm pack does not resolve `catalog:` and would produce false positives.
|
|
451
|
+
const pack = await spawnCapture(
|
|
452
|
+
['bun', 'pm', 'pack', '--destination', tmp],
|
|
453
|
+
ws.path
|
|
454
|
+
);
|
|
455
|
+
if (pack.exitCode !== 0) {
|
|
456
|
+
throw new Error(
|
|
457
|
+
`bun pm pack failed for ${ws.name} (exit ${pack.exitCode})`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
const tarEntries = await readdir(tmp);
|
|
461
|
+
const tarName = tarEntries.find((n) =>
|
|
462
|
+
typeof n === 'string' ? n.endsWith('.tgz') : String(n).endsWith('.tgz')
|
|
463
|
+
);
|
|
464
|
+
if (!tarName) {
|
|
465
|
+
throw new Error(`bun pm pack produced no tarball for ${ws.name}`);
|
|
466
|
+
}
|
|
467
|
+
const tarPath = join(tmp, String(tarName));
|
|
468
|
+
|
|
469
|
+
const extract = Bun.spawn(
|
|
470
|
+
['tar', '-xOf', tarPath, 'package/package.json'],
|
|
471
|
+
{ stderr: 'pipe', stdin: 'ignore', stdout: 'pipe' }
|
|
472
|
+
);
|
|
473
|
+
const [manifestText, tarStderr, extractExit] = await Promise.all([
|
|
474
|
+
new Response(extract.stdout).text(),
|
|
475
|
+
new Response(extract.stderr).text(),
|
|
476
|
+
extract.exited,
|
|
477
|
+
]);
|
|
478
|
+
if (extractExit !== 0) {
|
|
479
|
+
const detail = tarStderr.trim() || '(no stderr output)';
|
|
480
|
+
throw new Error(
|
|
481
|
+
`tar extraction failed for ${ws.name} (exit ${extractExit}): ${detail}`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
let packedPackage: NativeBunPublishPackageJson;
|
|
486
|
+
try {
|
|
487
|
+
packedPackage = JSON.parse(manifestText) as NativeBunPublishPackageJson;
|
|
488
|
+
} catch (error) {
|
|
489
|
+
throw new Error(
|
|
490
|
+
`Invalid packed package.json for ${ws.name}: ${(error as Error).message}`,
|
|
491
|
+
{ cause: error }
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const offenders: string[] = [];
|
|
496
|
+
for (const [lineNo, line] of manifestText.split('\n').entries()) {
|
|
497
|
+
if (line.includes('"workspace:') || line.includes('"catalog:')) {
|
|
498
|
+
offenders.push(` line ${lineNo + 1}: ${line.trim()}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (offenders.length > 0) {
|
|
502
|
+
const relPath = relative(REPO_ROOT, ws.path);
|
|
503
|
+
const hint =
|
|
504
|
+
' Hint: `bun publish` rewrites these at pack time. Verify the package was packed via bun, not npm.';
|
|
505
|
+
throw new Error(
|
|
506
|
+
`Packed manifest for ${ws.name} (${relPath}) contains forbidden ranges:\n${offenders.join('\n')}\n${hint}`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
const sourcePackage = await readJson<NativeBunPublishPackageJson>(
|
|
510
|
+
join(ws.path, 'package.json')
|
|
511
|
+
);
|
|
512
|
+
const mismatches = findPackedFirstPartyDependencyMismatches({
|
|
513
|
+
packageName: ws.name,
|
|
514
|
+
packagePath: ws.path,
|
|
515
|
+
packedPackage,
|
|
516
|
+
sourcePackage,
|
|
517
|
+
workspacesByName,
|
|
518
|
+
});
|
|
519
|
+
if (mismatches.length > 0) {
|
|
520
|
+
throw new Error(mismatches.join('\n'));
|
|
521
|
+
}
|
|
522
|
+
} finally {
|
|
523
|
+
await rm(tmp, { force: true, recursive: true });
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
/** Run `--check` flow: pack dry-run plus manifest-range assertion per package. */
|
|
528
|
+
const runCheck = async (
|
|
529
|
+
workspaces: readonly NativeBunPublishWorkspace[],
|
|
530
|
+
allWorkspaces: readonly NativeBunPublishWorkspace[]
|
|
531
|
+
): Promise<number> => {
|
|
532
|
+
const workspacesByName = new Map(allWorkspaces.map((ws) => [ws.name, ws]));
|
|
533
|
+
for (const ws of workspaces) {
|
|
534
|
+
if (ws.isPrivate) {
|
|
535
|
+
info(`Skipping ${ws.name} (private)`);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
info(`Checking ${ws.name}@${ws.version}...`);
|
|
539
|
+
const dryRun = await spawnInherit(
|
|
540
|
+
['bun', 'pm', 'pack', '--dry-run'],
|
|
541
|
+
ws.path
|
|
542
|
+
);
|
|
543
|
+
if (dryRun !== 0) {
|
|
544
|
+
fail(`bun pm pack --dry-run failed for ${ws.name}`);
|
|
545
|
+
return 1;
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
await assertManifestClean(ws, workspacesByName);
|
|
549
|
+
} catch (error) {
|
|
550
|
+
fail((error as Error).message);
|
|
551
|
+
return 1;
|
|
552
|
+
}
|
|
553
|
+
success(`${ws.name}@${ws.version} pack check passed`);
|
|
554
|
+
}
|
|
555
|
+
console.log('');
|
|
556
|
+
success('All package pack checks passed!');
|
|
557
|
+
return 0;
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
/** Run the actual publish flow sequentially. Aborts on first failure. */
|
|
561
|
+
const runPublish = async (
|
|
562
|
+
workspaces: readonly NativeBunPublishWorkspace[],
|
|
563
|
+
tag: string,
|
|
564
|
+
otp: string | undefined
|
|
565
|
+
): Promise<number> => {
|
|
566
|
+
for (const ws of workspaces) {
|
|
567
|
+
if (ws.isPrivate) {
|
|
568
|
+
info(`Skipping ${ws.name} (private)`);
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
info(`Publishing ${ws.name}@${ws.version}... (tag=${tag})`);
|
|
572
|
+
const cmd: string[] = [
|
|
573
|
+
'bun',
|
|
574
|
+
'publish',
|
|
575
|
+
'--access',
|
|
576
|
+
'public',
|
|
577
|
+
'--tag',
|
|
578
|
+
tag,
|
|
579
|
+
];
|
|
580
|
+
if (otp) {
|
|
581
|
+
cmd.push('--otp', otp);
|
|
582
|
+
}
|
|
583
|
+
const code = await spawnInherit(cmd, ws.path);
|
|
584
|
+
if (code !== 0) {
|
|
585
|
+
fail(
|
|
586
|
+
`Failed to publish ${ws.name} (exit ${code}); aborting remaining publishes`
|
|
587
|
+
);
|
|
588
|
+
return 1;
|
|
589
|
+
}
|
|
590
|
+
success(`${ws.name}@${ws.version} published`);
|
|
591
|
+
}
|
|
592
|
+
console.log('');
|
|
593
|
+
success('All packages published!');
|
|
594
|
+
return 0;
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
/** Apply `--only` filter, erroring if any requested name is unknown. */
|
|
598
|
+
const applyOnlyFilter = (
|
|
599
|
+
workspaces: readonly NativeBunPublishWorkspace[],
|
|
600
|
+
only: readonly string[] | undefined
|
|
601
|
+
): readonly NativeBunPublishWorkspace[] => {
|
|
602
|
+
if (!only) {
|
|
603
|
+
return workspaces;
|
|
604
|
+
}
|
|
605
|
+
const set = new Set(only);
|
|
606
|
+
const names = new Set(workspaces.map((w) => w.name));
|
|
607
|
+
const unknown = [...set].filter((n) => !names.has(n));
|
|
608
|
+
if (unknown.length > 0) {
|
|
609
|
+
fail(`--only references unknown packages: ${unknown.join(', ')}`);
|
|
610
|
+
process.exit(2);
|
|
611
|
+
}
|
|
612
|
+
return workspaces.filter((w) => set.has(w.name));
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
export const runNativeBunPublishCli = async (
|
|
616
|
+
args: readonly string[] = process.argv.slice(2)
|
|
617
|
+
): Promise<number> => {
|
|
618
|
+
try {
|
|
619
|
+
const opts = parseArgs(args);
|
|
620
|
+
const all = await discoverWorkspaces();
|
|
621
|
+
const sorted = topoSort(all);
|
|
622
|
+
const selected = applyOnlyFilter(sorted, opts.only);
|
|
623
|
+
|
|
624
|
+
if (selected.length === 0) {
|
|
625
|
+
fail('No workspaces selected');
|
|
626
|
+
return 1;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (opts.mode === 'check') {
|
|
630
|
+
return await runCheck(selected, all);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const tag = opts.tag ?? (await resolveDefaultTag());
|
|
634
|
+
if (!tag) {
|
|
635
|
+
fail('Could not resolve a dist-tag. Pass --tag <tag> explicitly.');
|
|
636
|
+
return 1;
|
|
637
|
+
}
|
|
638
|
+
return await runPublish(selected, tag, opts.otp);
|
|
639
|
+
} catch (error) {
|
|
640
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
641
|
+
fail(msg);
|
|
642
|
+
if (process.env['DEBUG'] === '1' && error instanceof Error && error.stack) {
|
|
643
|
+
console.error(error.stack);
|
|
644
|
+
}
|
|
645
|
+
return 1;
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
if (import.meta.main) {
|
|
650
|
+
process.exit(await runNativeBunPublishCli(process.argv.slice(2)));
|
|
651
|
+
}
|