@ontrails/trails 1.0.0-beta.19 → 1.0.0-beta.21
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 +83 -0
- package/README.md +2 -0
- package/package.json +19 -13
- package/src/app.ts +43 -1
- package/src/cli.ts +10 -1
- package/src/load-app-mirror.ts +42 -0
- package/src/mcp-app.ts +30 -0
- package/src/mcp-options.ts +77 -0
- package/src/mcp.ts +8 -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/run-release-check.ts +74 -0
- package/src/scaffold-version-sync.ts +183 -0
- package/src/scaffold-versions.generated.ts +1 -1
- package/src/trails/compile.ts +13 -9
- package/src/trails/create-versions.ts +62 -0
- package/src/trails/guide.ts +10 -6
- package/src/trails/load-app.ts +440 -49
- package/src/trails/release-check.ts +104 -0
- package/src/trails/release-smoke.ts +48 -0
- package/src/trails/run-example.ts +17 -13
- package/src/trails/run-examples.ts +16 -12
- package/src/trails/run.ts +22 -18
- package/src/trails/topo-history.ts +12 -8
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const repoRoot = resolve(process.cwd());
|
|
6
|
+
const trailsBin = join(repoRoot, 'apps/trails/bin/trails.ts');
|
|
7
|
+
|
|
8
|
+
type JsonObject = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
export interface WayfinderDogfoodSmokeResult {
|
|
11
|
+
readonly check: 'wayfinder-dogfood';
|
|
12
|
+
readonly message: string;
|
|
13
|
+
readonly passed: true;
|
|
14
|
+
readonly trailCount: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const assertObject = (value: unknown, label: string): JsonObject => {
|
|
18
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
19
|
+
throw new Error(`${label} did not return a JSON object`);
|
|
20
|
+
}
|
|
21
|
+
return value as JsonObject;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const assertFreshSource = (value: JsonObject, label: string): void => {
|
|
25
|
+
const freshness = assertObject(value['freshness'], `${label}.freshness`);
|
|
26
|
+
if (freshness['status'] !== 'fresh') {
|
|
27
|
+
throw new Error(`${label} did not read fresh artifacts`);
|
|
28
|
+
}
|
|
29
|
+
const source = assertObject(value['source'], `${label}.source`);
|
|
30
|
+
if (source['kind'] !== 'topoGraph') {
|
|
31
|
+
throw new Error(`${label} did not read the TopoGraph source`);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const parseJson = (stdout: string, label: string): JsonObject => {
|
|
36
|
+
try {
|
|
37
|
+
return assertObject(JSON.parse(stdout) as unknown, label);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw new Error(`${label} did not produce valid JSON`, { cause: error });
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const runWayfind = (tempRoot: string, args: readonly string[]): JsonObject => {
|
|
44
|
+
const command = [
|
|
45
|
+
process.execPath,
|
|
46
|
+
trailsBin,
|
|
47
|
+
'wayfind',
|
|
48
|
+
...args,
|
|
49
|
+
'--root-dir',
|
|
50
|
+
tempRoot,
|
|
51
|
+
'--json',
|
|
52
|
+
];
|
|
53
|
+
const result = Bun.spawnSync({
|
|
54
|
+
cmd: command,
|
|
55
|
+
cwd: repoRoot,
|
|
56
|
+
env: { ...process.env, NO_COLOR: '1' } as Record<
|
|
57
|
+
string,
|
|
58
|
+
string | undefined
|
|
59
|
+
>,
|
|
60
|
+
stderr: 'pipe',
|
|
61
|
+
stdout: 'pipe',
|
|
62
|
+
});
|
|
63
|
+
const stdout = result.stdout.toString();
|
|
64
|
+
const stderr = result.stderr.toString();
|
|
65
|
+
if (result.exitCode !== 0) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
[
|
|
68
|
+
`Wayfinder dogfood command failed: ${command.join(' ')}`,
|
|
69
|
+
`exitCode: ${result.exitCode}`,
|
|
70
|
+
`stdout: ${stdout}`,
|
|
71
|
+
`stderr: ${stderr}`,
|
|
72
|
+
].join('\n')
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return parseJson(stdout, `trails ${args.join(' ')}`);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const assertSearchFindsWayfinder = (search: JsonObject): void => {
|
|
79
|
+
const { matches } = search;
|
|
80
|
+
if (!Array.isArray(matches)) {
|
|
81
|
+
throw new TypeError('wayfind search did not return matches');
|
|
82
|
+
}
|
|
83
|
+
const ids = matches
|
|
84
|
+
.map((match) => assertObject(match, 'wayfind search match')['id'])
|
|
85
|
+
.filter((id): id is string => typeof id === 'string');
|
|
86
|
+
if (!ids.includes('wayfind.search')) {
|
|
87
|
+
throw new Error('wayfind search did not find wayfind.search');
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const assertErrorsFindWayfinder = (errorsResult: JsonObject): void => {
|
|
92
|
+
const { errors } = errorsResult;
|
|
93
|
+
if (!Array.isArray(errors)) {
|
|
94
|
+
throw new TypeError('wayfind errors did not return errors');
|
|
95
|
+
}
|
|
96
|
+
const searchEntry = errors
|
|
97
|
+
.map((entry) => assertObject(entry, 'wayfind errors entry'))
|
|
98
|
+
.find((entry) => entry['trailId'] === 'wayfind.search');
|
|
99
|
+
if (searchEntry === undefined) {
|
|
100
|
+
throw new Error('wayfind errors did not include wayfind.search');
|
|
101
|
+
}
|
|
102
|
+
const completeness = assertObject(
|
|
103
|
+
searchEntry['completeness'],
|
|
104
|
+
'wayfind errors completeness'
|
|
105
|
+
);
|
|
106
|
+
const emitted = assertObject(
|
|
107
|
+
completeness['emitted'],
|
|
108
|
+
'wayfind errors emitted completeness'
|
|
109
|
+
);
|
|
110
|
+
if (emitted['status'] !== 'unknown') {
|
|
111
|
+
throw new Error('wayfind errors overclaimed emitted-error completeness');
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const assertAdaptersFindHono = (adaptersResult: JsonObject): void => {
|
|
116
|
+
const counts = assertObject(
|
|
117
|
+
adaptersResult['counts'],
|
|
118
|
+
'wayfind adapters counts'
|
|
119
|
+
);
|
|
120
|
+
if (counts['configured'] !== 1 || counts['used'] !== 1) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
'wayfind adapters did not report configured/used adapter facts'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (counts['observed'] !== 0) {
|
|
126
|
+
throw new Error('wayfind adapters reported unsupported observed facts');
|
|
127
|
+
}
|
|
128
|
+
const { adapters } = adaptersResult;
|
|
129
|
+
if (!Array.isArray(adapters)) {
|
|
130
|
+
throw new TypeError('wayfind adapters did not return adapters');
|
|
131
|
+
}
|
|
132
|
+
const ids = adapters
|
|
133
|
+
.map((adapter) => assertObject(adapter, 'wayfind adapters fact')['key'])
|
|
134
|
+
.filter((key): key is string => typeof key === 'string');
|
|
135
|
+
if (!ids.includes('@ontrails/hono:http:used')) {
|
|
136
|
+
throw new Error('wayfind adapters did not find Hono conformance usage');
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const assertContractForSearch = (contractResult: JsonObject): void => {
|
|
141
|
+
const contract = assertObject(contractResult['contract'], 'contract');
|
|
142
|
+
if (contract['id'] !== 'wayfind.search') {
|
|
143
|
+
throw new Error('wayfind contract did not inspect wayfind.search');
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const assertResolvedTarget = (value: JsonObject, label: string): void => {
|
|
148
|
+
const target = assertObject(value['target'], `${label}.target`);
|
|
149
|
+
if (target['id'] !== 'wayfind.search') {
|
|
150
|
+
throw new Error(`${label} did not resolve wayfind.search`);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const runWayfinderDogfoodSmoke =
|
|
155
|
+
async (): Promise<WayfinderDogfoodSmokeResult> => {
|
|
156
|
+
const tempRoot = await mkdtemp(join(tmpdir(), 'trails-wayfinder-dogfood-'));
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const [{ trailsMcpApp }, { exportCurrentTopo }] = await Promise.all([
|
|
160
|
+
import('../mcp-app.js'),
|
|
161
|
+
import('../trails/topo-store-support.js'),
|
|
162
|
+
]);
|
|
163
|
+
const exported = await exportCurrentTopo(trailsMcpApp, {
|
|
164
|
+
rootDir: tempRoot,
|
|
165
|
+
});
|
|
166
|
+
if (exported.isErr()) {
|
|
167
|
+
throw exported.error;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const overview = runWayfind(tempRoot, ['overview']);
|
|
171
|
+
assertFreshSource(overview, 'wayfind overview');
|
|
172
|
+
const counts = assertObject(
|
|
173
|
+
overview['counts'],
|
|
174
|
+
'wayfind overview counts'
|
|
175
|
+
);
|
|
176
|
+
const trailCount = counts['trails'];
|
|
177
|
+
if (typeof trailCount !== 'number' || trailCount < 1) {
|
|
178
|
+
throw new Error('wayfind overview did not report trail counts');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const search = runWayfind(tempRoot, [
|
|
182
|
+
'search',
|
|
183
|
+
'--input-json',
|
|
184
|
+
'{"filters":{"kind":"trail","idPrefix":"wayfind."}}',
|
|
185
|
+
]);
|
|
186
|
+
assertFreshSource(search, 'wayfind search');
|
|
187
|
+
assertSearchFindsWayfinder(search);
|
|
188
|
+
|
|
189
|
+
const errors = runWayfind(tempRoot, [
|
|
190
|
+
'errors',
|
|
191
|
+
'--input-json',
|
|
192
|
+
'{"filters":{"kind":"trail","idPrefix":"wayfind."}}',
|
|
193
|
+
]);
|
|
194
|
+
assertFreshSource(errors, 'wayfind errors');
|
|
195
|
+
assertErrorsFindWayfinder(errors);
|
|
196
|
+
|
|
197
|
+
const adapters = runWayfind(repoRoot, ['adapters']);
|
|
198
|
+
assertAdaptersFindHono(adapters);
|
|
199
|
+
|
|
200
|
+
const contract = runWayfind(tempRoot, ['contract', 'wayfind.search']);
|
|
201
|
+
assertFreshSource(contract, 'wayfind contract');
|
|
202
|
+
assertContractForSearch(contract);
|
|
203
|
+
|
|
204
|
+
const nearby = runWayfind(tempRoot, ['nearby', 'wayfind.search']);
|
|
205
|
+
assertFreshSource(nearby, 'wayfind nearby');
|
|
206
|
+
assertResolvedTarget(nearby, 'wayfind nearby');
|
|
207
|
+
|
|
208
|
+
const impact = runWayfind(tempRoot, ['impact', 'wayfind.search']);
|
|
209
|
+
assertFreshSource(impact, 'wayfind impact');
|
|
210
|
+
assertResolvedTarget(impact, 'wayfind impact');
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
check: 'wayfinder-dogfood',
|
|
214
|
+
message: `Wayfinder dogfood smoke passed: ${String(trailCount)} trails inspected from saved operator topo artifacts.`,
|
|
215
|
+
passed: true,
|
|
216
|
+
trailCount,
|
|
217
|
+
};
|
|
218
|
+
} finally {
|
|
219
|
+
await rm(tempRoot, { force: true, recursive: true });
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
if (import.meta.main) {
|
|
224
|
+
const result = await runWayfinderDogfoodSmoke();
|
|
225
|
+
console.log(result.message);
|
|
226
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ActionResultContext } from '@ontrails/cli';
|
|
2
|
+
import { deriveOutputMode } from '@ontrails/cli';
|
|
3
|
+
|
|
4
|
+
interface ReleaseCheckResultValue {
|
|
5
|
+
readonly formatted: string;
|
|
6
|
+
readonly passed: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const isReleaseCheckResultValue = (
|
|
10
|
+
value: unknown
|
|
11
|
+
): value is ReleaseCheckResultValue => {
|
|
12
|
+
if (typeof value !== 'object' || value === null) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const candidate = value as Record<string, unknown>;
|
|
17
|
+
return (
|
|
18
|
+
typeof candidate['formatted'] === 'string' &&
|
|
19
|
+
typeof candidate['passed'] === 'boolean'
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const wantsStructuredOutput = (ctx: ActionResultContext): boolean =>
|
|
24
|
+
deriveOutputMode(ctx.flags, ctx.topoName).mode !== 'text';
|
|
25
|
+
|
|
26
|
+
const readReleaseCheckResultValue = (
|
|
27
|
+
ctx: ActionResultContext
|
|
28
|
+
): ReleaseCheckResultValue | undefined => {
|
|
29
|
+
if (ctx.trail.id !== 'release.check' || ctx.result.isErr()) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return isReleaseCheckResultValue(ctx.result.value)
|
|
34
|
+
? ctx.result.value
|
|
35
|
+
: undefined;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const applyReleaseCheckExitCode = (
|
|
39
|
+
ctx: ActionResultContext
|
|
40
|
+
): boolean => {
|
|
41
|
+
if (ctx.trail.id !== 'release.check') {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (ctx.result.isErr()) {
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const value = readReleaseCheckResultValue(ctx);
|
|
51
|
+
if (!value) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
process.exitCode = value.passed ? 0 : 1;
|
|
56
|
+
return true;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const tryReleaseCheckOutput = (ctx: ActionResultContext): boolean => {
|
|
60
|
+
const value = readReleaseCheckResultValue(ctx);
|
|
61
|
+
if (!value) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
applyReleaseCheckExitCode(ctx);
|
|
66
|
+
if (wantsStructuredOutput(ctx)) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (value.formatted.length > 0) {
|
|
71
|
+
process.stdout.write(`${value.formatted}\n`);
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffold dependency version derivation for the `create.versions` trail.
|
|
3
|
+
*
|
|
4
|
+
* Generates or validates `apps/trails/src/scaffold-versions.generated.ts`
|
|
5
|
+
* from the root `package.json` catalog and devDependencies, and validates
|
|
6
|
+
* that generated `@ontrails/*` package pins track the `@ontrails/trails`
|
|
7
|
+
* version exactly.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
ontrailsPackageRange as appOntrailsPackageRange,
|
|
14
|
+
trailsPackageVersion as appTrailsPackageVersion,
|
|
15
|
+
} from './versions.js';
|
|
16
|
+
|
|
17
|
+
interface RootPackageJson {
|
|
18
|
+
readonly catalog?: Record<string, string>;
|
|
19
|
+
readonly devDependencies?: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface OntrailsPackagePinState {
|
|
23
|
+
readonly ontrailsPackageRange?: string;
|
|
24
|
+
readonly trailsPackageVersion?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SyncScaffoldVersionsResult {
|
|
28
|
+
readonly generatedPath: string;
|
|
29
|
+
readonly mode: 'check' | 'write';
|
|
30
|
+
readonly written: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const diagnoseOntrailsPackagePin = ({
|
|
34
|
+
ontrailsPackageRange,
|
|
35
|
+
trailsPackageVersion,
|
|
36
|
+
}: OntrailsPackagePinState): string | undefined => {
|
|
37
|
+
if (
|
|
38
|
+
typeof ontrailsPackageRange !== 'string' ||
|
|
39
|
+
typeof trailsPackageVersion !== 'string'
|
|
40
|
+
) {
|
|
41
|
+
return (
|
|
42
|
+
'create.versions: apps/trails/src/versions.ts must export ' +
|
|
43
|
+
'`ontrailsPackageRange` and `trailsPackageVersion`.'
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (ontrailsPackageRange !== trailsPackageVersion) {
|
|
47
|
+
return (
|
|
48
|
+
'create.versions: scaffolded @ontrails/* packages must be exact ' +
|
|
49
|
+
`pins for @ontrails/trails (${trailsPackageVersion}); got ` +
|
|
50
|
+
`${ontrailsPackageRange}.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const requireValue = (
|
|
57
|
+
value: string | undefined,
|
|
58
|
+
label: string,
|
|
59
|
+
source: string,
|
|
60
|
+
rootPackageJsonPath: string
|
|
61
|
+
): string => {
|
|
62
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`create.versions: missing "${label}" under ${source} in ${rootPackageJsonPath}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const loadScaffoldVersions = async (
|
|
71
|
+
rootPackageJsonPath: string
|
|
72
|
+
): Promise<Record<string, string> & Readonly<Record<string, string>>> => {
|
|
73
|
+
const rootPkg = (await Bun.file(
|
|
74
|
+
rootPackageJsonPath
|
|
75
|
+
).json()) as RootPackageJson;
|
|
76
|
+
const catalog = rootPkg.catalog ?? {};
|
|
77
|
+
const devDeps = rootPkg.devDependencies ?? {};
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
bunTypes: requireValue(
|
|
81
|
+
devDeps['@types/bun'],
|
|
82
|
+
'@types/bun',
|
|
83
|
+
'devDependencies',
|
|
84
|
+
rootPackageJsonPath
|
|
85
|
+
),
|
|
86
|
+
commander: requireValue(
|
|
87
|
+
catalog['commander'],
|
|
88
|
+
'commander',
|
|
89
|
+
'catalog',
|
|
90
|
+
rootPackageJsonPath
|
|
91
|
+
),
|
|
92
|
+
lefthook: requireValue(
|
|
93
|
+
devDeps['lefthook'],
|
|
94
|
+
'lefthook',
|
|
95
|
+
'devDependencies',
|
|
96
|
+
rootPackageJsonPath
|
|
97
|
+
),
|
|
98
|
+
oxfmt: requireValue(
|
|
99
|
+
devDeps['oxfmt'],
|
|
100
|
+
'oxfmt',
|
|
101
|
+
'devDependencies',
|
|
102
|
+
rootPackageJsonPath
|
|
103
|
+
),
|
|
104
|
+
oxlint: requireValue(
|
|
105
|
+
devDeps['oxlint'],
|
|
106
|
+
'oxlint',
|
|
107
|
+
'devDependencies',
|
|
108
|
+
rootPackageJsonPath
|
|
109
|
+
),
|
|
110
|
+
typescript: requireValue(
|
|
111
|
+
devDeps['typescript'],
|
|
112
|
+
'typescript',
|
|
113
|
+
'devDependencies',
|
|
114
|
+
rootPackageJsonPath
|
|
115
|
+
),
|
|
116
|
+
ultracite: requireValue(
|
|
117
|
+
devDeps['ultracite'],
|
|
118
|
+
'ultracite',
|
|
119
|
+
'devDependencies',
|
|
120
|
+
rootPackageJsonPath
|
|
121
|
+
),
|
|
122
|
+
zod: requireValue(catalog['zod'], 'zod', 'catalog', rootPackageJsonPath),
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const renderGeneratedFile = (
|
|
127
|
+
versions: Record<string, string> & Readonly<Record<string, string>>
|
|
128
|
+
): string => {
|
|
129
|
+
const keys = Object.keys(versions).toSorted();
|
|
130
|
+
const lines = keys.map((key: string) => ` ${key}: '${versions[key]}',`);
|
|
131
|
+
return [
|
|
132
|
+
'// GENERATED FILE — do not edit by hand. Run `bun run scaffold-versions:sync` to regenerate.',
|
|
133
|
+
'',
|
|
134
|
+
'export const scaffoldDependencyVersions = {',
|
|
135
|
+
...lines,
|
|
136
|
+
'} as const;',
|
|
137
|
+
'',
|
|
138
|
+
].join('\n');
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const syncScaffoldVersions = async (options: {
|
|
142
|
+
check: boolean;
|
|
143
|
+
rootDir: string;
|
|
144
|
+
}): Promise<SyncScaffoldVersionsResult> => {
|
|
145
|
+
const rootPackageJsonPath = resolve(options.rootDir, 'package.json');
|
|
146
|
+
const generatedPath = resolve(
|
|
147
|
+
options.rootDir,
|
|
148
|
+
'apps/trails/src/scaffold-versions.generated.ts'
|
|
149
|
+
);
|
|
150
|
+
const versions = await loadScaffoldVersions(rootPackageJsonPath);
|
|
151
|
+
const expected = renderGeneratedFile(versions);
|
|
152
|
+
|
|
153
|
+
if (options.check) {
|
|
154
|
+
const generatedFile = Bun.file(generatedPath);
|
|
155
|
+
const existing = (await generatedFile.exists())
|
|
156
|
+
? await generatedFile.text()
|
|
157
|
+
: undefined;
|
|
158
|
+
if (existing !== expected) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`create.versions: ${generatedPath} is out of date.\n` +
|
|
161
|
+
'Run `bun run scaffold-versions:sync` to regenerate.'
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
await Bun.write(generatedPath, expected);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Normally quiet because versions.ts derives both exports from one source;
|
|
169
|
+
// this trips only if future/manual drift breaks that invariant.
|
|
170
|
+
const diagnostic = diagnoseOntrailsPackagePin({
|
|
171
|
+
ontrailsPackageRange: appOntrailsPackageRange,
|
|
172
|
+
trailsPackageVersion: appTrailsPackageVersion,
|
|
173
|
+
});
|
|
174
|
+
if (diagnostic !== undefined) {
|
|
175
|
+
throw new Error(diagnostic);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
generatedPath,
|
|
180
|
+
mode: options.check ? 'check' : 'write',
|
|
181
|
+
written: !options.check,
|
|
182
|
+
};
|
|
183
|
+
};
|
package/src/trails/compile.ts
CHANGED
|
@@ -16,8 +16,19 @@ export const compileCurrentTopo = async (
|
|
|
16
16
|
options?: { readonly force?: boolean | undefined; readonly rootDir?: string }
|
|
17
17
|
): Promise<Result<TopoExportReport, Error>> => exportCurrentTopo(app, options);
|
|
18
18
|
|
|
19
|
+
const compileTrailInputSchema = z.object({
|
|
20
|
+
force: z
|
|
21
|
+
.boolean()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Record graph-only force events for breaking changes'),
|
|
24
|
+
module: z.string().optional().describe('Path to the app module'),
|
|
25
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
type CompileTrailInput = z.output<typeof compileTrailInputSchema>;
|
|
29
|
+
|
|
19
30
|
export const compileTrail = trail('compile', {
|
|
20
|
-
blaze: async (input, ctx) => {
|
|
31
|
+
blaze: async (input: CompileTrailInput, ctx) => {
|
|
21
32
|
const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
|
|
22
33
|
if (rootDirResult.isErr()) {
|
|
23
34
|
return rootDirResult;
|
|
@@ -44,14 +55,7 @@ export const compileTrail = trail('compile', {
|
|
|
44
55
|
name: 'Compile the current topo artifacts',
|
|
45
56
|
},
|
|
46
57
|
],
|
|
47
|
-
input:
|
|
48
|
-
force: z
|
|
49
|
-
.boolean()
|
|
50
|
-
.optional()
|
|
51
|
-
.describe('Record graph-only force events for breaking changes'),
|
|
52
|
-
module: z.string().optional().describe('Path to the app module'),
|
|
53
|
-
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
54
|
-
}),
|
|
58
|
+
input: compileTrailInputSchema,
|
|
55
59
|
intent: 'write',
|
|
56
60
|
output: z.object({
|
|
57
61
|
hash: z.string(),
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `create.versions` trail -- Sync generated scaffold dependency versions.
|
|
3
|
+
*
|
|
4
|
+
* Derives `apps/trails/src/scaffold-versions.generated.ts` from the root
|
|
5
|
+
* `package.json` catalog and devDependencies. Graduated from
|
|
6
|
+
* `scripts/sync-scaffold-versions.ts`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Result, trail, ValidationError } from '@ontrails/core';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
|
|
12
|
+
import { syncScaffoldVersions } from '../scaffold-version-sync.js';
|
|
13
|
+
import { resolveTrailRootDir } from './root-dir.js';
|
|
14
|
+
|
|
15
|
+
const createVersionsInputSchema = z.object({
|
|
16
|
+
check: z
|
|
17
|
+
.boolean()
|
|
18
|
+
.default(false)
|
|
19
|
+
.describe('Verify the generated file is current instead of writing'),
|
|
20
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const createVersionsOutputSchema = z.object({
|
|
24
|
+
generatedPath: z.string(),
|
|
25
|
+
mode: z.enum(['check', 'write']),
|
|
26
|
+
written: z.boolean(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const createVersionsTrail = trail('create.versions', {
|
|
30
|
+
blaze: async (input, ctx) => {
|
|
31
|
+
const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
|
|
32
|
+
if (rootDirResult.isErr()) {
|
|
33
|
+
return rootDirResult;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
return Result.ok(
|
|
38
|
+
await syncScaffoldVersions({
|
|
39
|
+
check: input.check,
|
|
40
|
+
rootDir: rootDirResult.value,
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return Result.err(
|
|
45
|
+
new ValidationError(
|
|
46
|
+
error instanceof Error ? error.message : String(error)
|
|
47
|
+
)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
description: 'Sync generated scaffold dependency versions',
|
|
52
|
+
examples: [
|
|
53
|
+
{
|
|
54
|
+
input: { check: true },
|
|
55
|
+
name: 'Verify generated scaffold versions are current',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
input: createVersionsInputSchema,
|
|
59
|
+
intent: 'write',
|
|
60
|
+
output: createVersionsOutputSchema,
|
|
61
|
+
permit: { scopes: ['project:write'] },
|
|
62
|
+
});
|
package/src/trails/guide.ts
CHANGED
|
@@ -28,12 +28,20 @@ interface GuideEntry {
|
|
|
28
28
|
readonly kind: 'trail';
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const guideTrailInputSchema = z.object({
|
|
32
|
+
module: z.string().optional().describe('Path to the app module'),
|
|
33
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
34
|
+
trailId: z.string().optional().describe('Trail ID for detailed guidance'),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
type GuideTrailInput = z.output<typeof guideTrailInputSchema>;
|
|
38
|
+
|
|
31
39
|
// ---------------------------------------------------------------------------
|
|
32
40
|
// Helpers
|
|
33
41
|
// ---------------------------------------------------------------------------
|
|
34
42
|
|
|
35
43
|
export const guideTrail = trail('guide', {
|
|
36
|
-
blaze: async (input, ctx) => {
|
|
44
|
+
blaze: async (input: GuideTrailInput, ctx) => {
|
|
37
45
|
const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
|
|
38
46
|
if (rootDirResult.isErr()) {
|
|
39
47
|
return rootDirResult;
|
|
@@ -80,11 +88,7 @@ export const guideTrail = trail('guide', {
|
|
|
80
88
|
name: 'List trail guidance',
|
|
81
89
|
},
|
|
82
90
|
],
|
|
83
|
-
input:
|
|
84
|
-
module: z.string().optional().describe('Path to the app module'),
|
|
85
|
-
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
86
|
-
trailId: z.string().optional().describe('Trail ID for detailed guidance'),
|
|
87
|
-
}),
|
|
91
|
+
input: guideTrailInputSchema,
|
|
88
92
|
intent: 'read',
|
|
89
93
|
output: z.discriminatedUnion('mode', [
|
|
90
94
|
z.object({
|