@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.
Files changed (42) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +154 -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 +41 -2
  23. package/dist/src/trails/survey.d.ts.map +1 -1
  24. package/dist/src/trails/survey.js +141 -33
  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 +69 -14
  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 +232 -44
  42. 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.implementation(input as never, {} as never);
77
+ return await createScaffold.run(input as never, {} as never);
78
78
  }
79
79
  case 'add.surface': {
80
- return await addSurface.implementation(input as never, {} as never);
80
+ return await addSurface.run(input as never, {} as never);
81
81
  }
82
82
  case 'add.verify': {
83
- return await addVerify.implementation(input as never, {} as never);
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.implementation(
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, hike } from '@ontrails/core'",
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.implementation({ dir, surface: 'mcp' }, {} as never)
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.implementation({ dir, surface: 'mcp' }, {} as never)
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
- implementation: (input) => {
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, Result } from '@ontrails/core';
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 { generateBriefReport } from '../trails/survey.js';
13
- import type { BriefReport } from '../trails/survey.js';
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
- implementation: (input) => {
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
- input: z.object({ name: z.string().optional() }),
36
- output: z.object({ message: z.string() }),
37
- readOnly: true,
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 app = topo('test-app', { bye: byeTrail, hello: helloTrail });
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(2);
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?.readOnly).toBe(true);
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(2);
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
- implementation: async (input, ctx) => {
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
- implementation: async () => { throw new Error("boom"); }
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
- 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
  });