@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.
- package/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +141 -0
- package/bin/trails.ts +0 -0
- package/dist/src/clack.d.ts.map +1 -1
- package/dist/src/clack.js +22 -0
- package/dist/src/clack.js.map +1 -1
- package/dist/src/trails/add-surface.js +9 -9
- package/dist/src/trails/add-surface.js.map +1 -1
- package/dist/src/trails/add-trail.d.ts +1 -2
- package/dist/src/trails/add-trail.d.ts.map +1 -1
- package/dist/src/trails/add-trail.js +16 -24
- package/dist/src/trails/add-trail.js.map +1 -1
- package/dist/src/trails/add-verify.js +9 -9
- package/dist/src/trails/add-verify.js.map +1 -1
- package/dist/src/trails/create-scaffold.js +25 -25
- package/dist/src/trails/create-scaffold.js.map +1 -1
- package/dist/src/trails/create.d.ts +1 -1
- package/dist/src/trails/create.js +10 -10
- package/dist/src/trails/create.js.map +1 -1
- package/dist/src/trails/guide.js +12 -12
- package/dist/src/trails/guide.js.map +1 -1
- package/dist/src/trails/survey.d.ts +1 -2
- package/dist/src/trails/survey.d.ts.map +1 -1
- package/dist/src/trails/survey.js +59 -26
- package/dist/src/trails/survey.js.map +1 -1
- package/dist/src/trails/warden.d.ts +1 -1
- package/dist/src/trails/warden.js +28 -28
- package/dist/src/trails/warden.js.map +1 -1
- package/package.json +9 -9
- package/src/__tests__/create.test.ts +7 -7
- package/src/__tests__/guide.test.ts +4 -4
- package/src/__tests__/survey.test.ts +6 -8
- package/src/__tests__/warden.test.ts +2 -2
- package/src/clack.ts +22 -0
- package/src/trails/add-surface.ts +9 -9
- package/src/trails/add-trail.ts +16 -25
- package/src/trails/add-verify.ts +9 -9
- package/src/trails/create-scaffold.ts +27 -27
- package/src/trails/create.ts +10 -10
- package/src/trails/guide.ts +14 -14
- package/src/trails/survey.ts +88 -36
- 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
|
-
|
|
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
|
});
|
package/src/trails/add-trail.ts
CHANGED
|
@@ -14,17 +14,9 @@ import { z } from 'zod';
|
|
|
14
14
|
|
|
15
15
|
const generateTrailFile = (
|
|
16
16
|
id: string,
|
|
17
|
-
|
|
18
|
-
destructive: boolean
|
|
17
|
+
intent: 'read' | 'write' | 'destroy'
|
|
19
18
|
): string => {
|
|
20
|
-
const
|
|
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
|
-
|
|
32
|
+
run: async (input) => {
|
|
41
33
|
return Result.ok({ message: 'TODO' });
|
|
42
34
|
},
|
|
43
|
-
input: z.object({}),${
|
|
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
|
-
|
|
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
|
});
|
package/src/trails/add-verify.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
+
intent: 'read',
|
|
188
188
|
});
|
|
189
189
|
`;
|
|
190
190
|
|
|
191
|
-
const
|
|
192
|
-
`import { Result,
|
|
191
|
+
const generateOnboardTrail = (): string =>
|
|
192
|
+
`import { Result, trail } from '@ontrails/core';
|
|
193
193
|
import { z } from 'zod';
|
|
194
194
|
|
|
195
|
-
export const onboard =
|
|
195
|
+
export const onboard = trail('entity.onboard', {
|
|
196
196
|
description: 'Onboard a new entity end-to-end',
|
|
197
|
-
|
|
198
|
-
|
|
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',
|
|
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
|
-
|
|
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
|
});
|
package/src/trails/create.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { FollowFn } from '@ontrails/core';
|
|
9
|
-
import { Result,
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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
|
});
|
package/src/trails/guide.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
});
|
package/src/trails/survey.ts
CHANGED
|
@@ -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
|
-
|
|
92
|
-
destructive?: boolean;
|
|
88
|
+
intent?: 'read' | 'write' | 'destroy';
|
|
93
89
|
}): string => {
|
|
94
|
-
if (entry.
|
|
95
|
-
return '
|
|
90
|
+
if (entry.intent === 'destroy') {
|
|
91
|
+
return 'destroy';
|
|
96
92
|
}
|
|
97
|
-
if (entry.
|
|
98
|
-
return '
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
355
|
+
run: async (input, ctx) => {
|
|
356
|
+
const app = await loadApp(input.module, ctx.cwd ?? '.');
|
|
357
|
+
return dispatchSurvey(app, input);
|
|
358
|
+
},
|
|
307
359
|
});
|
package/src/trails/warden.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
});
|