@ontrails/trails 1.0.0-beta.1 → 1.0.0-beta.11
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 +154 -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 +41 -2
- package/dist/src/trails/survey.d.ts.map +1 -1
- package/dist/src/trails/survey.js +141 -33
- 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 +69 -14
- 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 +232 -44
- package/src/trails/warden.ts +33 -33
|
@@ -74,13 +74,13 @@ const runFollow = async (
|
|
|
74
74
|
): Promise<Result<unknown, Error>> => {
|
|
75
75
|
switch (id) {
|
|
76
76
|
case 'create.scaffold': {
|
|
77
|
-
return await createScaffold.
|
|
77
|
+
return await createScaffold.run(input as never, {} as never);
|
|
78
78
|
}
|
|
79
79
|
case 'add.surface': {
|
|
80
|
-
return await addSurface.
|
|
80
|
+
return await addSurface.run(input as never, {} as never);
|
|
81
81
|
}
|
|
82
82
|
case 'add.verify': {
|
|
83
|
-
return await addVerify.
|
|
83
|
+
return await addVerify.run(input as never, {} as never);
|
|
84
84
|
}
|
|
85
85
|
default: {
|
|
86
86
|
return Result.err(new Error(`Unknown follow target: ${id}`));
|
|
@@ -96,7 +96,7 @@ const runBlaze = (
|
|
|
96
96
|
verify: boolean;
|
|
97
97
|
}>
|
|
98
98
|
) =>
|
|
99
|
-
createRoute.
|
|
99
|
+
createRoute.run(
|
|
100
100
|
{
|
|
101
101
|
dir: dirname(projectDir),
|
|
102
102
|
name: basename(projectDir),
|
|
@@ -199,7 +199,7 @@ const assertEntityStarter = (dir: string): void => {
|
|
|
199
199
|
'return Result.ok({ results: [] })',
|
|
200
200
|
]);
|
|
201
201
|
expectContainsAll(readText(dir, 'src/trails/onboard.ts'), [
|
|
202
|
-
"import { Result,
|
|
202
|
+
"import { Result, trail } from '@ontrails/core'",
|
|
203
203
|
'return Result.ok({ onboarded: true })',
|
|
204
204
|
]);
|
|
205
205
|
};
|
|
@@ -289,7 +289,7 @@ describe('trails blaze', () => {
|
|
|
289
289
|
await withTempProject(async (dir) => {
|
|
290
290
|
setupMinimalProject(dir);
|
|
291
291
|
const result = expectOk(
|
|
292
|
-
await addSurface.
|
|
292
|
+
await addSurface.run({ dir, surface: 'mcp' }, {} as never)
|
|
293
293
|
);
|
|
294
294
|
|
|
295
295
|
expect(result.created).toBe('src/mcp.ts');
|
|
@@ -313,7 +313,7 @@ describe('trails blaze', () => {
|
|
|
313
313
|
writeFileSync(join(dir, 'src', 'mcp.ts'), 'existing content');
|
|
314
314
|
|
|
315
315
|
const error = expectErr(
|
|
316
|
-
await addSurface.
|
|
316
|
+
await addSurface.run({ dir, surface: 'mcp' }, {} as never)
|
|
317
317
|
);
|
|
318
318
|
expect(error.message).toBe('MCP is already blazed. Nothing to do.');
|
|
319
319
|
});
|
|
@@ -25,13 +25,13 @@ const helloTrail = trail('hello', {
|
|
|
25
25
|
name: 'Named greeting',
|
|
26
26
|
},
|
|
27
27
|
],
|
|
28
|
-
|
|
28
|
+
input: z.object({ name: z.string().optional() }),
|
|
29
|
+
intent: 'read',
|
|
30
|
+
output: z.object({ message: z.string() }),
|
|
31
|
+
run: (input) => {
|
|
29
32
|
const name = input.name ?? 'world';
|
|
30
33
|
return Result.ok({ message: `Hello, ${name}!` });
|
|
31
34
|
},
|
|
32
|
-
input: z.object({ name: z.string().optional() }),
|
|
33
|
-
output: z.object({ message: z.string() }),
|
|
34
|
-
readOnly: true,
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
const app = topo('test-app', { hello: helloTrail });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
2
|
|
|
3
|
-
import { topo, trail
|
|
3
|
+
import { Result, service, topo, trail } from '@ontrails/core';
|
|
4
4
|
import {
|
|
5
5
|
generateSurfaceMap,
|
|
6
6
|
hashSurfaceMap,
|
|
@@ -9,8 +9,16 @@ import {
|
|
|
9
9
|
import type { SurfaceMap } from '@ontrails/schema';
|
|
10
10
|
import { z } from 'zod';
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
generateBriefReport,
|
|
14
|
+
generateSurveyList,
|
|
15
|
+
generateTrailDetail,
|
|
16
|
+
} from '../trails/survey.js';
|
|
17
|
+
import type {
|
|
18
|
+
BriefReport,
|
|
19
|
+
SurveyListReport,
|
|
20
|
+
TrailDetailReport,
|
|
21
|
+
} from '../trails/survey.js';
|
|
14
22
|
|
|
15
23
|
// ---------------------------------------------------------------------------
|
|
16
24
|
// Test fixtures
|
|
@@ -28,23 +36,37 @@ const helloTrail = trail('hello', {
|
|
|
28
36
|
name: 'Default greeting',
|
|
29
37
|
},
|
|
30
38
|
],
|
|
31
|
-
|
|
39
|
+
input: z.object({ name: z.string().optional() }),
|
|
40
|
+
intent: 'read',
|
|
41
|
+
output: z.object({ message: z.string() }),
|
|
42
|
+
run: (input) => {
|
|
32
43
|
const name = input.name ?? 'world';
|
|
33
44
|
return Result.ok({ message: `Hello, ${name}!` });
|
|
34
45
|
},
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
services: [
|
|
47
|
+
service('db.main', {
|
|
48
|
+
create: () => Result.ok({ source: 'factory' }),
|
|
49
|
+
}),
|
|
50
|
+
],
|
|
38
51
|
});
|
|
39
52
|
|
|
40
53
|
const byeTrail = trail('bye', {
|
|
41
54
|
description: 'Say goodbye',
|
|
42
|
-
implementation: (input) => Result.ok({ message: `Goodbye, ${input.name}!` }),
|
|
43
55
|
input: z.object({ name: z.string() }),
|
|
44
56
|
output: z.object({ message: z.string() }),
|
|
57
|
+
run: (input) => Result.ok({ message: `Goodbye, ${input.name}!` }),
|
|
45
58
|
});
|
|
46
59
|
|
|
47
|
-
const
|
|
60
|
+
const [dbService] = helloTrail.services;
|
|
61
|
+
if (!dbService) {
|
|
62
|
+
throw new Error('Expected helloTrail to declare db.main');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const app = topo('test-app', {
|
|
66
|
+
bye: byeTrail,
|
|
67
|
+
dbService,
|
|
68
|
+
hello: helloTrail,
|
|
69
|
+
});
|
|
48
70
|
|
|
49
71
|
// ---------------------------------------------------------------------------
|
|
50
72
|
// Tests
|
|
@@ -53,10 +75,11 @@ const app = topo('test-app', { bye: byeTrail, hello: helloTrail });
|
|
|
53
75
|
describe('trails survey', () => {
|
|
54
76
|
test('generateSurfaceMap includes all trails', () => {
|
|
55
77
|
const surfaceMap = generateSurfaceMap(app);
|
|
56
|
-
expect(surfaceMap.entries.length).toBe(
|
|
78
|
+
expect(surfaceMap.entries.length).toBe(3);
|
|
57
79
|
const ids = surfaceMap.entries.map((e) => e.id);
|
|
58
80
|
expect(ids).toContain('hello');
|
|
59
81
|
expect(ids).toContain('bye');
|
|
82
|
+
expect(ids).toContain('db.main');
|
|
60
83
|
});
|
|
61
84
|
|
|
62
85
|
test('surface map entries have expected fields', () => {
|
|
@@ -64,8 +87,9 @@ describe('trails survey', () => {
|
|
|
64
87
|
const hello = surfaceMap.entries.find((e) => e.id === 'hello');
|
|
65
88
|
expect(hello).toBeDefined();
|
|
66
89
|
expect(hello?.kind).toBe('trail');
|
|
67
|
-
expect(hello?.
|
|
90
|
+
expect(hello?.intent).toBe('read');
|
|
68
91
|
expect(hello?.exampleCount).toBe(1);
|
|
92
|
+
expect(hello?.services).toEqual(['db.main']);
|
|
69
93
|
});
|
|
70
94
|
|
|
71
95
|
test('JSON output is valid JSON', () => {
|
|
@@ -73,7 +97,7 @@ describe('trails survey', () => {
|
|
|
73
97
|
const json = JSON.stringify(surfaceMap, null, 2);
|
|
74
98
|
const parsed = JSON.parse(json) as SurfaceMap;
|
|
75
99
|
expect(parsed.version).toBe('1.0');
|
|
76
|
-
expect(parsed.entries.length).toBe(
|
|
100
|
+
expect(parsed.entries.length).toBe(3);
|
|
77
101
|
});
|
|
78
102
|
|
|
79
103
|
test('hashSurfaceMap produces stable hash', () => {
|
|
@@ -129,8 +153,8 @@ describe('trails survey --brief', () => {
|
|
|
129
153
|
test('report includes correct trail count', () => {
|
|
130
154
|
const report = generateBriefReport(app);
|
|
131
155
|
expect(report.trails).toBe(2);
|
|
132
|
-
expect(report.hikes).toBe(0);
|
|
133
156
|
expect(report.events).toBe(0);
|
|
157
|
+
expect(report.services).toBe(1);
|
|
134
158
|
});
|
|
135
159
|
|
|
136
160
|
test('detects features in use', () => {
|
|
@@ -138,8 +162,8 @@ describe('trails survey --brief', () => {
|
|
|
138
162
|
expect(report.features.outputSchemas).toBe(true);
|
|
139
163
|
expect(report.features.examples).toBe(true);
|
|
140
164
|
expect(report.features.detours).toBe(true);
|
|
141
|
-
expect(report.features.hikes).toBe(false);
|
|
142
165
|
expect(report.features.events).toBe(false);
|
|
166
|
+
expect(report.features.services).toBe(true);
|
|
143
167
|
});
|
|
144
168
|
|
|
145
169
|
test('JSON output is valid', () => {
|
|
@@ -148,6 +172,7 @@ describe('trails survey --brief', () => {
|
|
|
148
172
|
const parsed = JSON.parse(json) as BriefReport;
|
|
149
173
|
expect(parsed.name).toBe('test-app');
|
|
150
174
|
expect(parsed.trails).toBe(2);
|
|
175
|
+
expect(parsed.services).toBe(1);
|
|
151
176
|
});
|
|
152
177
|
|
|
153
178
|
test('empty app reports zero features', () => {
|
|
@@ -157,5 +182,35 @@ describe('trails survey --brief', () => {
|
|
|
157
182
|
expect(report.features.outputSchemas).toBe(false);
|
|
158
183
|
expect(report.features.examples).toBe(false);
|
|
159
184
|
expect(report.features.detours).toBe(false);
|
|
185
|
+
expect(report.features.services).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('trails survey detail', () => {
|
|
190
|
+
test('trail detail includes declared services, follow, and intent', () => {
|
|
191
|
+
const detail = generateTrailDetail(helloTrail);
|
|
192
|
+
const parsed = structuredClone(detail) as TrailDetailReport;
|
|
193
|
+
|
|
194
|
+
expect(parsed.follow).toEqual([]);
|
|
195
|
+
expect(parsed.intent).toBe('read');
|
|
196
|
+
expect(parsed.services).toEqual(['db.main']);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('trails survey services section', () => {
|
|
201
|
+
test('list output includes service lifetime and health status', () => {
|
|
202
|
+
const report = generateSurveyList(app);
|
|
203
|
+
const parsed = structuredClone(report) as SurveyListReport;
|
|
204
|
+
const db = parsed.services.find((entry) => entry.id === 'db.main');
|
|
205
|
+
|
|
206
|
+
expect(parsed.serviceCount).toBe(1);
|
|
207
|
+
expect(db).toEqual({
|
|
208
|
+
description: null,
|
|
209
|
+
health: 'none',
|
|
210
|
+
id: 'db.main',
|
|
211
|
+
kind: 'service',
|
|
212
|
+
lifetime: 'singleton',
|
|
213
|
+
usedBy: ['hello'],
|
|
214
|
+
});
|
|
160
215
|
});
|
|
161
216
|
});
|
|
@@ -21,7 +21,7 @@ describe('trails warden', () => {
|
|
|
21
21
|
writeFileSync(
|
|
22
22
|
join(dir, 'good.ts'),
|
|
23
23
|
`trail("hello", {
|
|
24
|
-
|
|
24
|
+
run: async (input, ctx) => {
|
|
25
25
|
return Result.ok({ message: "hi" });
|
|
26
26
|
}
|
|
27
27
|
})`
|
|
@@ -54,7 +54,7 @@ describe('trails warden', () => {
|
|
|
54
54
|
writeFileSync(
|
|
55
55
|
join(dir, 'bad.ts'),
|
|
56
56
|
`trail("x", {
|
|
57
|
-
|
|
57
|
+
run: async () => { throw new Error("boom"); }
|
|
58
58
|
})`
|
|
59
59
|
);
|
|
60
60
|
const report = await runWarden({ driftOnly: true, rootDir: dir });
|
package/src/clack.ts
CHANGED
|
@@ -55,8 +55,30 @@ const fieldResolvers: Record<Field['type'], FieldResolver> = {
|
|
|
55
55
|
const raw = await clack.text({ message: field.label });
|
|
56
56
|
return clack.isCancel(raw) ? undefined : Number(raw);
|
|
57
57
|
},
|
|
58
|
+
'number[]': async (field) => {
|
|
59
|
+
const raw = await clack.text({
|
|
60
|
+
message: `${field.label} (comma-separated numbers)`,
|
|
61
|
+
});
|
|
62
|
+
if (clack.isCancel(raw)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
return String(raw)
|
|
66
|
+
.split(',')
|
|
67
|
+
.map((s) => Number(s.trim()));
|
|
68
|
+
},
|
|
58
69
|
string: async (field) =>
|
|
59
70
|
cancelable(await clack.text({ message: field.label })),
|
|
71
|
+
'string[]': async (field) => {
|
|
72
|
+
const raw = await clack.text({
|
|
73
|
+
message: `${field.label} (comma-separated)`,
|
|
74
|
+
});
|
|
75
|
+
if (clack.isCancel(raw)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
return String(raw)
|
|
79
|
+
.split(',')
|
|
80
|
+
.map((s) => s.trim());
|
|
81
|
+
},
|
|
60
82
|
};
|
|
61
83
|
|
|
62
84
|
/** Resolve a single field value with Clack. */
|
|
@@ -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
|
});
|