@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,425 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
|
|
7
|
+
import type { WorkspaceInfo } from './check.js';
|
|
8
|
+
|
|
9
|
+
export type ContractReleaseFactAspect =
|
|
10
|
+
| 'input'
|
|
11
|
+
| 'output'
|
|
12
|
+
| 'surfaces'
|
|
13
|
+
| 'trail'
|
|
14
|
+
| 'visibility';
|
|
15
|
+
|
|
16
|
+
export interface ContractReleaseFact {
|
|
17
|
+
readonly aspect: ContractReleaseFactAspect;
|
|
18
|
+
readonly baseHash: string | null;
|
|
19
|
+
readonly changedFiles: readonly string[];
|
|
20
|
+
readonly currentHash: string | null;
|
|
21
|
+
readonly packageName?: string;
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly trailId: string;
|
|
24
|
+
readonly workspacePath?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ContractReleaseFactInput {
|
|
28
|
+
readonly baseRef?: string;
|
|
29
|
+
readonly changedFiles: readonly string[];
|
|
30
|
+
readonly repoRoot: string;
|
|
31
|
+
readonly workspaces: readonly WorkspaceInfo[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ContractSourceSnapshot {
|
|
35
|
+
readonly baseSource: string | null;
|
|
36
|
+
readonly changedFiles?: readonly string[];
|
|
37
|
+
readonly currentSource: string | null;
|
|
38
|
+
readonly packageName?: string;
|
|
39
|
+
readonly path: string;
|
|
40
|
+
readonly workspacePath?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface TrailDefinition {
|
|
44
|
+
readonly aspects: Readonly<Record<ContractReleaseFactAspect, string | null>>;
|
|
45
|
+
readonly trailId: string;
|
|
46
|
+
readonly visibility: 'internal' | 'public';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const NON_SHIPPING_SOURCE_PATTERNS = [
|
|
50
|
+
/(?:^|\/)__tests__(?:\/|$)/u,
|
|
51
|
+
/(?:^|\/)__snapshots__(?:\/|$)/u,
|
|
52
|
+
/(?:^|\/)dist(?:\/|$)/u,
|
|
53
|
+
/(?:^|\/)\.turbo(?:\/|$)/u,
|
|
54
|
+
/(?:^|\/)node_modules(?:\/|$)/u,
|
|
55
|
+
/\.(?:test|spec|snap)\.[cm]?[jt]sx?$/u,
|
|
56
|
+
/\.test-d\.ts$/u,
|
|
57
|
+
/\.tsbuildinfo$/u,
|
|
58
|
+
] as const;
|
|
59
|
+
|
|
60
|
+
const SOURCE_PATH_PATTERN = /\.[cm]?[jt]sx?$/u;
|
|
61
|
+
|
|
62
|
+
const normalizePath = (path: string): string =>
|
|
63
|
+
path.replaceAll('\\', '/').replace(/^\.\//u, '');
|
|
64
|
+
|
|
65
|
+
const isPublishableOnTrailsWorkspace = (workspace: WorkspaceInfo): boolean =>
|
|
66
|
+
!workspace.isPrivate && workspace.name.startsWith('@ontrails/');
|
|
67
|
+
|
|
68
|
+
const isUnderWorkspace = (filePath: string, workspacePath: string): boolean =>
|
|
69
|
+
filePath === workspacePath || filePath.startsWith(`${workspacePath}/`);
|
|
70
|
+
|
|
71
|
+
const getWorkspaceRelativePath = (
|
|
72
|
+
filePath: string,
|
|
73
|
+
workspacePath: string
|
|
74
|
+
): string => filePath.slice(workspacePath.length + 1);
|
|
75
|
+
|
|
76
|
+
const isNonShippingSourcePath = (workspaceRelativePath: string): boolean =>
|
|
77
|
+
NON_SHIPPING_SOURCE_PATTERNS.some((pattern) =>
|
|
78
|
+
pattern.test(workspaceRelativePath)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const hashSource = (source: string | null): string | null =>
|
|
82
|
+
source === null
|
|
83
|
+
? null
|
|
84
|
+
: createHash('sha256').update(source).digest('hex').slice(0, 16);
|
|
85
|
+
|
|
86
|
+
const readCurrentSource = (
|
|
87
|
+
repoRoot: string,
|
|
88
|
+
relativePath: string
|
|
89
|
+
): string | null => {
|
|
90
|
+
const absolutePath = join(repoRoot, relativePath);
|
|
91
|
+
return existsSync(absolutePath) ? readFileSync(absolutePath, 'utf8') : null;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const readBaseSource = (
|
|
95
|
+
repoRoot: string,
|
|
96
|
+
baseRef: string | undefined,
|
|
97
|
+
relativePath: string
|
|
98
|
+
): string | null => {
|
|
99
|
+
if (!baseRef) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result = Bun.spawnSync({
|
|
104
|
+
cmd: ['git', 'show', `${baseRef}:${relativePath}`],
|
|
105
|
+
cwd: repoRoot,
|
|
106
|
+
stderr: 'pipe',
|
|
107
|
+
stdout: 'pipe',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return result.exitCode === 0 ? result.stdout.toString() : null;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const workspaceForFile = (
|
|
114
|
+
filePath: string,
|
|
115
|
+
workspaces: readonly WorkspaceInfo[]
|
|
116
|
+
): WorkspaceInfo | undefined =>
|
|
117
|
+
workspaces
|
|
118
|
+
.filter(isPublishableOnTrailsWorkspace)
|
|
119
|
+
.find((workspace) => isUnderWorkspace(filePath, workspace.relativePath));
|
|
120
|
+
|
|
121
|
+
export const createContractSourceSnapshots = (
|
|
122
|
+
input: ContractReleaseFactInput
|
|
123
|
+
): readonly ContractSourceSnapshot[] => {
|
|
124
|
+
const snapshots: ContractSourceSnapshot[] = [];
|
|
125
|
+
|
|
126
|
+
for (const changedFile of input.changedFiles.map(normalizePath)) {
|
|
127
|
+
if (!SOURCE_PATH_PATTERN.test(changedFile)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const workspace = workspaceForFile(changedFile, input.workspaces);
|
|
132
|
+
if (!workspace) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const workspaceRelativePath = getWorkspaceRelativePath(
|
|
137
|
+
changedFile,
|
|
138
|
+
workspace.relativePath
|
|
139
|
+
);
|
|
140
|
+
if (isNonShippingSourcePath(workspaceRelativePath)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const baseSource = readBaseSource(
|
|
145
|
+
input.repoRoot,
|
|
146
|
+
input.baseRef,
|
|
147
|
+
changedFile
|
|
148
|
+
);
|
|
149
|
+
const currentSource = readCurrentSource(input.repoRoot, changedFile);
|
|
150
|
+
if (baseSource === null && currentSource === null) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
snapshots.push({
|
|
155
|
+
baseSource,
|
|
156
|
+
changedFiles: [changedFile],
|
|
157
|
+
currentSource,
|
|
158
|
+
packageName: workspace.name,
|
|
159
|
+
path: changedFile,
|
|
160
|
+
workspacePath: workspace.relativePath,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return snapshots;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const isPropertyNamed = (
|
|
168
|
+
property: ts.ObjectLiteralElementLike,
|
|
169
|
+
name: string
|
|
170
|
+
): property is ts.PropertyAssignment => {
|
|
171
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const propertyName = property.name;
|
|
176
|
+
return (
|
|
177
|
+
(ts.isIdentifier(propertyName) && propertyName.text === name) ||
|
|
178
|
+
(ts.isStringLiteral(propertyName) && propertyName.text === name)
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const findProperty = (
|
|
183
|
+
object: ts.ObjectLiteralExpression,
|
|
184
|
+
name: string
|
|
185
|
+
): ts.PropertyAssignment | undefined =>
|
|
186
|
+
object.properties.find((property) => isPropertyNamed(property, name));
|
|
187
|
+
|
|
188
|
+
const isTrailCall = (node: ts.CallExpression): boolean =>
|
|
189
|
+
ts.isIdentifier(node.expression) && node.expression.text === 'trail';
|
|
190
|
+
|
|
191
|
+
const literalText = (node: ts.Expression | undefined): string | undefined =>
|
|
192
|
+
node && ts.isStringLiteralLike(node) ? node.text : undefined;
|
|
193
|
+
|
|
194
|
+
const normalizeText = (value: string): string =>
|
|
195
|
+
value.replaceAll(/\s+/gu, ' ').trim();
|
|
196
|
+
|
|
197
|
+
const normalizeExpressionText = (
|
|
198
|
+
expression: ts.Expression | undefined,
|
|
199
|
+
sourceFile: ts.SourceFile,
|
|
200
|
+
constInitializers: ReadonlyMap<string, ts.Expression>
|
|
201
|
+
): string | null => {
|
|
202
|
+
if (!expression) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (ts.isIdentifier(expression)) {
|
|
207
|
+
const initializer = constInitializers.get(expression.text);
|
|
208
|
+
if (initializer) {
|
|
209
|
+
return `${expression.text}=${normalizeText(
|
|
210
|
+
initializer.getText(sourceFile)
|
|
211
|
+
)}`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return normalizeText(expression.getText(sourceFile));
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const visibilityFor = (
|
|
219
|
+
object: ts.ObjectLiteralExpression
|
|
220
|
+
): TrailDefinition['visibility'] => {
|
|
221
|
+
const visibility = findProperty(object, 'visibility');
|
|
222
|
+
const value = literalText(visibility?.initializer);
|
|
223
|
+
if (value === 'internal') {
|
|
224
|
+
return 'internal';
|
|
225
|
+
}
|
|
226
|
+
return 'public';
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const collectConstInitializers = (
|
|
230
|
+
sourceFile: ts.SourceFile
|
|
231
|
+
): ReadonlyMap<string, ts.Expression> => {
|
|
232
|
+
const initializers = new Map<string, ts.Expression>();
|
|
233
|
+
|
|
234
|
+
const visit = (node: ts.Node): void => {
|
|
235
|
+
if (
|
|
236
|
+
ts.isVariableDeclaration(node) &&
|
|
237
|
+
ts.isIdentifier(node.name) &&
|
|
238
|
+
node.initializer
|
|
239
|
+
) {
|
|
240
|
+
initializers.set(node.name.text, node.initializer);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
ts.forEachChild(node, visit);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
visit(sourceFile);
|
|
247
|
+
return initializers;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const collectTrailDefinitions = (
|
|
251
|
+
source: string
|
|
252
|
+
): readonly TrailDefinition[] => {
|
|
253
|
+
const sourceFile = ts.createSourceFile(
|
|
254
|
+
'release-contract-facts.ts',
|
|
255
|
+
source,
|
|
256
|
+
ts.ScriptTarget.Latest,
|
|
257
|
+
true,
|
|
258
|
+
ts.ScriptKind.TS
|
|
259
|
+
);
|
|
260
|
+
const constInitializers = collectConstInitializers(sourceFile);
|
|
261
|
+
const definitions: TrailDefinition[] = [];
|
|
262
|
+
|
|
263
|
+
const visit = (node: ts.Node): void => {
|
|
264
|
+
if (ts.isCallExpression(node) && isTrailCall(node)) {
|
|
265
|
+
const [idArgument, spec] = node.arguments;
|
|
266
|
+
const trailId = literalText(idArgument);
|
|
267
|
+
|
|
268
|
+
if (trailId && spec && ts.isObjectLiteralExpression(spec)) {
|
|
269
|
+
const visibility = visibilityFor(spec);
|
|
270
|
+
definitions.push({
|
|
271
|
+
aspects: {
|
|
272
|
+
input: normalizeExpressionText(
|
|
273
|
+
findProperty(spec, 'input')?.initializer,
|
|
274
|
+
sourceFile,
|
|
275
|
+
constInitializers
|
|
276
|
+
),
|
|
277
|
+
output: normalizeExpressionText(
|
|
278
|
+
findProperty(spec, 'output')?.initializer,
|
|
279
|
+
sourceFile,
|
|
280
|
+
constInitializers
|
|
281
|
+
),
|
|
282
|
+
surfaces: normalizeExpressionText(
|
|
283
|
+
findProperty(spec, 'surfaces')?.initializer,
|
|
284
|
+
sourceFile,
|
|
285
|
+
constInitializers
|
|
286
|
+
),
|
|
287
|
+
trail: normalizeText(trailId),
|
|
288
|
+
visibility,
|
|
289
|
+
},
|
|
290
|
+
trailId,
|
|
291
|
+
visibility,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
ts.forEachChild(node, visit);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
visit(sourceFile);
|
|
300
|
+
return definitions;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const definitionsById = (
|
|
304
|
+
definitions: readonly TrailDefinition[]
|
|
305
|
+
): ReadonlyMap<string, TrailDefinition> =>
|
|
306
|
+
new Map(definitions.map((definition) => [definition.trailId, definition]));
|
|
307
|
+
|
|
308
|
+
const isPublicContract = (definition: TrailDefinition | undefined): boolean =>
|
|
309
|
+
definition !== undefined && definition.visibility !== 'internal';
|
|
310
|
+
|
|
311
|
+
const fact = (
|
|
312
|
+
snapshot: ContractSourceSnapshot,
|
|
313
|
+
trailId: string,
|
|
314
|
+
aspect: ContractReleaseFactAspect,
|
|
315
|
+
baseValue: string | null,
|
|
316
|
+
currentValue: string | null
|
|
317
|
+
): ContractReleaseFact => ({
|
|
318
|
+
aspect,
|
|
319
|
+
baseHash: hashSource(baseValue),
|
|
320
|
+
changedFiles: snapshot.changedFiles ?? [snapshot.path],
|
|
321
|
+
currentHash: hashSource(currentValue),
|
|
322
|
+
...(snapshot.packageName ? { packageName: snapshot.packageName } : {}),
|
|
323
|
+
path: snapshot.path,
|
|
324
|
+
trailId,
|
|
325
|
+
...(snapshot.workspacePath ? { workspacePath: snapshot.workspacePath } : {}),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const compareDefinitions = (
|
|
329
|
+
snapshot: ContractSourceSnapshot,
|
|
330
|
+
baseDefinition: TrailDefinition | undefined,
|
|
331
|
+
currentDefinition: TrailDefinition | undefined
|
|
332
|
+
): readonly ContractReleaseFact[] => {
|
|
333
|
+
if (
|
|
334
|
+
!isPublicContract(baseDefinition) &&
|
|
335
|
+
!isPublicContract(currentDefinition)
|
|
336
|
+
) {
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const trailId =
|
|
341
|
+
currentDefinition?.trailId ?? baseDefinition?.trailId ?? '(unknown)';
|
|
342
|
+
|
|
343
|
+
if (!baseDefinition || !currentDefinition) {
|
|
344
|
+
return [
|
|
345
|
+
fact(
|
|
346
|
+
snapshot,
|
|
347
|
+
trailId,
|
|
348
|
+
'trail',
|
|
349
|
+
baseDefinition?.aspects.trail ?? null,
|
|
350
|
+
currentDefinition?.aspects.trail ?? null
|
|
351
|
+
),
|
|
352
|
+
];
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const facts: ContractReleaseFact[] = [];
|
|
356
|
+
|
|
357
|
+
for (const aspect of ['input', 'output', 'surfaces'] as const) {
|
|
358
|
+
const baseValue = baseDefinition.aspects[aspect];
|
|
359
|
+
const currentValue = currentDefinition.aspects[aspect];
|
|
360
|
+
if (baseValue !== currentValue) {
|
|
361
|
+
facts.push(fact(snapshot, trailId, aspect, baseValue, currentValue));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (baseDefinition.visibility !== currentDefinition.visibility) {
|
|
366
|
+
facts.push(
|
|
367
|
+
fact(
|
|
368
|
+
snapshot,
|
|
369
|
+
trailId,
|
|
370
|
+
'visibility',
|
|
371
|
+
baseDefinition.visibility,
|
|
372
|
+
currentDefinition.visibility
|
|
373
|
+
)
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return facts;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
export const findPublicTrailContractChangeFactsFromSnapshots = (
|
|
381
|
+
snapshots: readonly ContractSourceSnapshot[]
|
|
382
|
+
): readonly ContractReleaseFact[] => {
|
|
383
|
+
const facts: ContractReleaseFact[] = [];
|
|
384
|
+
|
|
385
|
+
for (const snapshot of snapshots) {
|
|
386
|
+
const baseDefinitions = definitionsById(
|
|
387
|
+
snapshot.baseSource === null
|
|
388
|
+
? []
|
|
389
|
+
: collectTrailDefinitions(snapshot.baseSource)
|
|
390
|
+
);
|
|
391
|
+
const currentDefinitions = definitionsById(
|
|
392
|
+
snapshot.currentSource === null
|
|
393
|
+
? []
|
|
394
|
+
: collectTrailDefinitions(snapshot.currentSource)
|
|
395
|
+
);
|
|
396
|
+
const trailIds = new Set([
|
|
397
|
+
...baseDefinitions.keys(),
|
|
398
|
+
...currentDefinitions.keys(),
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
for (const trailId of [...trailIds].toSorted()) {
|
|
402
|
+
facts.push(
|
|
403
|
+
...compareDefinitions(
|
|
404
|
+
snapshot,
|
|
405
|
+
baseDefinitions.get(trailId),
|
|
406
|
+
currentDefinitions.get(trailId)
|
|
407
|
+
)
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return facts.toSorted(
|
|
413
|
+
(left, right) =>
|
|
414
|
+
left.path.localeCompare(right.path) ||
|
|
415
|
+
left.trailId.localeCompare(right.trailId) ||
|
|
416
|
+
left.aspect.localeCompare(right.aspect)
|
|
417
|
+
);
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
export const findPublicTrailContractChangeFacts = (
|
|
421
|
+
input: ContractReleaseFactInput
|
|
422
|
+
): readonly ContractReleaseFact[] =>
|
|
423
|
+
findPublicTrailContractChangeFactsFromSnapshots(
|
|
424
|
+
createContractSourceSnapshots(input)
|
|
425
|
+
);
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
expectedTrailsCliReleaseAssetNames,
|
|
6
|
+
trailsCliArchiveName,
|
|
7
|
+
trailsCliChecksumName,
|
|
8
|
+
trailsCliReleasePlatformValues,
|
|
9
|
+
} from './cli-bundle.js';
|
|
10
|
+
import type { TrailsCliReleasePlatform } from './cli-bundle.js';
|
|
11
|
+
|
|
12
|
+
export interface GitHubReleaseAsset {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface GitHubReleaseMetadata {
|
|
17
|
+
readonly assets: readonly GitHubReleaseAsset[];
|
|
18
|
+
readonly draft: boolean;
|
|
19
|
+
readonly published_at: string | null;
|
|
20
|
+
readonly tag_name: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type TrailsCliReleaseChecksums = Readonly<
|
|
24
|
+
Record<TrailsCliReleasePlatform, string>
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
const versionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
|
|
28
|
+
const checksumPattern = /^[0-9a-f]{64}$/u;
|
|
29
|
+
|
|
30
|
+
const requireVersion = (version: string): void => {
|
|
31
|
+
if (!versionPattern.test(version)) {
|
|
32
|
+
throw new Error(`Invalid Trails release version: ${version}`);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const validateTrailsHomebrewRelease = (
|
|
37
|
+
release: GitHubReleaseMetadata,
|
|
38
|
+
version: string
|
|
39
|
+
): void => {
|
|
40
|
+
requireVersion(version);
|
|
41
|
+
if (release.draft || !release.published_at) {
|
|
42
|
+
throw new Error(`GitHub release v${version} is not published`);
|
|
43
|
+
}
|
|
44
|
+
if (release.tag_name !== `v${version}`) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`GitHub release tag ${release.tag_name} does not match v${version}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
for (const expectedName of expectedTrailsCliReleaseAssetNames(version)) {
|
|
50
|
+
const count = release.assets.filter(
|
|
51
|
+
({ name }) => name === expectedName
|
|
52
|
+
).length;
|
|
53
|
+
if (count !== 1) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Expected exactly one ${expectedName} release asset, found ${count}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const parseTrailsCliChecksums = (
|
|
62
|
+
contents: string,
|
|
63
|
+
version: string
|
|
64
|
+
): TrailsCliReleaseChecksums => {
|
|
65
|
+
requireVersion(version);
|
|
66
|
+
const found = new Map<TrailsCliReleasePlatform, string>();
|
|
67
|
+
for (const line of contents.split('\n').filter(Boolean)) {
|
|
68
|
+
const match = /^(?<checksum>[0-9a-f]{64}) {2}(?<name>.+)$/u.exec(line);
|
|
69
|
+
if (!match?.groups) {
|
|
70
|
+
throw new Error(`Invalid Trails CLI checksum line: ${line}`);
|
|
71
|
+
}
|
|
72
|
+
const platform = trailsCliReleasePlatformValues.find(
|
|
73
|
+
(candidate) =>
|
|
74
|
+
trailsCliArchiveName(version, candidate) === match.groups?.['name']
|
|
75
|
+
);
|
|
76
|
+
if (!platform) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Unexpected Trails CLI checksum asset: ${match.groups['name']}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (found.has(platform)) {
|
|
82
|
+
throw new Error(`Duplicate Trails CLI checksum for ${platform}`);
|
|
83
|
+
}
|
|
84
|
+
const parsedChecksum = match.groups['checksum'];
|
|
85
|
+
if (!parsedChecksum) {
|
|
86
|
+
throw new Error(`Missing Trails CLI checksum in line: ${line}`);
|
|
87
|
+
}
|
|
88
|
+
found.set(platform, parsedChecksum);
|
|
89
|
+
}
|
|
90
|
+
for (const platform of trailsCliReleasePlatformValues) {
|
|
91
|
+
if (!found.has(platform)) {
|
|
92
|
+
throw new Error(`Missing Trails CLI checksum for ${platform}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return Object.fromEntries(found) as TrailsCliReleaseChecksums;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const verifyTrailsHomebrewAssetDirectory = async (
|
|
99
|
+
assetsDir: string,
|
|
100
|
+
release: GitHubReleaseMetadata,
|
|
101
|
+
version: string
|
|
102
|
+
): Promise<TrailsCliReleaseChecksums> => {
|
|
103
|
+
validateTrailsHomebrewRelease(release, version);
|
|
104
|
+
const checksumPath = join(assetsDir, trailsCliChecksumName(version));
|
|
105
|
+
const checksums = parseTrailsCliChecksums(
|
|
106
|
+
readFileSync(checksumPath, 'utf8'),
|
|
107
|
+
version
|
|
108
|
+
);
|
|
109
|
+
for (const platform of trailsCliReleasePlatformValues) {
|
|
110
|
+
const archiveName = trailsCliArchiveName(version, platform);
|
|
111
|
+
const bytes = await Bun.file(join(assetsDir, archiveName)).bytes();
|
|
112
|
+
const actual = new Bun.CryptoHasher('sha256').update(bytes).digest('hex');
|
|
113
|
+
if (actual !== checksums[platform]) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Checksum mismatch for ${archiveName}: expected ${checksums[platform]}, found ${actual}`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return checksums;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const renderTrailsHomebrewFormula = (
|
|
123
|
+
version: string,
|
|
124
|
+
checksums: TrailsCliReleaseChecksums
|
|
125
|
+
): string => {
|
|
126
|
+
requireVersion(version);
|
|
127
|
+
for (const [platform, checksum] of Object.entries(checksums)) {
|
|
128
|
+
if (!checksumPattern.test(checksum)) {
|
|
129
|
+
throw new Error(`Invalid ${platform} SHA256: ${checksum}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return `class Trails < Formula
|
|
133
|
+
desc "Agent-native, contract-first TypeScript framework"
|
|
134
|
+
homepage "https://github.com/outfitter-dev/trails"
|
|
135
|
+
license "MIT"
|
|
136
|
+
# Keep this scheme after the reset from 1.0.0-beta to 0.2.0.
|
|
137
|
+
version_scheme 1
|
|
138
|
+
|
|
139
|
+
depends_on "bun"
|
|
140
|
+
|
|
141
|
+
on_macos do
|
|
142
|
+
on_arm do
|
|
143
|
+
url "https://github.com/outfitter-dev/trails/releases/download/v${version}/trails-v${version}-darwin-arm64.tar.gz"
|
|
144
|
+
sha256 "${checksums['darwin-arm64']}"
|
|
145
|
+
end
|
|
146
|
+
on_intel do
|
|
147
|
+
url "https://github.com/outfitter-dev/trails/releases/download/v${version}/trails-v${version}-darwin-x64.tar.gz"
|
|
148
|
+
sha256 "${checksums['darwin-x64']}"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
on_linux do
|
|
153
|
+
on_arm do
|
|
154
|
+
url "https://github.com/outfitter-dev/trails/releases/download/v${version}/trails-v${version}-linux-arm64.tar.gz"
|
|
155
|
+
sha256 "${checksums['linux-arm64']}"
|
|
156
|
+
end
|
|
157
|
+
on_intel do
|
|
158
|
+
url "https://github.com/outfitter-dev/trails/releases/download/v${version}/trails-v${version}-linux-x64.tar.gz"
|
|
159
|
+
sha256 "${checksums['linux-x64']}"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def install
|
|
164
|
+
libexec.install Dir["*"]
|
|
165
|
+
bin.install_symlink libexec/"bin/trails"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
test do
|
|
169
|
+
assert_match version.to_s, shell_output("#{bin}/trails --version")
|
|
170
|
+
assert_match "Usage: trails", shell_output("#{bin}/trails --help")
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
`;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const readOption = (args: readonly string[], flag: string): string => {
|
|
177
|
+
const index = args.indexOf(flag);
|
|
178
|
+
const value = index === -1 ? undefined : args[index + 1];
|
|
179
|
+
if (!value) {
|
|
180
|
+
throw new Error(`Missing required argument: ${flag}`);
|
|
181
|
+
}
|
|
182
|
+
return value;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export const runTrailsHomebrewCli = async (
|
|
186
|
+
args: readonly string[]
|
|
187
|
+
): Promise<number> => {
|
|
188
|
+
try {
|
|
189
|
+
const [command, ...options] = args;
|
|
190
|
+
const version = readOption(options, '--version');
|
|
191
|
+
if (command === 'validate') {
|
|
192
|
+
const releasePath = resolve(readOption(options, '--release-json'));
|
|
193
|
+
const assetsDir = resolve(readOption(options, '--assets-dir'));
|
|
194
|
+
const release = JSON.parse(
|
|
195
|
+
readFileSync(releasePath, 'utf8')
|
|
196
|
+
) as GitHubReleaseMetadata;
|
|
197
|
+
await verifyTrailsHomebrewAssetDirectory(assetsDir, release, version);
|
|
198
|
+
console.log(`Validated Homebrew release assets for v${version}`);
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
if (command === 'render') {
|
|
202
|
+
const checksumsPath = resolve(readOption(options, '--checksums'));
|
|
203
|
+
const outputPath = resolve(readOption(options, '--output'));
|
|
204
|
+
const checksums = parseTrailsCliChecksums(
|
|
205
|
+
readFileSync(checksumsPath, 'utf8'),
|
|
206
|
+
version
|
|
207
|
+
);
|
|
208
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
209
|
+
writeFileSync(
|
|
210
|
+
outputPath,
|
|
211
|
+
renderTrailsHomebrewFormula(version, checksums)
|
|
212
|
+
);
|
|
213
|
+
console.log(`Rendered ${outputPath}`);
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
throw new Error(`Unknown Homebrew release command: ${command ?? '<none>'}`);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
219
|
+
return 1;
|
|
220
|
+
}
|
|
221
|
+
};
|