@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.
- package/CHANGELOG.md +1906 -0
- package/README.md +48 -0
- package/bin/trails.ts +3 -0
- package/package.json +57 -0
- package/src/app.ts +167 -0
- package/src/clack.ts +111 -0
- package/src/cli.ts +308 -0
- package/src/completions.ts +431 -0
- package/src/lifecycle-source-io.ts +33 -0
- package/src/load-app-mirror.ts +202 -0
- package/src/local-state-io.ts +129 -0
- package/src/mcp-app.ts +42 -0
- package/src/mcp-options.ts +92 -0
- package/src/mcp.ts +8 -0
- package/src/project-writes.ts +377 -0
- package/src/regrade/audit.ts +571 -0
- package/src/regrade/config.ts +152 -0
- package/src/regrade/history.ts +636 -0
- package/src/regrade/lifecycle.ts +76 -0
- package/src/regrade/live-api-preserve.ts +123 -0
- package/src/regrade/plan-artifact.ts +515 -0
- package/src/regrade/plan-derivation.ts +301 -0
- package/src/regrade/prepared-run.ts +259 -0
- package/src/regrade/receipt-history.ts +446 -0
- package/src/regrade/source-transaction.ts +185 -0
- package/src/release/bindings.ts +58 -0
- package/src/release/changeset-packages.ts +99 -0
- package/src/release/check.ts +1191 -0
- package/src/release/cli-bundle.ts +575 -0
- package/src/release/config.ts +73 -0
- package/src/release/contract-facts.ts +425 -0
- package/src/release/homebrew.ts +221 -0
- package/src/release/index.ts +180 -0
- package/src/release/lock-roundtrip-smoke.ts +255 -0
- package/src/release/lock-roundtrip-workspace.ts +107 -0
- package/src/release/native-bun-publish.ts +964 -0
- package/src/release/native-bun-registry.ts +848 -0
- package/src/release/notes-cli.ts +171 -0
- package/src/release/notes.ts +390 -0
- package/src/release/pack-coherence.ts +455 -0
- package/src/release/package-route-facts.ts +146 -0
- package/src/release/packed-artifacts-smoke.ts +236 -0
- package/src/release/policy.ts +1780 -0
- package/src/release/semver.ts +104 -0
- package/src/release/smoke.ts +56 -0
- package/src/release/stable-version-release.ts +80 -0
- package/src/release/wayfinder-dogfood-smoke.ts +762 -0
- package/src/release/zero-line-transition.ts +68 -0
- package/src/retired-topo-command.ts +36 -0
- package/src/run-adapter-check.ts +76 -0
- package/src/run-argv.ts +133 -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-regrade-progress.ts +47 -0
- package/src/run-release-check.ts +74 -0
- package/src/run-schema.ts +74 -0
- package/src/run-trace.ts +273 -0
- package/src/run-warden.ts +39 -0
- package/src/run-watch-project.ts +52 -0
- package/src/run-watch.ts +381 -0
- package/src/run-wayfind-outline.ts +170 -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 +816 -0
- package/src/trails/add-trail.ts +141 -0
- package/src/trails/add-verify.ts +252 -0
- package/src/trails/compile.ts +118 -0
- package/src/trails/completions-complete.ts +236 -0
- package/src/trails/completions.ts +47 -0
- package/src/trails/config-explain.ts +43 -0
- package/src/trails/create-adapter.ts +785 -0
- package/src/trails/create-scaffold.ts +1215 -0
- package/src/trails/create-versions.ts +62 -0
- package/src/trails/create.ts +652 -0
- package/src/trails/deprecate.ts +59 -0
- package/src/trails/dev-clean.ts +80 -0
- package/src/trails/dev-reset.ts +48 -0
- package/src/trails/dev-stats.ts +71 -0
- package/src/trails/dev-support.ts +360 -0
- package/src/trails/doctor.ts +77 -0
- package/src/trails/draft-promote.ts +949 -0
- package/src/trails/guide.ts +106 -0
- package/src/trails/load-app.ts +1145 -0
- package/src/trails/operator-context.ts +66 -0
- package/src/trails/project-context-output.ts +304 -0
- package/src/trails/project-context.ts +613 -0
- package/src/trails/project.ts +65 -0
- package/src/trails/regrade.ts +4951 -0
- package/src/trails/release-check.ts +113 -0
- package/src/trails/release-smoke.ts +49 -0
- package/src/trails/revise.ts +53 -0
- package/src/trails/root-dir.ts +21 -0
- package/src/trails/run-example.ts +592 -0
- package/src/trails/run-examples.ts +149 -0
- package/src/trails/run.ts +496 -0
- package/src/trails/scaffold-json.ts +60 -0
- package/src/trails/scaffold-topo-identity.ts +479 -0
- package/src/trails/survey.ts +990 -0
- package/src/trails/topo-activation.ts +14 -0
- package/src/trails/topo-constants.ts +2 -0
- package/src/trails/topo-history.ts +47 -0
- package/src/trails/topo-output-schemas.ts +259 -0
- package/src/trails/topo-pin.ts +38 -0
- package/src/trails/topo-read-support.ts +368 -0
- package/src/trails/topo-reports.ts +809 -0
- package/src/trails/topo-store-support.ts +323 -0
- package/src/trails/topo-support.ts +247 -0
- package/src/trails/topo-unpin.ts +61 -0
- package/src/trails/topo.ts +92 -0
- package/src/trails/validate.ts +348 -0
- package/src/trails/version-lifecycle-support.ts +936 -0
- package/src/trails/warden-guide.ts +134 -0
- package/src/trails/warden.ts +598 -0
- package/src/trails/wayfind-diff.ts +716 -0
- package/src/trails/wayfind-outline.ts +876 -0
- package/src/trails/wayfind.ts +1319 -0
- 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({});
|