@ontrails/trails 1.0.0-beta.12 → 1.0.0-beta.13

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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `add.surface` trail -- Add a surface to an existing project.
2
+ * `add.trailhead` trail -- Add a trailhead to an existing project.
3
3
  *
4
4
  * Generates the CLI or MCP entry point and updates package.json dependencies.
5
5
  */
@@ -13,24 +13,24 @@ import { z } from 'zod';
13
13
  import { findTopoPath } from './project.js';
14
14
 
15
15
  const generateCliEntry = (appImportPath: string): string =>
16
- `import { blaze } from '@ontrails/cli/commander';
16
+ `import { trailhead } from '@ontrails/cli/commander';
17
17
 
18
18
  import { app } from '${appImportPath}';
19
19
 
20
- blaze(app);
20
+ trailhead(app);
21
21
  `;
22
22
 
23
23
  const generateMcpEntry = (appImportPath: string): string =>
24
- `import { blaze } from '@ontrails/mcp';
24
+ `import { trailhead } from '@ontrails/mcp';
25
25
 
26
26
  import { app } from '${appImportPath}';
27
27
 
28
- await blaze(app);
28
+ await trailhead(app);
29
29
  `;
30
30
 
31
- /** Resolve the entry file for a surface. */
32
- const getEntryFile = (surface: 'cli' | 'mcp'): string =>
33
- surface === 'cli' ? 'src/cli.ts' : 'src/mcp.ts';
31
+ /** Resolve the entry file for a trailhead. */
32
+ const getEntryFile = (trailhead: 'cli' | 'mcp'): string =>
33
+ trailhead === 'cli' ? 'src/cli.ts' : 'src/mcp.ts';
34
34
 
35
35
  // ---------------------------------------------------------------------------
36
36
  // Trail definition
@@ -39,13 +39,13 @@ const getEntryFile = (surface: 'cli' | 'mcp'): string =>
39
39
  /** Patch deps and optionally bin in a parsed package.json. */
40
40
  const patchPkgDeps = (
41
41
  pkg: Record<string, unknown>,
42
- surface: 'cli' | 'mcp',
42
+ trailhead: 'cli' | 'mcp',
43
43
  cwd: string
44
44
  ): string => {
45
- const depName = surface === 'cli' ? '@ontrails/cli' : '@ontrails/mcp';
45
+ const depName = trailhead === 'cli' ? '@ontrails/cli' : '@ontrails/mcp';
46
46
  const deps = (pkg['dependencies'] ?? {}) as Record<string, string>;
47
47
  deps[depName] = 'workspace:*';
48
- if (surface === 'cli') {
48
+ if (trailhead === 'cli') {
49
49
  deps['commander'] = '^14.0.0';
50
50
  pkg['bin'] = {
51
51
  [(pkg['name'] as string | undefined) ?? basename(cwd)]: './src/cli.ts',
@@ -57,31 +57,31 @@ const patchPkgDeps = (
57
57
  return depName;
58
58
  };
59
59
 
60
- /** Update package.json with surface dependency and CLI bin if needed. */
61
- const updatePkgJsonForSurface = async (
60
+ /** Update package.json with trailhead dependency and CLI bin if needed. */
61
+ const updatePkgJsonForTrailhead = async (
62
62
  cwd: string,
63
- surface: 'cli' | 'mcp'
63
+ trailhead: 'cli' | 'mcp'
64
64
  ): Promise<string> => {
65
65
  const pkgPath = join(cwd, 'package.json');
66
66
  if (!existsSync(pkgPath)) {
67
- return surface === 'cli' ? '@ontrails/cli' : '@ontrails/mcp';
67
+ return trailhead === 'cli' ? '@ontrails/cli' : '@ontrails/mcp';
68
68
  }
69
69
  const pkg = (await Bun.file(pkgPath).json()) as Record<string, unknown>;
70
- const depName = patchPkgDeps(pkg, surface, cwd);
70
+ const depName = patchPkgDeps(pkg, trailhead, cwd);
71
71
  await Bun.write(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
72
72
  return depName;
73
73
  };
74
74
 
75
- /** Create the entry file for a surface and return the relative path. */
76
- const writeSurfaceEntry = async (
75
+ /** Create the entry file for a trailhead and return the relative path. */
76
+ const writeTrailheadEntry = async (
77
77
  cwd: string,
78
- surface: 'cli' | 'mcp'
78
+ trailhead: 'cli' | 'mcp'
79
79
  ): Promise<string> => {
80
- const entryFile = getEntryFile(surface);
80
+ const entryFile = getEntryFile(trailhead);
81
81
  const fullEntryPath = join(cwd, entryFile);
82
82
  const appImport = (await findTopoPath(cwd)) ?? './app.js';
83
83
  const content =
84
- surface === 'cli'
84
+ trailhead === 'cli'
85
85
  ? generateCliEntry(appImport)
86
86
  : generateMcpEntry(appImport);
87
87
 
@@ -90,30 +90,32 @@ const writeSurfaceEntry = async (
90
90
  return entryFile;
91
91
  };
92
92
 
93
- export const addSurface = trail('add.surface', {
94
- description: 'Add a surface to an existing project',
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) => {
93
+ export const addTrailhead = trail('add.trailhead', {
94
+ blaze: async (input) => {
104
95
  const cwd = resolve(input.dir ?? '.');
105
- const { surface } = input;
106
- const entryFile = getEntryFile(surface);
96
+ const { trailhead } = input;
97
+ const entryFile = getEntryFile(trailhead);
107
98
 
108
99
  if (existsSync(join(cwd, entryFile))) {
109
100
  return Result.err(
110
- new Error(`${surface.toUpperCase()} is already blazed. Nothing to do.`)
101
+ new Error(
102
+ `${trailhead.toUpperCase()} trailhead already exists. Nothing to do.`
103
+ )
111
104
  );
112
105
  }
113
106
 
114
107
  return Result.ok({
115
- created: await writeSurfaceEntry(cwd, surface),
116
- dependency: await updatePkgJsonForSurface(cwd, surface),
108
+ created: await writeTrailheadEntry(cwd, trailhead),
109
+ dependency: await updatePkgJsonForTrailhead(cwd, trailhead),
117
110
  });
118
111
  },
112
+ description: 'Add a trailhead to an existing project',
113
+ input: z.object({
114
+ dir: z.string().optional().describe('Project directory'),
115
+ trailhead: z.enum(['cli', 'mcp']).describe('Trailhead to add'),
116
+ }),
117
+ output: z.object({
118
+ created: z.string(),
119
+ dependency: z.string(),
120
+ }),
119
121
  });
@@ -55,16 +55,7 @@ const updatePackageJsonForVerify = async (
55
55
  // ---------------------------------------------------------------------------
56
56
 
57
57
  export const addVerify = trail('add.verify', {
58
- description: 'Add testing and warden verification',
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) => {
58
+ blaze: async (input) => {
68
59
  const projectDir = resolve(input.dir ?? '.', input.name);
69
60
  const files: string[] = [];
70
61
 
@@ -84,4 +75,13 @@ export const addVerify = trail('add.verify', {
84
75
 
85
76
  return Result.ok({ created: files });
86
77
  },
78
+ description: 'Add testing and warden verification',
79
+ input: z.object({
80
+ dir: z.string().optional().describe('Parent directory'),
81
+ name: z.string().describe('Project name'),
82
+ }),
83
+ meta: { internal: true },
84
+ output: z.object({
85
+ created: z.array(z.string()),
86
+ }),
87
87
  });
@@ -73,7 +73,7 @@ const TSCONFIG_CONTENT = JSON.stringify(
73
73
  const GITIGNORE_CONTENT = `node_modules/
74
74
  dist/
75
75
  *.tsbuildinfo
76
- .trails/_surface.json
76
+ .trails/_trailhead.json
77
77
  `;
78
78
 
79
79
  const OXLINTRC_CONTENT = JSON.stringify(
@@ -107,7 +107,7 @@ export const hello = trail('hello', {
107
107
  name: 'Named greeting',
108
108
  },
109
109
  ],
110
- run: (input) => {
110
+ blaze: (input) => {
111
111
  const name = input.name ?? 'world';
112
112
  return Result.ok({ message: \`Hello, \${name}!\` });
113
113
  },
@@ -139,7 +139,7 @@ export const show = trail('entity.show', {
139
139
  name: 'Show entity',
140
140
  },
141
141
  ],
142
- run: (input) => {
142
+ blaze: (input) => {
143
143
  return Result.ok({ id: input.id, name: 'Example' });
144
144
  },
145
145
  input: z.object({ id: z.string() }),
@@ -156,7 +156,7 @@ export const add = trail('entity.add', {
156
156
  name: 'Add entity',
157
157
  },
158
158
  ],
159
- run: (input) => {
159
+ blaze: (input) => {
160
160
  return Result.ok({ id: '1', name: input.name });
161
161
  },
162
162
  input: z.object({ name: z.string() }),
@@ -177,7 +177,7 @@ export const search = trail('search', {
177
177
  name: 'Search entities',
178
178
  },
179
179
  ],
180
- run: () => {
180
+ blaze: () => {
181
181
  return Result.ok({ results: [] });
182
182
  },
183
183
  input: z.object({ query: z.string() }),
@@ -194,9 +194,9 @@ import { z } from 'zod';
194
194
 
195
195
  export const onboard = trail('entity.onboard', {
196
196
  description: 'Onboard a new entity end-to-end',
197
- follow: ['entity.add'],
198
- run: async (input, ctx) => {
199
- const result = await ctx.follow('entity.add', { name: input.name });
197
+ crosses: ['entity.add'],
198
+ blaze: async (input, ctx) => {
199
+ const result = await ctx.cross('entity.add', { name: input.name });
200
200
  if (result.isErr()) {
201
201
  return result;
202
202
  }
@@ -207,11 +207,11 @@ export const onboard = trail('entity.onboard', {
207
207
  });
208
208
  `;
209
209
 
210
- const generateEntityEvents = (): string =>
211
- `import { event } from '@ontrails/core';
210
+ const generateEntitySignals = (): string =>
211
+ `import { signal } from '@ontrails/core';
212
212
  import { z } from 'zod';
213
213
 
214
- export const entityUpdated = event('entity.updated', {
214
+ export const entityUpdated = signal('entity.updated', {
215
215
  description: 'Fired when an entity is updated',
216
216
  payload: z.object({
217
217
  entityId: z.string(),
@@ -248,9 +248,9 @@ const starterImports: Record<
248
248
  "import * as entity from './trails/entity.js';",
249
249
  "import * as search from './trails/search.js';",
250
250
  "import * as onboard from './trails/onboard.js';",
251
- "import * as entityEvents from './events/entity-events.js';",
251
+ "import * as entitySignals from './signals/entity-signals.js';",
252
252
  ],
253
- modules: ['entity', 'search', 'onboard', 'entityEvents'],
253
+ modules: ['entity', 'search', 'onboard', 'entitySignals'],
254
254
  },
255
255
  hello: {
256
256
  imports: ["import * as hello from './trails/hello.js';"],
@@ -282,7 +282,7 @@ const starterFileGenerators: Record<Starter, () => [string, string][]> = {
282
282
  ['src/trails/entity.ts', generateEntityTrails()],
283
283
  ['src/trails/search.ts', generateSearchTrail()],
284
284
  ['src/trails/onboard.ts', generateOnboardTrail()],
285
- ['src/events/entity-events.ts', generateEntityEvents()],
285
+ ['src/signals/entity-signals.ts', generateEntitySignals()],
286
286
  ['src/store.ts', generateStore()],
287
287
  ],
288
288
  hello: () => [['src/trails/hello.ts', generateHelloTrail()]],
@@ -321,6 +321,19 @@ const writeScaffoldFiles = async (
321
321
  // ---------------------------------------------------------------------------
322
322
 
323
323
  export const createScaffold = trail('create.scaffold', {
324
+ blaze: async (input) => {
325
+ const projectDir = resolve(input.dir ?? '.', input.name);
326
+ const starter = (input.starter ?? 'hello') as Starter;
327
+ const fileMap = collectScaffoldFiles(input.name, starter);
328
+ const files = await writeScaffoldFiles(projectDir, fileMap);
329
+ mkdirSync(join(projectDir, '.trails'), { recursive: true });
330
+
331
+ return Result.ok({
332
+ created: files,
333
+ dir: projectDir,
334
+ name: input.name,
335
+ } satisfies ScaffoldResult);
336
+ },
324
337
  description: 'Scaffold a new Trails project',
325
338
  input: z.object({
326
339
  dir: z.string().optional().describe('Parent directory'),
@@ -330,23 +343,10 @@ export const createScaffold = trail('create.scaffold', {
330
343
  .default('hello')
331
344
  .describe('Starter trail'),
332
345
  }),
333
- metadata: { internal: true },
346
+ meta: { internal: true },
334
347
  output: z.object({
335
348
  created: z.array(z.string()),
336
349
  dir: z.string(),
337
350
  name: z.string(),
338
351
  }),
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
  });
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * `create` route -- Create a new Trails project.
3
3
  *
4
- * Composes create.scaffold, add.surface, and add.verify sub-trails
5
- * via ctx.follow().
4
+ * Composes create.scaffold, add.trailhead, and add.verify sub-trails
5
+ * via ctx.cross().
6
6
  */
7
7
 
8
- import type { FollowFn } from '@ontrails/core';
8
+ import type { CrossFn } from '@ontrails/core';
9
9
  import { Result, trail } from '@ontrails/core';
10
10
  import { z } from 'zod';
11
11
 
@@ -14,13 +14,13 @@ import { z } from 'zod';
14
14
  // ---------------------------------------------------------------------------
15
15
 
16
16
  type Starter = 'empty' | 'entity' | 'hello';
17
- type Surface = 'cli' | 'mcp';
17
+ type Trailhead = 'cli' | 'mcp';
18
18
 
19
- interface BlazeInput {
19
+ interface CreateInput {
20
20
  readonly dir?: string | undefined;
21
21
  readonly name: string;
22
22
  readonly starter: Starter;
23
- readonly surfaces: readonly Surface[];
23
+ readonly trailheads: readonly Trailhead[];
24
24
  readonly verify: boolean;
25
25
  }
26
26
 
@@ -48,9 +48,9 @@ const buildScaffoldInput = (input: ScaffoldRequest) => ({
48
48
  starter: input.starter,
49
49
  });
50
50
 
51
- const buildSurfaceInput = (dir: string, surface: string) => ({
51
+ const buildTrailheadInput = (dir: string, trailhead: string) => ({
52
52
  dir,
53
- surface,
53
+ trailhead,
54
54
  });
55
55
 
56
56
  const buildVerifyInput = (input: VerifyRequest) => ({
@@ -59,22 +59,22 @@ const buildVerifyInput = (input: VerifyRequest) => ({
59
59
  });
60
60
 
61
61
  const scaffoldProject = (
62
- follow: FollowFn,
62
+ cross: CrossFn,
63
63
  input: ScaffoldRequest
64
64
  ): Promise<Result<ScaffoldedProject, Error>> =>
65
- follow('create.scaffold', buildScaffoldInput(input));
65
+ cross('create.scaffold', buildScaffoldInput(input));
66
66
 
67
- const addSurfaceFiles = async (
68
- follow: FollowFn,
67
+ const addTrailheadFiles = async (
68
+ cross: CrossFn,
69
69
  dir: string,
70
- surfaces: readonly string[]
70
+ trailheads: readonly string[]
71
71
  ): Promise<Result<string[], Error>> => {
72
72
  const created: string[] = [];
73
73
 
74
- for (const surface of surfaces) {
75
- const result = await follow<{ created: string; dependency: string }>(
76
- 'add.surface',
77
- buildSurfaceInput(dir, surface)
74
+ for (const trailhead of trailheads) {
75
+ const result = await cross<{ created: string; dependency: string }>(
76
+ 'add.trailhead',
77
+ buildTrailheadInput(dir, trailhead)
78
78
  );
79
79
  if (result.isErr()) {
80
80
  return Result.err(result.error);
@@ -86,14 +86,14 @@ const addSurfaceFiles = async (
86
86
  };
87
87
 
88
88
  const collectVerifyFiles = async (
89
- follow: FollowFn,
89
+ cross: CrossFn,
90
90
  input: VerifyRequest
91
91
  ): Promise<Result<string[], Error>> => {
92
92
  if (!input.verify) {
93
93
  return Result.ok([]);
94
94
  }
95
95
 
96
- const result = await follow<{ created: string[] }>(
96
+ const result = await cross<{ created: string[] }>(
97
97
  'add.verify',
98
98
  buildVerifyInput(input)
99
99
  );
@@ -104,29 +104,29 @@ const collectVerifyFiles = async (
104
104
 
105
105
  const collectCreatedFiles = (
106
106
  scaffolded: readonly string[],
107
- surfaces: readonly string[],
107
+ trailheads: readonly string[],
108
108
  verify: readonly string[]
109
- ): string[] => [...scaffolded, ...surfaces, ...verify];
109
+ ): string[] => [...scaffolded, ...trailheads, ...verify];
110
110
 
111
111
  const runCreate = async (
112
- follow: FollowFn,
113
- input: BlazeInput
112
+ cross: CrossFn,
113
+ input: CreateInput
114
114
  ): Promise<Result<{ created: string[]; dir: string; name: string }, Error>> => {
115
- const scaffolded = await scaffoldProject(follow, input);
115
+ const scaffolded = await scaffoldProject(cross, input);
116
116
  if (scaffolded.isErr()) {
117
117
  return Result.err(scaffolded.error);
118
118
  }
119
119
 
120
- const surfaceResults = await addSurfaceFiles(
121
- follow,
120
+ const trailheadResults = await addTrailheadFiles(
121
+ cross,
122
122
  scaffolded.value.dir,
123
- input.surfaces
123
+ input.trailheads
124
124
  );
125
- if (surfaceResults.isErr()) {
126
- return Result.err(surfaceResults.error);
125
+ if (trailheadResults.isErr()) {
126
+ return Result.err(trailheadResults.error);
127
127
  }
128
128
 
129
- const verifyFiles = await collectVerifyFiles(follow, input);
129
+ const verifyFiles = await collectVerifyFiles(cross, input);
130
130
  if (verifyFiles.isErr()) {
131
131
  return Result.err(verifyFiles.error);
132
132
  }
@@ -134,7 +134,7 @@ const runCreate = async (
134
134
  return Result.ok({
135
135
  created: collectCreatedFiles(
136
136
  scaffolded.value.created,
137
- surfaceResults.value,
137
+ trailheadResults.value,
138
138
  verifyFiles.value
139
139
  ),
140
140
  dir: scaffolded.value.dir,
@@ -147,6 +147,13 @@ const runCreate = async (
147
147
  // ---------------------------------------------------------------------------
148
148
 
149
149
  export const createRoute = trail('create', {
150
+ blaze: async (input: CreateInput, ctx) => {
151
+ if (!ctx.cross) {
152
+ return Result.err(new Error('create route requires ctx.cross'));
153
+ }
154
+ return await runCreate(ctx.cross, input);
155
+ },
156
+ crosses: ['create.scaffold', 'add.trailhead', 'add.verify'],
150
157
  description: 'Create a new Trails project',
151
158
  fields: {
152
159
  starter: {
@@ -157,14 +164,14 @@ export const createRoute = trail('create', {
157
164
  value: 'hello',
158
165
  },
159
166
  {
160
- hint: '4 trails, event, store',
167
+ hint: '4 trails, signal, store',
161
168
  label: 'Entity CRUD',
162
169
  value: 'entity',
163
170
  },
164
171
  { hint: 'Just the structure', label: 'Empty', value: 'empty' },
165
172
  ],
166
173
  },
167
- surfaces: {
174
+ trailheads: {
168
175
  options: [
169
176
  { hint: 'Commander-based command line', label: 'CLI', value: 'cli' },
170
177
  {
@@ -175,7 +182,6 @@ export const createRoute = trail('create', {
175
182
  ],
176
183
  },
177
184
  },
178
- follow: ['create.scaffold', 'add.surface', 'add.verify'],
179
185
  input: z.object({
180
186
  dir: z.string().optional().describe('Parent directory'),
181
187
  name: z.string().describe('Project name'),
@@ -183,10 +189,10 @@ export const createRoute = trail('create', {
183
189
  .enum(['hello', 'entity', 'empty'])
184
190
  .default('hello')
185
191
  .describe('Starter trail'),
186
- surfaces: z
192
+ trailheads: z
187
193
  .array(z.enum(['cli', 'mcp']))
188
194
  .default(['cli'])
189
- .describe('Surfaces'),
195
+ .describe('Trailheads'),
190
196
  verify: z.boolean().default(true).describe('Include testing + warden'),
191
197
  }),
192
198
  output: z.object({
@@ -194,10 +200,4 @@ export const createRoute = trail('create', {
194
200
  dir: z.string(),
195
201
  name: z.string(),
196
202
  }),
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
  });
@@ -55,11 +55,24 @@ const toGuideDetail = (item: Trail<unknown, unknown>): object => ({
55
55
  });
56
56
 
57
57
  export const guideTrail = trail('guide', {
58
+ blaze: async (input, ctx) => {
59
+ const app = await loadApp(input.module, ctx.cwd ?? '.');
60
+
61
+ if (input.trailId) {
62
+ const item = app.get(input.trailId);
63
+ if (!item) {
64
+ return Result.err(new Error(`Trail not found: ${input.trailId}`));
65
+ }
66
+ return Result.ok(toGuideDetail(item as Trail<unknown, unknown>));
67
+ }
68
+
69
+ return Result.ok(toGuideEntries(app));
70
+ },
58
71
  description: 'Runtime guidance for trails',
59
72
  examples: [
60
73
  {
61
74
  description: 'Lists all trails with descriptions and example counts',
62
- input: {},
75
+ input: { module: './src/app.ts' },
63
76
  name: 'List trail guidance',
64
77
  },
65
78
  ],
@@ -88,17 +101,4 @@ export const guideTrail = trail('guide', {
88
101
  kind: z.string(),
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
  });