@ontrails/trails 1.0.0-beta.0 → 1.0.0-beta.10

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 (42) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +141 -0
  3. package/bin/trails.ts +0 -0
  4. package/dist/src/clack.d.ts.map +1 -1
  5. package/dist/src/clack.js +22 -0
  6. package/dist/src/clack.js.map +1 -1
  7. package/dist/src/trails/add-surface.js +9 -9
  8. package/dist/src/trails/add-surface.js.map +1 -1
  9. package/dist/src/trails/add-trail.d.ts +1 -2
  10. package/dist/src/trails/add-trail.d.ts.map +1 -1
  11. package/dist/src/trails/add-trail.js +16 -24
  12. package/dist/src/trails/add-trail.js.map +1 -1
  13. package/dist/src/trails/add-verify.js +9 -9
  14. package/dist/src/trails/add-verify.js.map +1 -1
  15. package/dist/src/trails/create-scaffold.js +25 -25
  16. package/dist/src/trails/create-scaffold.js.map +1 -1
  17. package/dist/src/trails/create.d.ts +1 -1
  18. package/dist/src/trails/create.js +10 -10
  19. package/dist/src/trails/create.js.map +1 -1
  20. package/dist/src/trails/guide.js +12 -12
  21. package/dist/src/trails/guide.js.map +1 -1
  22. package/dist/src/trails/survey.d.ts +1 -2
  23. package/dist/src/trails/survey.d.ts.map +1 -1
  24. package/dist/src/trails/survey.js +59 -26
  25. package/dist/src/trails/survey.js.map +1 -1
  26. package/dist/src/trails/warden.d.ts +1 -1
  27. package/dist/src/trails/warden.js +28 -28
  28. package/dist/src/trails/warden.js.map +1 -1
  29. package/package.json +9 -9
  30. package/src/__tests__/create.test.ts +7 -7
  31. package/src/__tests__/guide.test.ts +4 -4
  32. package/src/__tests__/survey.test.ts +6 -8
  33. package/src/__tests__/warden.test.ts +2 -2
  34. package/src/clack.ts +22 -0
  35. package/src/trails/add-surface.ts +9 -9
  36. package/src/trails/add-trail.ts +16 -25
  37. package/src/trails/add-verify.ts +9 -9
  38. package/src/trails/create-scaffold.ts +27 -27
  39. package/src/trails/create.ts +10 -10
  40. package/src/trails/guide.ts +14 -14
  41. package/src/trails/survey.ts +88 -36
  42. package/src/trails/warden.ts +33 -33
@@ -92,7 +92,15 @@ const writeSurfaceEntry = async (
92
92
 
93
93
  export const addSurface = trail('add.surface', {
94
94
  description: 'Add a surface to an existing project',
95
- implementation: async (input) => {
95
+ input: z.object({
96
+ dir: z.string().optional().describe('Project directory'),
97
+ surface: z.enum(['cli', 'mcp']).describe('Surface to add'),
98
+ }),
99
+ output: z.object({
100
+ created: z.string(),
101
+ dependency: z.string(),
102
+ }),
103
+ run: async (input) => {
96
104
  const cwd = resolve(input.dir ?? '.');
97
105
  const { surface } = input;
98
106
  const entryFile = getEntryFile(surface);
@@ -108,12 +116,4 @@ export const addSurface = trail('add.surface', {
108
116
  dependency: await updatePkgJsonForSurface(cwd, surface),
109
117
  });
110
118
  },
111
- input: z.object({
112
- dir: z.string().optional().describe('Project directory'),
113
- surface: z.enum(['cli', 'mcp']).describe('Surface to add'),
114
- }),
115
- output: z.object({
116
- created: z.string(),
117
- dependency: z.string(),
118
- }),
119
119
  });
@@ -14,17 +14,9 @@ import { z } from 'zod';
14
14
 
15
15
  const generateTrailFile = (
16
16
  id: string,
17
- readOnly: boolean,
18
- destructive: boolean
17
+ intent: 'read' | 'write' | 'destroy'
19
18
  ): string => {
20
- const markers: string[] = [];
21
- if (readOnly) {
22
- markers.push(' readOnly: true,');
23
- }
24
- if (destructive) {
25
- markers.push(' destructive: true,');
26
- }
27
- const markerBlock = markers.length > 0 ? `\n${markers.join('\n')}` : '';
19
+ const intentLine = intent === 'write' ? '' : `\n intent: '${intent}',`;
28
20
 
29
21
  return `import { Result, trail } from '@ontrails/core';
30
22
  import { z } from 'zod';
@@ -37,10 +29,10 @@ export const ${id.replaceAll('.', '_')} = trail('${id}', {
37
29
  name: 'TODO: add example',
38
30
  },
39
31
  ],
40
- implementation: async (input) => {
32
+ run: async (input) => {
41
33
  return Result.ok({ message: 'TODO' });
42
34
  },
43
- input: z.object({}),${markerBlock}
35
+ input: z.object({}),${intentLine}
44
36
  output: z.object({ message: z.string() }),
45
37
  });
46
38
  `;
@@ -73,16 +65,23 @@ const writeWithDirs = async (
73
65
 
74
66
  export const addTrail = trail('add.trail', {
75
67
  description: 'Scaffold a new trail with tests and examples',
76
- implementation: async (input, ctx) => {
68
+ input: z.object({
69
+ id: z.string().describe('Trail ID (e.g., entity.update)'),
70
+ intent: z
71
+ .enum(['read', 'write', 'destroy'])
72
+ .default('write')
73
+ .describe('Trail intent'),
74
+ }),
75
+ output: z.object({
76
+ created: z.array(z.string()),
77
+ }),
78
+ run: async (input, ctx) => {
77
79
  const { id } = input;
78
80
  const moduleName = id.replaceAll('.', '-');
79
81
  const cwd = resolve(ctx.cwd ?? '.');
80
82
 
81
83
  const files = new Map<string, string>([
82
- [
83
- `src/trails/${moduleName}.ts`,
84
- generateTrailFile(id, input.readOnly, input.destructive),
85
- ],
84
+ [`src/trails/${moduleName}.ts`, generateTrailFile(id, input.intent)],
86
85
  [`__tests__/${moduleName}.test.ts`, generateTestFile(id)],
87
86
  ]);
88
87
 
@@ -92,12 +91,4 @@ export const addTrail = trail('add.trail', {
92
91
 
93
92
  return Result.ok({ created: [...files.keys()] });
94
93
  },
95
- input: z.object({
96
- destructive: z.boolean().default(false).describe('Destructive trail'),
97
- id: z.string().describe('Trail ID (e.g., entity.update)'),
98
- readOnly: z.boolean().default(false).describe('Read-only trail'),
99
- }),
100
- output: z.object({
101
- created: z.array(z.string()),
102
- }),
103
94
  });
@@ -56,7 +56,15 @@ const updatePackageJsonForVerify = async (
56
56
 
57
57
  export const addVerify = trail('add.verify', {
58
58
  description: 'Add testing and warden verification',
59
- implementation: async (input) => {
59
+ input: z.object({
60
+ dir: z.string().optional().describe('Parent directory'),
61
+ name: z.string().describe('Project name'),
62
+ }),
63
+ metadata: { internal: true },
64
+ output: z.object({
65
+ created: z.array(z.string()),
66
+ }),
67
+ run: async (input) => {
60
68
  const projectDir = resolve(input.dir ?? '.', input.name);
61
69
  const files: string[] = [];
62
70
 
@@ -76,12 +84,4 @@ export const addVerify = trail('add.verify', {
76
84
 
77
85
  return Result.ok({ created: files });
78
86
  },
79
- input: z.object({
80
- dir: z.string().optional().describe('Parent directory'),
81
- name: z.string().describe('Project name'),
82
- }),
83
- markers: { internal: true },
84
- output: z.object({
85
- created: z.array(z.string()),
86
- }),
87
87
  });
@@ -107,7 +107,7 @@ export const hello = trail('hello', {
107
107
  name: 'Named greeting',
108
108
  },
109
109
  ],
110
- implementation: (input) => {
110
+ run: (input) => {
111
111
  const name = input.name ?? 'world';
112
112
  return Result.ok({ message: \`Hello, \${name}!\` });
113
113
  },
@@ -117,7 +117,7 @@ export const hello = trail('hello', {
117
117
  output: z.object({
118
118
  message: z.string(),
119
119
  }),
120
- readOnly: true,
120
+ intent: 'read',
121
121
  });
122
122
  `;
123
123
 
@@ -139,12 +139,12 @@ export const show = trail('entity.show', {
139
139
  name: 'Show entity',
140
140
  },
141
141
  ],
142
- implementation: (input) => {
142
+ run: (input) => {
143
143
  return Result.ok({ id: input.id, name: 'Example' });
144
144
  },
145
145
  input: z.object({ id: z.string() }),
146
146
  output: entitySchema,
147
- readOnly: true,
147
+ intent: 'read',
148
148
  });
149
149
 
150
150
  export const add = trail('entity.add', {
@@ -156,7 +156,7 @@ export const add = trail('entity.add', {
156
156
  name: 'Add entity',
157
157
  },
158
158
  ],
159
- implementation: (input) => {
159
+ run: (input) => {
160
160
  return Result.ok({ id: '1', name: input.name });
161
161
  },
162
162
  input: z.object({ name: z.string() }),
@@ -177,25 +177,25 @@ export const search = trail('search', {
177
177
  name: 'Search entities',
178
178
  },
179
179
  ],
180
- implementation: () => {
180
+ run: () => {
181
181
  return Result.ok({ results: [] });
182
182
  },
183
183
  input: z.object({ query: z.string() }),
184
184
  output: z.object({
185
185
  results: z.array(z.object({ id: z.string(), name: z.string() })),
186
186
  }),
187
- readOnly: true,
187
+ intent: 'read',
188
188
  });
189
189
  `;
190
190
 
191
- const generateOnboardHike = (): string =>
192
- `import { Result, hike } from '@ontrails/core';
191
+ const generateOnboardTrail = (): string =>
192
+ `import { Result, trail } from '@ontrails/core';
193
193
  import { z } from 'zod';
194
194
 
195
- export const onboard = hike('entity.onboard', {
195
+ export const onboard = trail('entity.onboard', {
196
196
  description: 'Onboard a new entity end-to-end',
197
- follows: ['entity.add'],
198
- implementation: async (input, ctx) => {
197
+ follow: ['entity.add'],
198
+ run: async (input, ctx) => {
199
199
  const result = await ctx.follow('entity.add', { name: input.name });
200
200
  if (result.isErr()) {
201
201
  return result;
@@ -281,7 +281,7 @@ const starterFileGenerators: Record<Starter, () => [string, string][]> = {
281
281
  entity: () => [
282
282
  ['src/trails/entity.ts', generateEntityTrails()],
283
283
  ['src/trails/search.ts', generateSearchTrail()],
284
- ['src/trails/onboard.ts', generateOnboardHike()],
284
+ ['src/trails/onboard.ts', generateOnboardTrail()],
285
285
  ['src/events/entity-events.ts', generateEntityEvents()],
286
286
  ['src/store.ts', generateStore()],
287
287
  ],
@@ -322,19 +322,6 @@ const writeScaffoldFiles = async (
322
322
 
323
323
  export const createScaffold = trail('create.scaffold', {
324
324
  description: 'Scaffold a new Trails project',
325
- implementation: async (input) => {
326
- const projectDir = resolve(input.dir ?? '.', input.name);
327
- const starter = (input.starter ?? 'hello') as Starter;
328
- const fileMap = collectScaffoldFiles(input.name, starter);
329
- const files = await writeScaffoldFiles(projectDir, fileMap);
330
- mkdirSync(join(projectDir, '.trails'), { recursive: true });
331
-
332
- return Result.ok({
333
- created: files,
334
- dir: projectDir,
335
- name: input.name,
336
- } satisfies ScaffoldResult);
337
- },
338
325
  input: z.object({
339
326
  dir: z.string().optional().describe('Parent directory'),
340
327
  name: z.string().describe('Project name'),
@@ -343,10 +330,23 @@ export const createScaffold = trail('create.scaffold', {
343
330
  .default('hello')
344
331
  .describe('Starter trail'),
345
332
  }),
346
- markers: { internal: true },
333
+ metadata: { internal: true },
347
334
  output: z.object({
348
335
  created: z.array(z.string()),
349
336
  dir: z.string(),
350
337
  name: z.string(),
351
338
  }),
339
+ run: async (input) => {
340
+ const projectDir = resolve(input.dir ?? '.', input.name);
341
+ const starter = (input.starter ?? 'hello') as Starter;
342
+ const fileMap = collectScaffoldFiles(input.name, starter);
343
+ const files = await writeScaffoldFiles(projectDir, fileMap);
344
+ mkdirSync(join(projectDir, '.trails'), { recursive: true });
345
+
346
+ return Result.ok({
347
+ created: files,
348
+ dir: projectDir,
349
+ name: input.name,
350
+ } satisfies ScaffoldResult);
351
+ },
352
352
  });
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { FollowFn } from '@ontrails/core';
9
- import { Result, hike } from '@ontrails/core';
9
+ import { Result, trail } from '@ontrails/core';
10
10
  import { z } from 'zod';
11
11
 
12
12
  // ---------------------------------------------------------------------------
@@ -146,7 +146,7 @@ const runCreate = async (
146
146
  // Route definition
147
147
  // ---------------------------------------------------------------------------
148
148
 
149
- export const createRoute = hike('create', {
149
+ export const createRoute = trail('create', {
150
150
  description: 'Create a new Trails project',
151
151
  fields: {
152
152
  starter: {
@@ -157,7 +157,7 @@ export const createRoute = hike('create', {
157
157
  value: 'hello',
158
158
  },
159
159
  {
160
- hint: '4 trails, hike, event, store',
160
+ hint: '4 trails, event, store',
161
161
  label: 'Entity CRUD',
162
162
  value: 'entity',
163
163
  },
@@ -175,13 +175,7 @@ export const createRoute = hike('create', {
175
175
  ],
176
176
  },
177
177
  },
178
- follows: ['create.scaffold', 'add.surface', 'add.verify'],
179
- implementation: async (input: BlazeInput, ctx) => {
180
- if (!ctx.follow) {
181
- return Result.err(new Error('create route requires ctx.follow'));
182
- }
183
- return await runCreate(ctx.follow, input);
184
- },
178
+ follow: ['create.scaffold', 'add.surface', 'add.verify'],
185
179
  input: z.object({
186
180
  dir: z.string().optional().describe('Parent directory'),
187
181
  name: z.string().describe('Project name'),
@@ -200,4 +194,10 @@ export const createRoute = hike('create', {
200
194
  dir: z.string(),
201
195
  name: z.string(),
202
196
  }),
197
+ run: async (input: BlazeInput, ctx) => {
198
+ if (!ctx.follow) {
199
+ return Result.err(new Error('create route requires ctx.follow'));
200
+ }
201
+ return await runCreate(ctx.follow, input);
202
+ },
203
203
  });
@@ -63,19 +63,6 @@ export const guideTrail = trail('guide', {
63
63
  name: 'List trail guidance',
64
64
  },
65
65
  ],
66
- implementation: async (input, ctx) => {
67
- const app = await loadApp(input.module, ctx.cwd ?? '.');
68
-
69
- if (input.trailId) {
70
- const item = app.get(input.trailId);
71
- if (!item) {
72
- return Result.err(new Error(`Trail not found: ${input.trailId}`));
73
- }
74
- return Result.ok(toGuideDetail(item as Trail<unknown, unknown>));
75
- }
76
-
77
- return Result.ok(toGuideEntries(app));
78
- },
79
66
  input: z.object({
80
67
  module: z
81
68
  .string()
@@ -83,6 +70,7 @@ export const guideTrail = trail('guide', {
83
70
  .describe('Path to the app module'),
84
71
  trailId: z.string().optional().describe('Trail ID for detailed guidance'),
85
72
  }),
73
+ intent: 'read',
86
74
  output: z.union([
87
75
  z.array(
88
76
  z.object({
@@ -100,5 +88,17 @@ export const guideTrail = trail('guide', {
100
88
  kind: z.string(),
101
89
  }),
102
90
  ]),
103
- readOnly: true,
91
+ run: async (input, ctx) => {
92
+ const app = await loadApp(input.module, ctx.cwd ?? '.');
93
+
94
+ if (input.trailId) {
95
+ const item = app.get(input.trailId);
96
+ if (!item) {
97
+ return Result.err(new Error(`Trail not found: ${input.trailId}`));
98
+ }
99
+ return Result.ok(toGuideDetail(item as Trail<unknown, unknown>));
100
+ }
101
+
102
+ return Result.ok(toGuideEntries(app));
103
+ },
104
104
  });
@@ -10,6 +10,7 @@ import { Result, trail } from '@ontrails/core';
10
10
  import type { DiffResult } from '@ontrails/schema';
11
11
  import {
12
12
  diffSurfaceMaps,
13
+ generateOpenApiSpec,
13
14
  generateSurfaceMap,
14
15
  hashSurfaceMap,
15
16
  readSurfaceMap,
@@ -32,11 +33,9 @@ export interface BriefReport {
32
33
  readonly outputSchemas: boolean;
33
34
  readonly examples: boolean;
34
35
  readonly detours: boolean;
35
- readonly hikes: boolean;
36
36
  readonly events: boolean;
37
37
  };
38
38
  readonly trails: number;
39
- readonly hikes: number;
40
39
  readonly events: number;
41
40
  }
42
41
 
@@ -73,10 +72,8 @@ export const generateBriefReport = (app: Topo): BriefReport => {
73
72
  detours: hasDetours,
74
73
  events: app.events.size > 0,
75
74
  examples: hasExamples,
76
- hikes: app.hikes.size > 0,
77
75
  outputSchemas: hasOutputSchemas,
78
76
  },
79
- hikes: app.hikes.size,
80
77
  name: app.name,
81
78
  trails: app.trails.size,
82
79
  version: '0.1.0',
@@ -88,14 +85,13 @@ export const generateBriefReport = (app: Topo): BriefReport => {
88
85
  // ---------------------------------------------------------------------------
89
86
 
90
87
  const safetyLabel = (entry: {
91
- readOnly?: boolean;
92
- destructive?: boolean;
88
+ intent?: 'read' | 'write' | 'destroy';
93
89
  }): string => {
94
- if (entry.destructive) {
95
- return 'destructive';
90
+ if (entry.intent === 'destroy') {
91
+ return 'destroy';
96
92
  }
97
- if (entry.readOnly) {
98
- return 'readOnly';
93
+ if (entry.intent === 'read') {
94
+ return 'read';
99
95
  }
100
96
  return '-';
101
97
  };
@@ -104,7 +100,7 @@ const formatTrailList = (app: Topo): object => {
104
100
  const items = app.list();
105
101
  const entries = items.map((item) => {
106
102
  const safety = safetyLabel(
107
- item as unknown as { readOnly?: boolean; destructive?: boolean }
103
+ item as unknown as { intent?: 'read' | 'write' | 'destroy' }
108
104
  );
109
105
  const examples = Array.isArray(
110
106
  (item as unknown as { examples?: unknown[] }).examples
@@ -132,7 +128,7 @@ const formatTrailList = (app: Topo): object => {
132
128
  */
133
129
  const formatTrailDetail = (item: Trail<unknown, unknown>): object => {
134
130
  const safety = safetyLabel(
135
- item as unknown as { readOnly?: boolean; destructive?: boolean }
131
+ item as unknown as { intent?: 'read' | 'write' | 'destroy' }
136
132
  );
137
133
 
138
134
  return {
@@ -200,6 +196,55 @@ const buildSurveyGenerate = async (
200
196
  return Result.ok({ hash, lockPath, mapPath });
201
197
  };
202
198
 
199
+ interface SurveyInput {
200
+ breakingOnly: boolean;
201
+ brief: boolean;
202
+ diff?: string | undefined;
203
+ generate: boolean;
204
+ openapi: boolean;
205
+ trailId?: string | undefined;
206
+ }
207
+
208
+ type SurveyMode = 'brief' | 'detail' | 'diff' | 'generate' | 'list' | 'openapi';
209
+
210
+ /** Ordered mode checks — first truthy predicate wins, otherwise 'list'. */
211
+ const modeChecks: readonly [(input: SurveyInput) => boolean, SurveyMode][] = [
212
+ [(i) => i.brief, 'brief'],
213
+ [(i) => Boolean(i.diff), 'diff'],
214
+ [(i) => Boolean(i.trailId), 'detail'],
215
+ [(i) => i.generate, 'generate'],
216
+ [(i) => i.openapi, 'openapi'],
217
+ ];
218
+
219
+ /** Determine which survey mode was requested, falling back to 'list'. */
220
+ const resolveSurveyMode = (input: SurveyInput): SurveyMode =>
221
+ modeChecks.find(([predicate]) => predicate(input))?.[1] ?? 'list';
222
+
223
+ type SurveyHandler = (
224
+ app: Topo,
225
+ input: SurveyInput
226
+ ) => Result<object, Error> | Promise<Result<object, Error>>;
227
+
228
+ /** Handlers keyed by survey mode. */
229
+ const surveyHandlers: Record<SurveyMode, SurveyHandler> = {
230
+ brief: (app) => Result.ok(generateBriefReport(app)),
231
+ detail: (app, input) => buildSurveyDetail(app, input.trailId ?? ''),
232
+ diff: (app, input) => buildSurveyDiff(app, input.breakingOnly),
233
+ generate: (app) => buildSurveyGenerate(app),
234
+ list: (app) => Result.ok(formatTrailList(app)),
235
+ openapi: (app) => Result.ok(generateOpenApiSpec(app)),
236
+ };
237
+
238
+ /** Dispatch to the appropriate survey sub-command based on input flags. */
239
+ const dispatchSurvey = (
240
+ app: Topo,
241
+ input: SurveyInput
242
+ ): Result<object, Error> | Promise<Result<object, Error>> => {
243
+ const mode = resolveSurveyMode(input);
244
+ const handler = surveyHandlers[mode];
245
+ return handler(app, input);
246
+ };
247
+
203
248
  // ---------------------------------------------------------------------------
204
249
  // Trail definition
205
250
  // ---------------------------------------------------------------------------
@@ -217,28 +262,12 @@ export const surveyTrail = trail('survey', {
217
262
  input: { brief: true },
218
263
  name: 'Brief capability report',
219
264
  },
265
+ {
266
+ description: 'Generate an OpenAPI 3.1 specification for the topo',
267
+ input: { openapi: true },
268
+ name: 'OpenAPI spec',
269
+ },
220
270
  ],
221
- implementation: async (input, ctx) => {
222
- const app = await loadApp(input.module, ctx.cwd ?? '.');
223
-
224
- if (input.brief) {
225
- return Result.ok(generateBriefReport(app));
226
- }
227
-
228
- if (input.diff) {
229
- return await buildSurveyDiff(app, input.breakingOnly);
230
- }
231
-
232
- if (input.trailId) {
233
- return buildSurveyDetail(app, input.trailId);
234
- }
235
-
236
- if (input.generate) {
237
- return await buildSurveyGenerate(app);
238
- }
239
-
240
- return Result.ok(formatTrailList(app));
241
- },
242
271
  input: z.object({
243
272
  breakingOnly: z
244
273
  .boolean()
@@ -254,8 +283,10 @@ export const surveyTrail = trail('survey', {
254
283
  .string()
255
284
  .default('./src/app.ts')
256
285
  .describe('Path to the app module'),
286
+ openapi: z.boolean().default(false).describe('Output OpenAPI 3.1 spec'),
257
287
  trailId: z.string().optional().describe('Trail ID for detail view'),
258
288
  }),
289
+ intent: 'read',
259
290
  output: z.union([
260
291
  z.object({
261
292
  count: z.number(),
@@ -275,10 +306,8 @@ export const surveyTrail = trail('survey', {
275
306
  detours: z.boolean(),
276
307
  events: z.boolean(),
277
308
  examples: z.boolean(),
278
- hikes: z.boolean(),
279
309
  outputSchemas: z.boolean(),
280
310
  }),
281
- hikes: z.number(),
282
311
  name: z.string(),
283
312
  trails: z.number(),
284
313
  version: z.string(),
@@ -302,6 +331,29 @@ export const surveyTrail = trail('survey', {
302
331
  lockPath: z.string(),
303
332
  mapPath: z.string(),
304
333
  }),
334
+ z.object({
335
+ components: z.object({
336
+ schemas: z.record(z.string(), z.unknown()),
337
+ }),
338
+ info: z.object({
339
+ description: z.string().optional(),
340
+ title: z.string(),
341
+ version: z.string(),
342
+ }),
343
+ openapi: z.literal('3.1.0'),
344
+ paths: z.record(z.string(), z.record(z.string(), z.unknown())),
345
+ servers: z
346
+ .array(
347
+ z.object({
348
+ description: z.string().optional(),
349
+ url: z.string(),
350
+ })
351
+ )
352
+ .optional(),
353
+ }),
305
354
  ]),
306
- readOnly: true,
355
+ run: async (input, ctx) => {
356
+ const app = await loadApp(input.module, ctx.cwd ?? '.');
357
+ return dispatchSurvey(app, input);
358
+ },
307
359
  });
@@ -37,38 +37,6 @@ export const wardenTrail = trail('warden', {
37
37
  name: 'GitHub Actions annotations',
38
38
  },
39
39
  ],
40
- implementation: async (input, ctx) => {
41
- const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
42
- // oxlint-disable-next-line prefer-await-to-then -- catch converts rejection to undefined cleanly
43
- const topo = await loadApp('./src/app.ts', rootDir).catch(
44
- (): undefined => undefined
45
- );
46
-
47
- const report = await runWarden({
48
- driftOnly: input.driftOnly,
49
- lintOnly: input.lintOnly,
50
- rootDir,
51
- topo,
52
- });
53
-
54
- const formatters: Record<string, (r: typeof report) => string> = {
55
- github: formatGitHubAnnotations,
56
- json: formatJson,
57
- summary: formatSummary,
58
- text: formatWardenReport,
59
- };
60
- const formatter = formatters[input.format] ?? formatWardenReport;
61
- const formatted = formatter(report);
62
-
63
- return Result.ok({
64
- diagnostics: report.diagnostics,
65
- drift: report.drift,
66
- errorCount: report.errorCount,
67
- formatted,
68
- passed: report.passed,
69
- warnCount: report.warnCount,
70
- });
71
- },
72
40
  input: z.object({
73
41
  driftOnly: z.boolean().default(false).describe('Only run drift detection'),
74
42
  format: z
@@ -78,6 +46,7 @@ export const wardenTrail = trail('warden', {
78
46
  lintOnly: z.boolean().default(false).describe('Only run lint rules'),
79
47
  rootDir: z.string().optional().describe('Root directory to scan'),
80
48
  }),
49
+ intent: 'read',
81
50
  output: z.object({
82
51
  diagnostics: z.array(
83
52
  z.object({
@@ -100,5 +69,36 @@ export const wardenTrail = trail('warden', {
100
69
  passed: z.boolean(),
101
70
  warnCount: z.number(),
102
71
  }),
103
- readOnly: true,
72
+ run: async (input, ctx) => {
73
+ const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
74
+ // oxlint-disable-next-line prefer-await-to-then -- catch converts rejection to undefined cleanly
75
+ const topo = await loadApp('./src/app.ts', rootDir).catch(
76
+ (): undefined => undefined
77
+ );
78
+
79
+ const report = await runWarden({
80
+ driftOnly: input.driftOnly,
81
+ lintOnly: input.lintOnly,
82
+ rootDir,
83
+ topo,
84
+ });
85
+
86
+ const formatters: Record<string, (r: typeof report) => string> = {
87
+ github: formatGitHubAnnotations,
88
+ json: formatJson,
89
+ summary: formatSummary,
90
+ text: formatWardenReport,
91
+ };
92
+ const formatter = formatters[input.format] ?? formatWardenReport;
93
+ const formatted = formatter(report);
94
+
95
+ return Result.ok({
96
+ diagnostics: report.diagnostics,
97
+ drift: report.drift,
98
+ errorCount: report.errorCount,
99
+ formatted,
100
+ passed: report.passed,
101
+ warnCount: report.warnCount,
102
+ });
103
+ },
104
104
  });