@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,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `run` trail -- Direct trail invocation by ID.
|
|
3
|
+
*
|
|
4
|
+
* Resolves a trail in the current app's topo and executes it through the
|
|
5
|
+
* shared `run()` pipeline from `@ontrails/core`. The CLI surface drives this
|
|
6
|
+
* trail with `trails run <id> [--app <name>] [inline-json]`.
|
|
7
|
+
*
|
|
8
|
+
* Resolution order:
|
|
9
|
+
*
|
|
10
|
+
* 1. If `module` is provided, load that module directly. This preserves the
|
|
11
|
+
* single-app code path used by `testExamples` and tests that hand-build a
|
|
12
|
+
* workspace fixture.
|
|
13
|
+
* 2. Otherwise, build the workspace trail-id index via
|
|
14
|
+
* {@link buildWorkspaceTrailIndex}.
|
|
15
|
+
* - If `app` is provided, resolve `app -> appDir -> module` and load that
|
|
16
|
+
* app's topo. The trail id must exist in the chosen app.
|
|
17
|
+
* - Else if the trail id has exactly one owner in the workspace index, use
|
|
18
|
+
* that owner.
|
|
19
|
+
* - Else if the trail id collides across multiple apps, return
|
|
20
|
+
* `Result.err(AmbiguousError)`. The CLI surface decides whether to prompt
|
|
21
|
+
* (TTY) or surface the error (non-TTY); the trail itself stays
|
|
22
|
+
* surface-agnostic.
|
|
23
|
+
* - Else return `Result.err(NotFoundError)`.
|
|
24
|
+
*
|
|
25
|
+
* The trail's output keeps a typed discriminator around the heterogeneous
|
|
26
|
+
* inner trail value. The value itself remains `unknown` because direct
|
|
27
|
+
* invocation can target any trail in the loaded app.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { join } from 'node:path';
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
AmbiguousError,
|
|
34
|
+
NotFoundError,
|
|
35
|
+
Result,
|
|
36
|
+
run,
|
|
37
|
+
trail,
|
|
38
|
+
} from '@ontrails/core';
|
|
39
|
+
import { buildWorkspaceTrailIndex } from '@ontrails/topographer';
|
|
40
|
+
import type {
|
|
41
|
+
WorkspaceTrailCollision,
|
|
42
|
+
WorkspaceTrailEntry,
|
|
43
|
+
} from '@ontrails/topographer';
|
|
44
|
+
import { z } from 'zod';
|
|
45
|
+
|
|
46
|
+
import {
|
|
47
|
+
createIsolatedExampleRoot,
|
|
48
|
+
writeIsolatedExampleJsonFile,
|
|
49
|
+
writeIsolatedExampleTextFile,
|
|
50
|
+
} from '../local-state-io.js';
|
|
51
|
+
|
|
52
|
+
import { tryLoadFreshAppLease } from './load-app.js';
|
|
53
|
+
import { resolveTrailRootDir } from './root-dir.js';
|
|
54
|
+
import { createIsolatedExampleInput } from './topo-support.js';
|
|
55
|
+
|
|
56
|
+
export const INNER_TRAIL_RESULT_KIND = 'inner-trail-result' as const;
|
|
57
|
+
|
|
58
|
+
export const innerTrailResultSchema = z.object({
|
|
59
|
+
kind: z.literal(INNER_TRAIL_RESULT_KIND),
|
|
60
|
+
trailId: z.string(),
|
|
61
|
+
value: z.unknown(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export type InnerTrailResult = z.infer<typeof innerTrailResultSchema>;
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Resolution outcomes
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
type ResolveAppOutcome =
|
|
71
|
+
| { readonly kind: 'resolved'; readonly module: string }
|
|
72
|
+
| {
|
|
73
|
+
readonly kind: 'ambiguous';
|
|
74
|
+
readonly candidates: readonly string[];
|
|
75
|
+
}
|
|
76
|
+
| {
|
|
77
|
+
readonly kind: 'wrong-app';
|
|
78
|
+
readonly actualOwner: string;
|
|
79
|
+
readonly requestedApp: string;
|
|
80
|
+
}
|
|
81
|
+
| { readonly kind: 'not-found'; readonly requestedApp?: string | undefined };
|
|
82
|
+
|
|
83
|
+
const collectCandidates = (
|
|
84
|
+
trailId: string,
|
|
85
|
+
index: Readonly<Record<string, WorkspaceTrailEntry>>,
|
|
86
|
+
collision: WorkspaceTrailCollision | undefined
|
|
87
|
+
): readonly string[] => {
|
|
88
|
+
if (collision !== undefined) {
|
|
89
|
+
return collision.apps;
|
|
90
|
+
}
|
|
91
|
+
const sole = index[trailId];
|
|
92
|
+
return sole === undefined ? [] : [sole.appName];
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const findOwner = (
|
|
96
|
+
trailId: string,
|
|
97
|
+
index: Readonly<Record<string, WorkspaceTrailEntry>>,
|
|
98
|
+
collision: WorkspaceTrailCollision | undefined,
|
|
99
|
+
appName: string
|
|
100
|
+
): WorkspaceTrailEntry | undefined => {
|
|
101
|
+
const sole = index[trailId];
|
|
102
|
+
if (sole?.appName === appName) {
|
|
103
|
+
return sole;
|
|
104
|
+
}
|
|
105
|
+
return collision?.owners.find((owner) => owner.appName === appName);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const resolveOwningAppViaIndex = async (
|
|
109
|
+
workspaceRoot: string,
|
|
110
|
+
trailId: string,
|
|
111
|
+
appOverride: string | undefined
|
|
112
|
+
): Promise<ResolveAppOutcome> => {
|
|
113
|
+
const result = await buildWorkspaceTrailIndex({ cwd: workspaceRoot });
|
|
114
|
+
|
|
115
|
+
const matchingCollision = result.collisions.find(
|
|
116
|
+
(entry) => entry.trailId === trailId
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Honor an explicit app override when provided.
|
|
120
|
+
if (appOverride !== undefined) {
|
|
121
|
+
const candidatesForId = collectCandidates(
|
|
122
|
+
trailId,
|
|
123
|
+
result.index,
|
|
124
|
+
matchingCollision
|
|
125
|
+
);
|
|
126
|
+
if (candidatesForId.length > 0 && !candidatesForId.includes(appOverride)) {
|
|
127
|
+
// Sole-owner mismatch: trail is owned uniquely by another app.
|
|
128
|
+
// Surface a wrong-app outcome so the user sees the actual owner,
|
|
129
|
+
// not an "ambiguous" message that doesn't apply.
|
|
130
|
+
if (candidatesForId.length === 1) {
|
|
131
|
+
const [actualOwner] = candidatesForId;
|
|
132
|
+
if (actualOwner !== undefined) {
|
|
133
|
+
return {
|
|
134
|
+
actualOwner,
|
|
135
|
+
kind: 'wrong-app',
|
|
136
|
+
requestedApp: appOverride,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { candidates: candidatesForId, kind: 'ambiguous' };
|
|
141
|
+
}
|
|
142
|
+
const owner = findOwner(
|
|
143
|
+
trailId,
|
|
144
|
+
result.index,
|
|
145
|
+
matchingCollision,
|
|
146
|
+
appOverride
|
|
147
|
+
);
|
|
148
|
+
if (owner === undefined) {
|
|
149
|
+
return { kind: 'not-found', requestedApp: appOverride };
|
|
150
|
+
}
|
|
151
|
+
return { kind: 'resolved', module: owner.modulePath };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (matchingCollision !== undefined) {
|
|
155
|
+
return {
|
|
156
|
+
candidates: matchingCollision.apps,
|
|
157
|
+
kind: 'ambiguous',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const owner = result.index[trailId];
|
|
162
|
+
if (owner === undefined) {
|
|
163
|
+
return { kind: 'not-found' };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { kind: 'resolved', module: owner.modulePath };
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const ambiguousMessage = (
|
|
170
|
+
trailId: string,
|
|
171
|
+
candidates: readonly string[]
|
|
172
|
+
): string =>
|
|
173
|
+
`Trail ID '${trailId}' exists in apps: ${candidates.join(', ')}. Re-run with --app <name>.`;
|
|
174
|
+
|
|
175
|
+
export const resolveRunModulePath = async (
|
|
176
|
+
rootDir: string,
|
|
177
|
+
module: string | undefined,
|
|
178
|
+
trailId: string,
|
|
179
|
+
app: string | undefined
|
|
180
|
+
): Promise<Result<string, Error>> => {
|
|
181
|
+
if (module !== undefined) {
|
|
182
|
+
return Result.ok(module);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const outcome = await resolveOwningAppViaIndex(rootDir, trailId, app);
|
|
186
|
+
if (outcome.kind === 'resolved') {
|
|
187
|
+
return Result.ok(outcome.module);
|
|
188
|
+
}
|
|
189
|
+
if (outcome.kind === 'ambiguous') {
|
|
190
|
+
return Result.err(
|
|
191
|
+
new AmbiguousError(ambiguousMessage(trailId, outcome.candidates), {
|
|
192
|
+
context: { candidates: outcome.candidates, trailId },
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (outcome.kind === 'wrong-app') {
|
|
197
|
+
return Result.err(
|
|
198
|
+
new NotFoundError(
|
|
199
|
+
`Trail '${trailId}' is owned by '${outcome.actualOwner}', not '${outcome.requestedApp}'.`,
|
|
200
|
+
{
|
|
201
|
+
context: {
|
|
202
|
+
actualOwner: outcome.actualOwner,
|
|
203
|
+
requestedApp: outcome.requestedApp,
|
|
204
|
+
trailId,
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
const appContext =
|
|
211
|
+
outcome.requestedApp === undefined
|
|
212
|
+
? ''
|
|
213
|
+
: ` for app '${outcome.requestedApp}'`;
|
|
214
|
+
return Result.err(
|
|
215
|
+
new NotFoundError(
|
|
216
|
+
`Trail '${trailId}' was not found${appContext} in any workspace app under ${rootDir}.`,
|
|
217
|
+
{
|
|
218
|
+
context: {
|
|
219
|
+
...(outcome.requestedApp === undefined
|
|
220
|
+
? {}
|
|
221
|
+
: { requestedApp: outcome.requestedApp }),
|
|
222
|
+
rootDir,
|
|
223
|
+
trailId,
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Ambiguous-example workspace fixture
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
const buildStubTopoSource = (appName: string): string =>
|
|
235
|
+
[
|
|
236
|
+
`const sharedIds = ['shared.id'];`,
|
|
237
|
+
`export const app = {`,
|
|
238
|
+
` name: '${appName}',`,
|
|
239
|
+
` ids: () => sharedIds,`,
|
|
240
|
+
`};`,
|
|
241
|
+
'',
|
|
242
|
+
].join('\n');
|
|
243
|
+
|
|
244
|
+
const writeAmbiguousWorkspaceFixture = (workspaceRoot: string): void => {
|
|
245
|
+
// Root package.json declaring two workspace apps.
|
|
246
|
+
writeIsolatedExampleJsonFile(workspaceRoot, 'package.json', {
|
|
247
|
+
name: 'run-ambiguous-fixture',
|
|
248
|
+
private: true,
|
|
249
|
+
type: 'module',
|
|
250
|
+
workspaces: ['apps/*'],
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Each app declares a Trails-app shape so discovery picks it up. The
|
|
254
|
+
// discovery layer only calls `topo.ids()` and reads `topo.name`, so a
|
|
255
|
+
// hand-rolled stub satisfies the `isTopo` shape without pulling in
|
|
256
|
+
// `@ontrails/core` from a temp directory that has no node_modules.
|
|
257
|
+
for (const appName of ['app-a', 'app-b'] as const) {
|
|
258
|
+
writeIsolatedExampleJsonFile(
|
|
259
|
+
workspaceRoot,
|
|
260
|
+
join('apps', appName, 'package.json'),
|
|
261
|
+
{
|
|
262
|
+
name: appName,
|
|
263
|
+
private: true,
|
|
264
|
+
trails: { module: 'src/app.ts' },
|
|
265
|
+
type: 'module',
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
writeIsolatedExampleTextFile(
|
|
269
|
+
workspaceRoot,
|
|
270
|
+
join('apps', appName, 'src/app.ts'),
|
|
271
|
+
buildStubTopoSource(appName)
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// Example input helpers
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
const buildHappyExampleInput = (): {
|
|
281
|
+
readonly input: { readonly module: string; readonly rootDir: string };
|
|
282
|
+
readonly id: string;
|
|
283
|
+
readonly module: string;
|
|
284
|
+
readonly rootDir: string;
|
|
285
|
+
} => {
|
|
286
|
+
const isolated = createIsolatedExampleInput('run-happy');
|
|
287
|
+
return {
|
|
288
|
+
id: 'survey.brief',
|
|
289
|
+
input: { module: isolated.module, rootDir: isolated.rootDir },
|
|
290
|
+
module: isolated.module,
|
|
291
|
+
rootDir: isolated.rootDir,
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const buildNotFoundExampleInput = (): {
|
|
296
|
+
readonly id: string;
|
|
297
|
+
readonly module: string;
|
|
298
|
+
readonly rootDir: string;
|
|
299
|
+
} => ({
|
|
300
|
+
...createIsolatedExampleInput('run-not-found'),
|
|
301
|
+
id: 'does.not.exist',
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const uniqueAmbiguousExampleName = (): string =>
|
|
305
|
+
`run-ambiguous-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
306
|
+
|
|
307
|
+
const buildAmbiguousExampleInput = (): {
|
|
308
|
+
readonly id: string;
|
|
309
|
+
readonly rootDir: string;
|
|
310
|
+
} => {
|
|
311
|
+
const root = createIsolatedExampleRoot(uniqueAmbiguousExampleName());
|
|
312
|
+
writeAmbiguousWorkspaceFixture(root);
|
|
313
|
+
return { id: 'shared.id', rootDir: root };
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const runTrailInputSchema = z.object({
|
|
317
|
+
app: z
|
|
318
|
+
.string()
|
|
319
|
+
.optional()
|
|
320
|
+
.describe(
|
|
321
|
+
'Workspace app to resolve the trail ID against; required when the ID is exposed by more than one app'
|
|
322
|
+
),
|
|
323
|
+
id: z.string().describe('Trail ID to invoke'),
|
|
324
|
+
input: z
|
|
325
|
+
.unknown()
|
|
326
|
+
.optional()
|
|
327
|
+
.describe(
|
|
328
|
+
'Parsed input for the resolved trail; the CLI surface JSON.parses the inline argument before passing it through'
|
|
329
|
+
),
|
|
330
|
+
module: z.string().optional().describe('Path to the app module'),
|
|
331
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
type RunTrailInput = z.output<typeof runTrailInputSchema>;
|
|
335
|
+
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
// Trail definition
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
export const runTrail = trail('run', {
|
|
341
|
+
args: ['id'],
|
|
342
|
+
blaze: async (input: RunTrailInput, ctx) => {
|
|
343
|
+
const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
|
|
344
|
+
if (rootDirResult.isErr()) {
|
|
345
|
+
return rootDirResult;
|
|
346
|
+
}
|
|
347
|
+
const rootDir = rootDirResult.value;
|
|
348
|
+
|
|
349
|
+
// Single-app back-compat: if the caller provided `module`, trust it.
|
|
350
|
+
const moduleResolution = await resolveRunModulePath(
|
|
351
|
+
rootDir,
|
|
352
|
+
input.module,
|
|
353
|
+
input.id,
|
|
354
|
+
input.app
|
|
355
|
+
);
|
|
356
|
+
if (moduleResolution.isErr()) {
|
|
357
|
+
return moduleResolution;
|
|
358
|
+
}
|
|
359
|
+
const modulePath = moduleResolution.value;
|
|
360
|
+
|
|
361
|
+
const leaseResult = await tryLoadFreshAppLease(modulePath, rootDir);
|
|
362
|
+
if (leaseResult.isErr()) {
|
|
363
|
+
return leaseResult;
|
|
364
|
+
}
|
|
365
|
+
const lease = leaseResult.value;
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const result = await run(lease.app, input.id, input.input, {
|
|
369
|
+
ctx: ctx.permit === undefined ? {} : { permit: ctx.permit },
|
|
370
|
+
});
|
|
371
|
+
if (result.isErr()) {
|
|
372
|
+
return Result.err(result.error);
|
|
373
|
+
}
|
|
374
|
+
return Result.ok({
|
|
375
|
+
kind: INNER_TRAIL_RESULT_KIND,
|
|
376
|
+
trailId: input.id,
|
|
377
|
+
value: result.value,
|
|
378
|
+
});
|
|
379
|
+
} finally {
|
|
380
|
+
lease.release();
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
description:
|
|
384
|
+
'Resolve a trail by ID in the current app and execute it through the shared pipeline',
|
|
385
|
+
examples: [
|
|
386
|
+
{
|
|
387
|
+
description:
|
|
388
|
+
'Resolve and execute a trail by ID, returning the inner trail Result value',
|
|
389
|
+
input: buildHappyExampleInput(),
|
|
390
|
+
name: 'Run trail by ID',
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
description: 'Reject an unknown trail ID with NotFoundError',
|
|
394
|
+
error: 'NotFoundError',
|
|
395
|
+
input: buildNotFoundExampleInput(),
|
|
396
|
+
name: 'Reject unknown trail ID',
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
description:
|
|
400
|
+
'Reject an ambiguous trail ID without --app with AmbiguousError so non-TTY callers see exit code 1',
|
|
401
|
+
error: 'AmbiguousError',
|
|
402
|
+
input: buildAmbiguousExampleInput(),
|
|
403
|
+
name: 'Reject ambiguous trail ID without --app',
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
input: runTrailInputSchema,
|
|
407
|
+
intent: 'write',
|
|
408
|
+
output: innerTrailResultSchema,
|
|
409
|
+
permit: { scopes: ['trails:run'] },
|
|
410
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const packageKeyOrder = [
|
|
2
|
+
'name',
|
|
3
|
+
'version',
|
|
4
|
+
'bin',
|
|
5
|
+
'type',
|
|
6
|
+
'scripts',
|
|
7
|
+
'dependencies',
|
|
8
|
+
'devDependencies',
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
const packageMapKeys = new Set<string>([
|
|
12
|
+
'bin',
|
|
13
|
+
'dependencies',
|
|
14
|
+
'devDependencies',
|
|
15
|
+
'scripts',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export type ScaffoldPackageJson = Record<string, unknown>;
|
|
19
|
+
|
|
20
|
+
const isPlainRecord = (value: unknown): value is Record<string, unknown> =>
|
|
21
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
22
|
+
|
|
23
|
+
const sortRecord = (record: Record<string, unknown>): Record<string, unknown> =>
|
|
24
|
+
Object.fromEntries(
|
|
25
|
+
Object.entries(record).toSorted(([left], [right]) =>
|
|
26
|
+
left.localeCompare(right)
|
|
27
|
+
)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const normalizePackageValue = (key: string, value: unknown): unknown =>
|
|
31
|
+
packageMapKeys.has(key) && isPlainRecord(value) ? sortRecord(value) : value;
|
|
32
|
+
|
|
33
|
+
export const normalizeScaffoldPackageJson = (
|
|
34
|
+
pkg: ScaffoldPackageJson
|
|
35
|
+
): ScaffoldPackageJson => {
|
|
36
|
+
const normalized: ScaffoldPackageJson = {};
|
|
37
|
+
|
|
38
|
+
for (const key of packageKeyOrder) {
|
|
39
|
+
if (pkg[key] !== undefined) {
|
|
40
|
+
normalized[key] = normalizePackageValue(key, pkg[key]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const key of Object.keys(pkg).toSorted()) {
|
|
45
|
+
if (!(key in normalized)) {
|
|
46
|
+
normalized[key] = normalizePackageValue(key, pkg[key]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return normalized;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const stringifyScaffoldJson = (value: unknown): string =>
|
|
54
|
+
`${JSON.stringify(value, null, 2)}\n`;
|
|
55
|
+
|
|
56
|
+
export const stringifyScaffoldPackageJson = (
|
|
57
|
+
pkg: ScaffoldPackageJson
|
|
58
|
+
): string => stringifyScaffoldJson(normalizeScaffoldPackageJson(pkg));
|