@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.
Files changed (121) hide show
  1. package/CHANGELOG.md +1906 -0
  2. package/README.md +48 -0
  3. package/bin/trails.ts +3 -0
  4. package/package.json +57 -0
  5. package/src/app.ts +167 -0
  6. package/src/clack.ts +111 -0
  7. package/src/cli.ts +308 -0
  8. package/src/completions.ts +431 -0
  9. package/src/lifecycle-source-io.ts +33 -0
  10. package/src/load-app-mirror.ts +202 -0
  11. package/src/local-state-io.ts +129 -0
  12. package/src/mcp-app.ts +42 -0
  13. package/src/mcp-options.ts +92 -0
  14. package/src/mcp.ts +8 -0
  15. package/src/project-writes.ts +377 -0
  16. package/src/regrade/audit.ts +571 -0
  17. package/src/regrade/config.ts +152 -0
  18. package/src/regrade/history.ts +636 -0
  19. package/src/regrade/lifecycle.ts +76 -0
  20. package/src/regrade/live-api-preserve.ts +123 -0
  21. package/src/regrade/plan-artifact.ts +515 -0
  22. package/src/regrade/plan-derivation.ts +301 -0
  23. package/src/regrade/prepared-run.ts +259 -0
  24. package/src/regrade/receipt-history.ts +446 -0
  25. package/src/regrade/source-transaction.ts +185 -0
  26. package/src/release/bindings.ts +58 -0
  27. package/src/release/changeset-packages.ts +99 -0
  28. package/src/release/check.ts +1191 -0
  29. package/src/release/cli-bundle.ts +575 -0
  30. package/src/release/config.ts +73 -0
  31. package/src/release/contract-facts.ts +425 -0
  32. package/src/release/homebrew.ts +221 -0
  33. package/src/release/index.ts +180 -0
  34. package/src/release/lock-roundtrip-smoke.ts +255 -0
  35. package/src/release/lock-roundtrip-workspace.ts +107 -0
  36. package/src/release/native-bun-publish.ts +964 -0
  37. package/src/release/native-bun-registry.ts +848 -0
  38. package/src/release/notes-cli.ts +171 -0
  39. package/src/release/notes.ts +390 -0
  40. package/src/release/pack-coherence.ts +455 -0
  41. package/src/release/package-route-facts.ts +146 -0
  42. package/src/release/packed-artifacts-smoke.ts +236 -0
  43. package/src/release/policy.ts +1780 -0
  44. package/src/release/semver.ts +104 -0
  45. package/src/release/smoke.ts +56 -0
  46. package/src/release/stable-version-release.ts +80 -0
  47. package/src/release/wayfinder-dogfood-smoke.ts +762 -0
  48. package/src/release/zero-line-transition.ts +68 -0
  49. package/src/retired-topo-command.ts +36 -0
  50. package/src/run-adapter-check.ts +76 -0
  51. package/src/run-argv.ts +133 -0
  52. package/src/run-collision.ts +126 -0
  53. package/src/run-completions-install.ts +179 -0
  54. package/src/run-example.ts +149 -0
  55. package/src/run-examples.ts +148 -0
  56. package/src/run-quiet.ts +75 -0
  57. package/src/run-regrade-progress.ts +47 -0
  58. package/src/run-release-check.ts +74 -0
  59. package/src/run-schema.ts +74 -0
  60. package/src/run-trace.ts +273 -0
  61. package/src/run-warden.ts +39 -0
  62. package/src/run-watch-project.ts +52 -0
  63. package/src/run-watch.ts +381 -0
  64. package/src/run-wayfind-outline.ts +170 -0
  65. package/src/scaffold-version-sync.ts +183 -0
  66. package/src/scaffold-versions.generated.ts +12 -0
  67. package/src/trails/adapter-check.ts +244 -0
  68. package/src/trails/add-surface.ts +816 -0
  69. package/src/trails/add-trail.ts +141 -0
  70. package/src/trails/add-verify.ts +252 -0
  71. package/src/trails/compile.ts +118 -0
  72. package/src/trails/completions-complete.ts +236 -0
  73. package/src/trails/completions.ts +47 -0
  74. package/src/trails/config-explain.ts +43 -0
  75. package/src/trails/create-adapter.ts +785 -0
  76. package/src/trails/create-scaffold.ts +1215 -0
  77. package/src/trails/create-versions.ts +62 -0
  78. package/src/trails/create.ts +652 -0
  79. package/src/trails/deprecate.ts +59 -0
  80. package/src/trails/dev-clean.ts +80 -0
  81. package/src/trails/dev-reset.ts +48 -0
  82. package/src/trails/dev-stats.ts +71 -0
  83. package/src/trails/dev-support.ts +360 -0
  84. package/src/trails/doctor.ts +77 -0
  85. package/src/trails/draft-promote.ts +949 -0
  86. package/src/trails/guide.ts +106 -0
  87. package/src/trails/load-app.ts +1145 -0
  88. package/src/trails/operator-context.ts +66 -0
  89. package/src/trails/project-context-output.ts +304 -0
  90. package/src/trails/project-context.ts +613 -0
  91. package/src/trails/project.ts +65 -0
  92. package/src/trails/regrade.ts +4951 -0
  93. package/src/trails/release-check.ts +113 -0
  94. package/src/trails/release-smoke.ts +49 -0
  95. package/src/trails/revise.ts +53 -0
  96. package/src/trails/root-dir.ts +21 -0
  97. package/src/trails/run-example.ts +592 -0
  98. package/src/trails/run-examples.ts +149 -0
  99. package/src/trails/run.ts +496 -0
  100. package/src/trails/scaffold-json.ts +60 -0
  101. package/src/trails/scaffold-topo-identity.ts +479 -0
  102. package/src/trails/survey.ts +990 -0
  103. package/src/trails/topo-activation.ts +14 -0
  104. package/src/trails/topo-constants.ts +2 -0
  105. package/src/trails/topo-history.ts +47 -0
  106. package/src/trails/topo-output-schemas.ts +259 -0
  107. package/src/trails/topo-pin.ts +38 -0
  108. package/src/trails/topo-read-support.ts +368 -0
  109. package/src/trails/topo-reports.ts +809 -0
  110. package/src/trails/topo-store-support.ts +323 -0
  111. package/src/trails/topo-support.ts +247 -0
  112. package/src/trails/topo-unpin.ts +61 -0
  113. package/src/trails/topo.ts +92 -0
  114. package/src/trails/validate.ts +348 -0
  115. package/src/trails/version-lifecycle-support.ts +936 -0
  116. package/src/trails/warden-guide.ts +134 -0
  117. package/src/trails/warden.ts +598 -0
  118. package/src/trails/wayfind-diff.ts +716 -0
  119. package/src/trails/wayfind-outline.ts +876 -0
  120. package/src/trails/wayfind.ts +1319 -0
  121. package/src/versions.ts +31 -0
@@ -0,0 +1,149 @@
1
+ /**
2
+ * `run.examples` trail -- list examples for a target trail.
3
+ */
4
+
5
+ import {
6
+ NotFoundError,
7
+ Result,
8
+ deriveStructuredTrailExamples,
9
+ trail,
10
+ } from '@ontrails/core';
11
+ import type { StructuredTrailExample, Topo } from '@ontrails/core';
12
+ import { z } from 'zod';
13
+
14
+ import { withFreshAppLease } from './operator-context.js';
15
+ import { assertConfiguredAppBinding } from './project-context.js';
16
+ import {
17
+ operatorProjectContextOutput,
18
+ operatorProjectContextOutputSchema,
19
+ } from './project-context-output.js';
20
+ import { resolveRunTargetProject } from './run.js';
21
+ import { createCurrentAppExampleInput } from './topo-support.js';
22
+
23
+ export const RUN_EXAMPLES_LISTING_KIND = 'examples-listing' as const;
24
+
25
+ export const structuredTrailExampleSchema = z
26
+ .object({
27
+ description: z.string().optional(),
28
+ error: z.string().optional(),
29
+ expected: z.unknown().optional(),
30
+ expectedMatch: z.unknown().optional(),
31
+ input: z.unknown(),
32
+ kind: z.union([z.literal('success'), z.literal('error')]),
33
+ name: z.string(),
34
+ provenance: z.object({ source: z.literal('trail.examples') }),
35
+ signals: z
36
+ .array(
37
+ z.object({
38
+ payload: z.unknown().optional(),
39
+ payloadMatch: z.unknown().optional(),
40
+ signalId: z.string(),
41
+ times: z.number().optional(),
42
+ })
43
+ )
44
+ .readonly()
45
+ .optional(),
46
+ })
47
+ .passthrough();
48
+
49
+ export const runExamplesListingSchema = z.object({
50
+ examples: z.array(structuredTrailExampleSchema).readonly(),
51
+ executedAppId: z.string(),
52
+ kind: z.literal(RUN_EXAMPLES_LISTING_KIND),
53
+ project: operatorProjectContextOutputSchema,
54
+ trailId: z.string(),
55
+ });
56
+
57
+ export type RunExamplesListing = z.infer<typeof runExamplesListingSchema>;
58
+
59
+ const buildHappyExampleInput = (): {
60
+ readonly id: string;
61
+ readonly module: string;
62
+ readonly rootDir: string;
63
+ } => ({
64
+ ...createCurrentAppExampleInput({ selection: 'configured-project' }),
65
+ id: 'survey.brief',
66
+ });
67
+
68
+ const buildExamplesListing = (
69
+ app: Topo,
70
+ trailId: string,
71
+ project: RunExamplesListing['project']
72
+ ): Result<RunExamplesListing, Error> => {
73
+ const target = app.get(trailId);
74
+ if (target === undefined) {
75
+ return Result.err(
76
+ new NotFoundError(
77
+ `Trail '${trailId}' was not found in the resolved app.`,
78
+ { context: { trailId } }
79
+ )
80
+ );
81
+ }
82
+
83
+ const structured =
84
+ (deriveStructuredTrailExamples(target.examples) as
85
+ | readonly StructuredTrailExample[]
86
+ | undefined) ?? [];
87
+ return Result.ok({
88
+ examples: structured as unknown as RunExamplesListing['examples'],
89
+ executedAppId: app.name,
90
+ kind: RUN_EXAMPLES_LISTING_KIND,
91
+ project,
92
+ trailId,
93
+ });
94
+ };
95
+
96
+ const runExamplesTrailInputSchema = z.object({
97
+ app: z
98
+ .string()
99
+ .optional()
100
+ .describe(
101
+ 'Workspace app to resolve the trail ID against; required when the ID is exposed by more than one app'
102
+ ),
103
+ id: z.string().describe('Trail ID whose examples should be listed'),
104
+ module: z.string().optional().describe('Path to the app module'),
105
+ rootDir: z.string().optional().describe('Workspace root directory'),
106
+ });
107
+
108
+ type RunExamplesTrailInput = z.output<typeof runExamplesTrailInputSchema>;
109
+
110
+ export const runExamplesTrail = trail('run.examples', {
111
+ args: ['id'],
112
+ description: "List a trail's examples without executing it",
113
+ examples: [
114
+ {
115
+ description: 'List examples authored on a target trail',
116
+ input: buildHappyExampleInput(),
117
+ name: 'List trail examples',
118
+ },
119
+ ],
120
+ implementation: async (input: RunExamplesTrailInput, ctx) => {
121
+ const target = await resolveRunTargetProject(input, input.id, {
122
+ cwd: ctx.cwd,
123
+ });
124
+ if (target.isErr()) {
125
+ return target;
126
+ }
127
+ return withFreshAppLease(
128
+ target.value.modulePath,
129
+ target.value.rootDir,
130
+ (lease) => {
131
+ const binding = assertConfiguredAppBinding(
132
+ target.value.context,
133
+ lease.app.name
134
+ );
135
+ if (binding.isErr()) {
136
+ return binding;
137
+ }
138
+ return buildExamplesListing(
139
+ lease.app,
140
+ input.id,
141
+ operatorProjectContextOutput(target.value.context)
142
+ );
143
+ }
144
+ );
145
+ },
146
+ input: runExamplesTrailInputSchema,
147
+ intent: 'read',
148
+ output: runExamplesListingSchema,
149
+ });
@@ -0,0 +1,496 @@
1
+ /**
2
+ * `run` trail -- Direct trail invocation by ID.
3
+ *
4
+ * Resolves a trail through the shared operator project context and executes it
5
+ * through the `run()` pipeline from `@ontrails/core`. The CLI surface drives
6
+ * this trail with `trails run <id> [--app <id>] [inline-json]`.
7
+ *
8
+ * Resolution order:
9
+ *
10
+ * 1. Resolve Config-owned project/app identity through the shared selector.
11
+ * 2. A selected app loads only that app for execution; an absent ID receives
12
+ * wrong-app or collision coaching from a complete live Config-owned view.
13
+ * Unavailable unrelated apps leave that coaching explicitly incomplete
14
+ * without replacing the selected app's result.
15
+ * 3. An unqualified workspace selection derives the complete ownership view
16
+ * live before choosing one owner. Missing apps fail closed and collisions
17
+ * return `AmbiguousError` for the CLI's optional TTY recovery.
18
+ * 4. `module` refines the selected app entry; it never replaces the selected
19
+ * app identity or collection boundary.
20
+ *
21
+ * The trail's output keeps a typed discriminator around the heterogeneous
22
+ * inner trail value. The value itself remains `unknown` because direct
23
+ * invocation can target any trail in the loaded app.
24
+ */
25
+
26
+ import { join } from 'node:path';
27
+
28
+ import {
29
+ AmbiguousError,
30
+ NotFoundError,
31
+ Result,
32
+ ValidationError,
33
+ run,
34
+ trail,
35
+ } from '@ontrails/core';
36
+ import { z } from 'zod';
37
+
38
+ import { withFreshAppLease } from './operator-context.js';
39
+ import {
40
+ assertConfiguredAppBinding,
41
+ resolveOperatorAppModuleContext,
42
+ resolveOperatorProjectContext,
43
+ } from './project-context.js';
44
+ import type {
45
+ OperatorAppProjectContext,
46
+ OperatorProjectApp,
47
+ OperatorProjectContext,
48
+ OperatorWorkspaceProjectContext,
49
+ } from './project-context.js';
50
+ import {
51
+ operatorProjectContextOutput,
52
+ operatorProjectContextOutputSchema,
53
+ } from './project-context-output.js';
54
+ import { createCurrentAppExampleInput } 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
+ executedAppId: z.string(),
60
+ kind: z.literal(INNER_TRAIL_RESULT_KIND),
61
+ project: operatorProjectContextOutputSchema,
62
+ trailId: z.string(),
63
+ value: z.unknown(),
64
+ });
65
+
66
+ export type InnerTrailResult = z.infer<typeof innerTrailResultSchema>;
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Resolution outcomes
70
+ // ---------------------------------------------------------------------------
71
+
72
+ const ambiguousMessage = (
73
+ trailId: string,
74
+ candidates: readonly string[]
75
+ ): string =>
76
+ `Trail ID '${trailId}' exists in apps: ${candidates.join(', ')}. Re-run with --app <id>.`;
77
+
78
+ const configuredApps = (
79
+ context: OperatorProjectContext
80
+ ): readonly OperatorProjectApp[] =>
81
+ context.selectedExtent === 'workspace'
82
+ ? context.apps
83
+ : context.identity.apps.map((app) => ({
84
+ configured: true,
85
+ id: app.id,
86
+ lockPath: join(app.rootDir, 'trails.lock'),
87
+ modulePath: app.entry,
88
+ moduleSource: app.entrySource === 'explicit' ? 'config' : 'convention',
89
+ root: app.root,
90
+ rootDir: app.rootDir,
91
+ }));
92
+
93
+ interface LiveOwnershipView {
94
+ readonly owners: readonly string[];
95
+ readonly unavailableAppIds: readonly string[];
96
+ }
97
+
98
+ const collectLiveOwners = async (
99
+ context: OperatorProjectContext,
100
+ trailId: string
101
+ ): Promise<Result<LiveOwnershipView, Error>> => {
102
+ const owners: string[] = [];
103
+ const unavailableAppIds: string[] = [];
104
+ for (const app of configuredApps(context)) {
105
+ if (app.id === undefined) {
106
+ return Result.err(
107
+ new ValidationError(
108
+ 'Configured ownership discovery requires a stable app ID.',
109
+ {
110
+ context: {
111
+ modulePath: app.modulePath,
112
+ reason: 'invalid-binding',
113
+ rootDir: app.rootDir,
114
+ },
115
+ }
116
+ )
117
+ );
118
+ }
119
+ const appId = app.id;
120
+ const loaded = await withFreshAppLease(
121
+ app.modulePath,
122
+ app.rootDir,
123
+ (lease) => {
124
+ if (lease.app.name !== appId) {
125
+ return Result.err(
126
+ new ValidationError(
127
+ `Configured app "${appId}" loaded topo "${lease.app.name}" while resolving trail ownership.`,
128
+ {
129
+ context: {
130
+ actualAppId: lease.app.name,
131
+ expectedAppId: appId,
132
+ modulePath: app.modulePath,
133
+ reason: 'invalid-binding',
134
+ },
135
+ }
136
+ )
137
+ );
138
+ }
139
+ if (lease.app.get(trailId) !== undefined) {
140
+ owners.push(appId);
141
+ }
142
+ return Result.ok();
143
+ }
144
+ );
145
+ if (loaded.isErr()) {
146
+ if (context.selectedExtent === 'workspace') {
147
+ return loaded;
148
+ }
149
+ unavailableAppIds.push(appId);
150
+ }
151
+ }
152
+ return Result.ok({
153
+ owners: owners.toSorted(),
154
+ unavailableAppIds: unavailableAppIds.toSorted(),
155
+ });
156
+ };
157
+
158
+ const ownerError = (
159
+ context: OperatorProjectContext,
160
+ trailId: string,
161
+ owners: readonly string[]
162
+ ): Error => {
163
+ const requestedApp =
164
+ context.selectedExtent === 'configured-app' ? context.app.id : undefined;
165
+ const [actualOwner] = owners;
166
+ if (
167
+ requestedApp !== undefined &&
168
+ owners.includes(requestedApp) &&
169
+ context.selectedExtent === 'configured-app'
170
+ ) {
171
+ return new NotFoundError(
172
+ `Trail '${trailId}' is not exposed by module '${context.app.modulePath}' selected for app '${requestedApp}', but the app's Config-owned default entry does expose it. Remove --module or select a module that exposes the trail.`,
173
+ {
174
+ context: {
175
+ modulePath: context.app.modulePath,
176
+ requestedApp,
177
+ trailId,
178
+ },
179
+ }
180
+ );
181
+ }
182
+ if (owners.length > 1) {
183
+ return new AmbiguousError(ambiguousMessage(trailId, owners), {
184
+ context: { candidates: owners, trailId },
185
+ });
186
+ }
187
+ if (
188
+ requestedApp !== undefined &&
189
+ actualOwner !== undefined &&
190
+ actualOwner !== requestedApp
191
+ ) {
192
+ return new NotFoundError(
193
+ `Trail '${trailId}' is owned by '${actualOwner}', not '${requestedApp}'.`,
194
+ { context: { actualOwner, requestedApp, trailId } }
195
+ );
196
+ }
197
+ const appContext =
198
+ requestedApp === undefined ? '' : ` for app '${requestedApp}'`;
199
+ return new NotFoundError(
200
+ `Trail '${trailId}' was not found${appContext} in any configured app under ${context.projectRoot}.`,
201
+ {
202
+ context: {
203
+ ...(requestedApp === undefined ? {} : { requestedApp }),
204
+ rootDir: context.projectRoot,
205
+ trailId,
206
+ },
207
+ }
208
+ );
209
+ };
210
+
211
+ const incompleteConfiguredOwnerError = (
212
+ context: OperatorAppProjectContext,
213
+ trailId: string,
214
+ ownership: LiveOwnershipView
215
+ ): Error => {
216
+ const requestedApp = context.app.id;
217
+ if (requestedApp !== undefined && ownership.owners.includes(requestedApp)) {
218
+ return ownerError(context, trailId, [requestedApp]);
219
+ }
220
+ return new NotFoundError(
221
+ `Trail '${trailId}' is not exposed by the selected app${requestedApp === undefined ? '' : ` '${requestedApp}'`}. Ownership coaching is incomplete because configured apps are unavailable: ${ownership.unavailableAppIds.join(', ')}.`,
222
+ {
223
+ context: {
224
+ ...(requestedApp === undefined ? {} : { requestedApp }),
225
+ rootDir: context.projectRoot,
226
+ trailId,
227
+ unavailableAppIds: ownership.unavailableAppIds,
228
+ },
229
+ }
230
+ );
231
+ };
232
+
233
+ const selectedOwnerContext = (
234
+ context: OperatorWorkspaceProjectContext,
235
+ appId: string
236
+ ): Result<OperatorAppProjectContext, ValidationError> => {
237
+ const app = context.apps.find((candidate) => candidate.id === appId);
238
+ return app === undefined
239
+ ? Result.err(
240
+ new ValidationError(
241
+ `Resolved trail owner "${appId}" is not present in configured project context.`,
242
+ { context: { appId, reason: 'invalid-binding' } }
243
+ )
244
+ )
245
+ : Result.ok({
246
+ app,
247
+ boundaryDir: context.boundaryDir,
248
+ identity: context.identity,
249
+ projectRoot: context.projectRoot,
250
+ selectedExtent: 'configured-app',
251
+ selectionProvenance: context.selectionProvenance,
252
+ });
253
+ };
254
+
255
+ const resolveRunContext = async (
256
+ context: OperatorProjectContext,
257
+ trailId: string
258
+ ): Promise<Result<OperatorAppProjectContext, Error>> => {
259
+ if (context.selectedExtent === 'standalone-app') {
260
+ return resolveOperatorAppModuleContext(context);
261
+ }
262
+ if (context.selectedExtent === 'configured-app') {
263
+ const owns = await withFreshAppLease(
264
+ context.app.modulePath,
265
+ context.app.rootDir,
266
+ (lease) => {
267
+ const binding = assertConfiguredAppBinding(context, lease.app.name);
268
+ return binding.isErr()
269
+ ? binding
270
+ : Result.ok(lease.app.get(trailId) !== undefined);
271
+ }
272
+ );
273
+ if (owns.isErr()) {
274
+ return owns;
275
+ }
276
+ if (owns.value) {
277
+ return Result.ok(context);
278
+ }
279
+ }
280
+ const owners = await collectLiveOwners(context, trailId);
281
+ if (owners.isErr()) {
282
+ return owners;
283
+ }
284
+ if (context.selectedExtent === 'configured-app') {
285
+ return Result.err(
286
+ owners.value.unavailableAppIds.length === 0
287
+ ? ownerError(context, trailId, owners.value.owners)
288
+ : incompleteConfiguredOwnerError(context, trailId, owners.value)
289
+ );
290
+ }
291
+ const [owner] = owners.value.owners;
292
+ if (owners.value.owners.length !== 1 || owner === undefined) {
293
+ return Result.err(ownerError(context, trailId, owners.value.owners));
294
+ }
295
+ return context.selectedExtent === 'workspace'
296
+ ? selectedOwnerContext(context, owner)
297
+ : Result.err(
298
+ new ValidationError('Unable to resolve workspace run context.', {
299
+ context: { reason: 'invalid-binding' },
300
+ })
301
+ );
302
+ };
303
+
304
+ interface RunTargetInput {
305
+ readonly app?: string | undefined;
306
+ readonly module?: string | undefined;
307
+ readonly rootDir?: string | undefined;
308
+ }
309
+
310
+ interface RunTargetRuntime {
311
+ readonly cwd?: string | undefined;
312
+ }
313
+
314
+ export interface RunTargetProject {
315
+ readonly context: OperatorAppProjectContext;
316
+ readonly modulePath: string;
317
+ readonly rootDir: string;
318
+ }
319
+
320
+ /** Resolve one run target without collapsing inferred CWD into explicit root selection. */
321
+ export const resolveRunTargetProject = async (
322
+ input: RunTargetInput,
323
+ trailId: string,
324
+ runtime: RunTargetRuntime
325
+ ): Promise<Result<RunTargetProject, Error>> => {
326
+ const context = await resolveOperatorProjectContext(input, runtime);
327
+ if (context.isErr()) {
328
+ return context;
329
+ }
330
+ const selected = await resolveRunContext(context.value, trailId);
331
+ if (selected.isErr()) {
332
+ return selected;
333
+ }
334
+ return Result.ok({
335
+ context: selected.value,
336
+ modulePath: selected.value.app.modulePath,
337
+ rootDir: selected.value.app.rootDir,
338
+ });
339
+ };
340
+
341
+ // ---------------------------------------------------------------------------
342
+ // Example input helpers
343
+ // ---------------------------------------------------------------------------
344
+
345
+ const buildHappyExampleInput = (): {
346
+ readonly input: { readonly module: string; readonly rootDir: string };
347
+ readonly id: string;
348
+ readonly module: string;
349
+ readonly rootDir: string;
350
+ } => {
351
+ const isolated = createCurrentAppExampleInput();
352
+ const configured = createCurrentAppExampleInput({
353
+ selection: 'configured-project',
354
+ });
355
+ return {
356
+ id: 'survey.brief',
357
+ input: isolated,
358
+ ...configured,
359
+ };
360
+ };
361
+
362
+ const buildNotFoundExampleInput = (): {
363
+ readonly id: string;
364
+ readonly module: string;
365
+ readonly rootDir: string;
366
+ } => ({
367
+ ...createCurrentAppExampleInput({ selection: 'configured-project' }),
368
+ id: 'does.not.exist',
369
+ });
370
+
371
+ const runTrailInputSchema = z
372
+ .object({
373
+ app: z
374
+ .string()
375
+ .optional()
376
+ .describe(
377
+ 'Workspace app to resolve the trail ID against; required when the ID is exposed by more than one app'
378
+ ),
379
+ id: z.string().describe('Trail ID to invoke'),
380
+ input: z
381
+ .unknown()
382
+ .optional()
383
+ .describe(
384
+ 'Parsed input for the resolved trail; the CLI surface JSON.parses the inline argument before passing it through'
385
+ ),
386
+ module: z.string().optional().describe('Path to the app module'),
387
+ rootDir: z.string().optional().describe('Workspace root directory'),
388
+ })
389
+ .catchall(z.unknown());
390
+
391
+ type RunTrailInput = z.output<typeof runTrailInputSchema>;
392
+
393
+ const RUN_TRAIL_CONTROL_KEYS = new Set([
394
+ 'app',
395
+ 'id',
396
+ 'input',
397
+ 'module',
398
+ 'rootDir',
399
+ ]);
400
+
401
+ const directInputEntries = (
402
+ input: RunTrailInput
403
+ ): readonly (readonly [string, unknown])[] =>
404
+ Object.entries(input).filter(([key]) => !RUN_TRAIL_CONTROL_KEYS.has(key));
405
+
406
+ const resolveInnerTrailInput = (
407
+ input: RunTrailInput
408
+ ): Result<unknown, ValidationError> => {
409
+ const entries = directInputEntries(input);
410
+ if (entries.length === 0) {
411
+ return Result.ok(input.input);
412
+ }
413
+ if (Object.hasOwn(input, 'input')) {
414
+ return Result.err(
415
+ new ValidationError(
416
+ 'trails run received both direct input fields and an explicit input wrapper. Use one shape: either {"name":"Alpha"} or {"input":{"name":"Alpha"}}.'
417
+ )
418
+ );
419
+ }
420
+ return Result.ok(Object.fromEntries(entries));
421
+ };
422
+
423
+ // ---------------------------------------------------------------------------
424
+ // Trail definition
425
+ // ---------------------------------------------------------------------------
426
+
427
+ export const runTrail = trail('run', {
428
+ args: ['id'],
429
+ description:
430
+ 'Resolve a trail by ID in the current app and execute it through the shared pipeline',
431
+ examples: [
432
+ {
433
+ description:
434
+ 'Resolve and execute a trail by ID, returning the inner trail Result value',
435
+ input: buildHappyExampleInput(),
436
+ name: 'Run trail by ID',
437
+ },
438
+ {
439
+ description: 'Reject an unknown trail ID with NotFoundError',
440
+ error: 'NotFoundError',
441
+ input: buildNotFoundExampleInput(),
442
+ name: 'Reject unknown trail ID',
443
+ },
444
+ ],
445
+ implementation: async (
446
+ input: RunTrailInput,
447
+ ctx
448
+ ): Promise<Result<InnerTrailResult, Error>> => {
449
+ const contextResult = await resolveOperatorProjectContext(input, {
450
+ cwd: ctx.cwd,
451
+ });
452
+ if (contextResult.isErr()) {
453
+ return contextResult;
454
+ }
455
+ const context = contextResult.value;
456
+ const resolved = await resolveRunContext(context, input.id);
457
+ if (resolved.isErr()) {
458
+ return resolved;
459
+ }
460
+ const selected = resolved.value;
461
+ const innerInput = resolveInnerTrailInput(input);
462
+ if (innerInput.isErr()) {
463
+ return innerInput;
464
+ }
465
+ return withFreshAppLease(
466
+ selected.app.modulePath,
467
+ selected.app.rootDir,
468
+ async (lease) => {
469
+ const binding = assertConfiguredAppBinding(selected, lease.app.name);
470
+ if (binding.isErr()) {
471
+ return binding;
472
+ }
473
+ const result = await run(lease.app, input.id, innerInput.value, {
474
+ ctx: {
475
+ cwd: selected.app.rootDir,
476
+ ...(ctx.permit === undefined ? {} : { permit: ctx.permit }),
477
+ },
478
+ });
479
+ if (result.isErr()) {
480
+ return result;
481
+ }
482
+ return Result.ok({
483
+ executedAppId: lease.app.name,
484
+ kind: INNER_TRAIL_RESULT_KIND,
485
+ project: operatorProjectContextOutput(selected),
486
+ trailId: input.id,
487
+ value: result.value,
488
+ });
489
+ }
490
+ );
491
+ },
492
+ input: runTrailInputSchema,
493
+ intent: 'write',
494
+ output: innerTrailResultSchema,
495
+ permit: { scopes: ['trails:run'] },
496
+ });
@@ -0,0 +1,60 @@
1
+ const packageKeyOrder = [
2
+ 'name',
3
+ 'version',
4
+ 'private',
5
+ 'bin',
6
+ 'type',
7
+ 'workspaces',
8
+ 'scripts',
9
+ 'dependencies',
10
+ 'devDependencies',
11
+ ] as const;
12
+
13
+ const packageMapKeys = new Set<string>([
14
+ 'bin',
15
+ 'dependencies',
16
+ 'devDependencies',
17
+ 'scripts',
18
+ ]);
19
+
20
+ export type ScaffoldPackageJson = Record<string, unknown>;
21
+
22
+ const isPlainRecord = (value: unknown): value is Record<string, unknown> =>
23
+ typeof value === 'object' && value !== null && !Array.isArray(value);
24
+
25
+ const sortRecord = (record: Record<string, unknown>): Record<string, unknown> =>
26
+ Object.fromEntries(
27
+ Object.entries(record).toSorted(([left], [right]) =>
28
+ left.localeCompare(right)
29
+ )
30
+ );
31
+
32
+ const normalizePackageValue = (key: string, value: unknown): unknown =>
33
+ packageMapKeys.has(key) && isPlainRecord(value) ? sortRecord(value) : value;
34
+
35
+ export const normalizeScaffoldPackageJson = (
36
+ pkg: ScaffoldPackageJson
37
+ ): ScaffoldPackageJson => {
38
+ const normalized: ScaffoldPackageJson = {};
39
+
40
+ for (const key of packageKeyOrder) {
41
+ if (pkg[key] !== undefined) {
42
+ normalized[key] = normalizePackageValue(key, pkg[key]);
43
+ }
44
+ }
45
+
46
+ for (const key of Object.keys(pkg).toSorted()) {
47
+ if (!(key in normalized)) {
48
+ normalized[key] = normalizePackageValue(key, pkg[key]);
49
+ }
50
+ }
51
+
52
+ return normalized;
53
+ };
54
+
55
+ export const stringifyScaffoldJson = (value: unknown): string =>
56
+ `${JSON.stringify(value, null, 2)}\n`;
57
+
58
+ export const stringifyScaffoldPackageJson = (
59
+ pkg: ScaffoldPackageJson
60
+ ): string => stringifyScaffoldJson(normalizeScaffoldPackageJson(pkg));