@ontrails/trails 1.0.0-beta.2 → 1.0.0-beta.21

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 (149) hide show
  1. package/CHANGELOG.md +628 -0
  2. package/README.md +26 -0
  3. package/package.json +28 -7
  4. package/src/app.ts +86 -2
  5. package/src/clack.ts +22 -0
  6. package/src/cli.ts +330 -11
  7. package/src/completions.ts +240 -0
  8. package/src/lifecycle-source-io.ts +33 -0
  9. package/src/load-app-mirror.ts +202 -0
  10. package/src/local-state-io.ts +153 -0
  11. package/src/mcp-app.ts +30 -0
  12. package/src/mcp-options.ts +77 -0
  13. package/src/mcp.ts +8 -0
  14. package/src/project-writes.ts +377 -0
  15. package/src/release/bindings.ts +39 -0
  16. package/src/release/check.ts +818 -0
  17. package/src/release/config.ts +63 -0
  18. package/src/release/contract-facts.ts +425 -0
  19. package/src/release/index.ts +85 -0
  20. package/src/release/native-bun-publish.ts +651 -0
  21. package/src/release/native-bun-registry.ts +350 -0
  22. package/src/release/packed-artifacts-smoke.ts +236 -0
  23. package/src/release/smoke.ts +46 -0
  24. package/src/release/wayfinder-dogfood-smoke.ts +226 -0
  25. package/src/retired-topo-command.ts +36 -0
  26. package/src/run-adapter-check.ts +76 -0
  27. package/src/run-collision.ts +126 -0
  28. package/src/run-completions-install.ts +179 -0
  29. package/src/run-example.ts +149 -0
  30. package/src/run-examples.ts +148 -0
  31. package/src/run-quiet.ts +75 -0
  32. package/src/run-release-check.ts +74 -0
  33. package/src/run-trace.ts +273 -0
  34. package/src/run-warden.ts +39 -0
  35. package/src/run-watch.ts +432 -0
  36. package/src/scaffold-version-sync.ts +183 -0
  37. package/src/scaffold-versions.generated.ts +12 -0
  38. package/src/trails/adapter-check.ts +244 -0
  39. package/src/trails/add-surface.ts +93 -40
  40. package/src/trails/add-trail.ts +79 -41
  41. package/src/trails/add-verify.ts +94 -25
  42. package/src/trails/compile.ts +67 -0
  43. package/src/trails/completions-complete.ts +165 -0
  44. package/src/trails/completions.ts +47 -0
  45. package/src/trails/create-adapter.ts +1084 -0
  46. package/src/trails/create-scaffold.ts +354 -74
  47. package/src/trails/create-versions.ts +62 -0
  48. package/src/trails/create.ts +185 -71
  49. package/src/trails/deprecate.ts +59 -0
  50. package/src/trails/dev-clean.ts +82 -0
  51. package/src/trails/dev-reset.ts +50 -0
  52. package/src/trails/dev-stats.ts +72 -0
  53. package/src/trails/dev-support.ts +340 -0
  54. package/src/trails/doctor.ts +56 -0
  55. package/src/trails/draft-promote.ts +949 -0
  56. package/src/trails/guide.ts +74 -68
  57. package/src/trails/load-app.ts +1143 -15
  58. package/src/trails/project.ts +17 -3
  59. package/src/trails/release-check.ts +104 -0
  60. package/src/trails/release-smoke.ts +48 -0
  61. package/src/trails/revise.ts +53 -0
  62. package/src/trails/root-dir.ts +21 -0
  63. package/src/trails/run-example.ts +491 -0
  64. package/src/trails/run-examples.ts +145 -0
  65. package/src/trails/run.ts +410 -0
  66. package/src/trails/survey.ts +881 -226
  67. package/src/trails/topo-activation.ts +385 -0
  68. package/src/trails/topo-constants.ts +2 -0
  69. package/src/trails/topo-history.ts +47 -0
  70. package/src/trails/topo-output-schemas.ts +248 -0
  71. package/src/trails/topo-pin.ts +52 -0
  72. package/src/trails/topo-read-support.ts +313 -0
  73. package/src/trails/topo-reports.ts +807 -0
  74. package/src/trails/topo-store-support.ts +174 -0
  75. package/src/trails/topo-support.ts +220 -0
  76. package/src/trails/topo-unpin.ts +61 -0
  77. package/src/trails/topo.ts +106 -0
  78. package/src/trails/validate.ts +38 -0
  79. package/src/trails/version-lifecycle-support.ts +945 -0
  80. package/src/trails/warden-guide.ts +129 -0
  81. package/src/trails/warden.ts +165 -58
  82. package/src/versions.ts +31 -0
  83. package/.turbo/turbo-build.log +0 -1
  84. package/.turbo/turbo-lint.log +0 -3
  85. package/.turbo/turbo-typecheck.log +0 -1
  86. package/__tests__/examples.test.ts +0 -6
  87. package/dist/bin/trails.d.ts +0 -3
  88. package/dist/bin/trails.d.ts.map +0 -1
  89. package/dist/bin/trails.js +0 -4
  90. package/dist/bin/trails.js.map +0 -1
  91. package/dist/src/app.d.ts +0 -2
  92. package/dist/src/app.d.ts.map +0 -1
  93. package/dist/src/app.js +0 -11
  94. package/dist/src/app.js.map +0 -1
  95. package/dist/src/clack.d.ts +0 -9
  96. package/dist/src/clack.d.ts.map +0 -1
  97. package/dist/src/clack.js +0 -62
  98. package/dist/src/clack.js.map +0 -1
  99. package/dist/src/cli.d.ts +0 -2
  100. package/dist/src/cli.d.ts.map +0 -1
  101. package/dist/src/cli.js +0 -13
  102. package/dist/src/cli.js.map +0 -1
  103. package/dist/src/trails/add-surface.d.ts +0 -13
  104. package/dist/src/trails/add-surface.d.ts.map +0 -1
  105. package/dist/src/trails/add-surface.js +0 -88
  106. package/dist/src/trails/add-surface.js.map +0 -1
  107. package/dist/src/trails/add-trail.d.ts +0 -11
  108. package/dist/src/trails/add-trail.d.ts.map +0 -1
  109. package/dist/src/trails/add-trail.js +0 -85
  110. package/dist/src/trails/add-trail.js.map +0 -1
  111. package/dist/src/trails/add-verify.d.ts +0 -10
  112. package/dist/src/trails/add-verify.d.ts.map +0 -1
  113. package/dist/src/trails/add-verify.js +0 -67
  114. package/dist/src/trails/add-verify.js.map +0 -1
  115. package/dist/src/trails/create-scaffold.d.ts +0 -15
  116. package/dist/src/trails/create-scaffold.d.ts.map +0 -1
  117. package/dist/src/trails/create-scaffold.js +0 -288
  118. package/dist/src/trails/create-scaffold.js.map +0 -1
  119. package/dist/src/trails/create.d.ts +0 -22
  120. package/dist/src/trails/create.d.ts.map +0 -1
  121. package/dist/src/trails/create.js +0 -121
  122. package/dist/src/trails/create.js.map +0 -1
  123. package/dist/src/trails/guide.d.ts +0 -11
  124. package/dist/src/trails/guide.d.ts.map +0 -1
  125. package/dist/src/trails/guide.js +0 -80
  126. package/dist/src/trails/guide.js.map +0 -1
  127. package/dist/src/trails/load-app.d.ts +0 -4
  128. package/dist/src/trails/load-app.d.ts.map +0 -1
  129. package/dist/src/trails/load-app.js +0 -24
  130. package/dist/src/trails/load-app.js.map +0 -1
  131. package/dist/src/trails/project.d.ts +0 -8
  132. package/dist/src/trails/project.d.ts.map +0 -1
  133. package/dist/src/trails/project.js +0 -43
  134. package/dist/src/trails/project.js.map +0 -1
  135. package/dist/src/trails/survey.d.ts +0 -33
  136. package/dist/src/trails/survey.d.ts.map +0 -1
  137. package/dist/src/trails/survey.js +0 -225
  138. package/dist/src/trails/survey.js.map +0 -1
  139. package/dist/src/trails/warden.d.ts +0 -19
  140. package/dist/src/trails/warden.d.ts.map +0 -1
  141. package/dist/src/trails/warden.js +0 -88
  142. package/dist/src/trails/warden.js.map +0 -1
  143. package/dist/tsconfig.tsbuildinfo +0 -1
  144. package/src/__tests__/create.test.ts +0 -349
  145. package/src/__tests__/guide.test.ts +0 -91
  146. package/src/__tests__/load-app.test.ts +0 -15
  147. package/src/__tests__/survey.test.ts +0 -161
  148. package/src/__tests__/warden.test.ts +0 -74
  149. package/tsconfig.json +0 -9
@@ -4,12 +4,28 @@
4
4
  * Generates package.json, tsconfig, app.ts, starter trails, and .trails/ directory.
5
5
  */
6
6
 
7
- import { mkdirSync } from 'node:fs';
8
- import { dirname, join, resolve } from 'node:path';
7
+ import { resolve } from 'node:path';
9
8
 
10
- import { Result, trail } from '@ontrails/core';
9
+ import { Result, trail, WORKSPACE_GITIGNORE_CONTENT } from '@ontrails/core';
11
10
  import { z } from 'zod';
12
11
 
12
+ import {
13
+ applyProjectOperations,
14
+ planProjectOperations,
15
+ PROJECT_NAME_MESSAGE,
16
+ PROJECT_NAME_PATTERN,
17
+ resolveProjectDir,
18
+ } from '../project-writes.js';
19
+ import type {
20
+ PlannedProjectOperation,
21
+ ProjectWriteOperation,
22
+ } from '../project-writes.js';
23
+ import {
24
+ ontrailsPackageRange,
25
+ scaffoldDependencyVersions,
26
+ trailsPackageVersion,
27
+ } from '../versions.js';
28
+
13
29
  // ---------------------------------------------------------------------------
14
30
  // Types
15
31
  // ---------------------------------------------------------------------------
@@ -19,30 +35,63 @@ type Starter = 'empty' | 'entity' | 'hello';
19
35
  interface ScaffoldResult {
20
36
  readonly created: string[];
21
37
  readonly dir: string;
38
+ readonly dryRun: boolean;
22
39
  readonly name: string;
40
+ readonly plannedOperations: PlannedProjectOperation[];
23
41
  }
24
42
 
43
+ const frameworkCommandScripts = {
44
+ add: 'trails add',
45
+ compile: 'trails compile',
46
+ completions: 'trails completions',
47
+ deprecate: 'trails deprecate',
48
+ diff: 'trails diff',
49
+ doctor: 'trails doctor',
50
+ guide: 'trails guide',
51
+ revise: 'trails revise',
52
+ run: 'trails run',
53
+ survey: 'trails survey',
54
+ topo: 'trails topo',
55
+ validate: 'trails validate',
56
+ warden: 'trails warden',
57
+ } as const satisfies Record<string, string>;
58
+
25
59
  // ---------------------------------------------------------------------------
26
60
  // Content generators
27
61
  // ---------------------------------------------------------------------------
28
62
 
29
63
  const generatePackageJson = (name: string): string => {
30
64
  const deps: Record<string, string> = {
31
- '@ontrails/core': 'workspace:*',
32
- zod: '^4.0.0',
65
+ '@ontrails/core': ontrailsPackageRange,
66
+ zod: scaffoldDependencyVersions.zod,
33
67
  };
34
68
 
35
69
  const pkg: Record<string, unknown> = {
36
70
  dependencies: Object.fromEntries(
37
71
  Object.entries(deps).toSorted(([a], [b]) => a.localeCompare(b))
38
72
  ),
73
+ devDependencies: Object.fromEntries(
74
+ Object.entries({
75
+ '@ontrails/trails': ontrailsPackageRange,
76
+ '@types/bun': scaffoldDependencyVersions.bunTypes,
77
+ oxfmt: scaffoldDependencyVersions.oxfmt,
78
+ oxlint: scaffoldDependencyVersions.oxlint,
79
+ typescript: scaffoldDependencyVersions.typescript,
80
+ ultracite: scaffoldDependencyVersions.ultracite,
81
+ }).toSorted(([a], [b]) => a.localeCompare(b))
82
+ ),
39
83
  name,
40
- scripts: {
41
- build: 'tsc -b',
42
- lint: 'oxlint ./src',
43
- test: 'bun test',
44
- typecheck: 'tsc --noEmit',
45
- },
84
+ scripts: Object.fromEntries(
85
+ Object.entries({
86
+ build: 'tsc -b',
87
+ 'format:check': 'bunx ultracite check .',
88
+ 'format:fix': 'bunx ultracite fix .',
89
+ lint: 'oxlint ./src',
90
+ test: 'bun test',
91
+ typecheck: 'tsc --noEmit',
92
+ ...frameworkCommandScripts,
93
+ }).toSorted(([a], [b]) => a.localeCompare(b))
94
+ ),
46
95
  type: 'module',
47
96
  version: '0.1.0',
48
97
  };
@@ -50,6 +99,18 @@ const generatePackageJson = (name: string): string => {
50
99
  return JSON.stringify(pkg, null, 2);
51
100
  };
52
101
 
102
+ const generateScaffoldProvenance = (starter: Starter): string =>
103
+ JSON.stringify(
104
+ {
105
+ generatedAt: new Date().toISOString(),
106
+ scaffoldVersion: trailsPackageVersion,
107
+ schemaVersion: 1,
108
+ template: starter,
109
+ },
110
+ null,
111
+ 2
112
+ );
113
+
53
114
  const TSCONFIG_CONTENT = JSON.stringify(
54
115
  {
55
116
  compilerOptions: {
@@ -70,19 +131,99 @@ const TSCONFIG_CONTENT = JSON.stringify(
70
131
  2
71
132
  );
72
133
 
134
+ const TSCONFIG_TESTS_CONTENT = JSON.stringify(
135
+ {
136
+ compilerOptions: {
137
+ noEmit: true,
138
+ rootDir: '.',
139
+ types: ['bun'],
140
+ },
141
+ exclude: [],
142
+ extends: './tsconfig.json',
143
+ include: ['src', '__tests__'],
144
+ },
145
+ null,
146
+ 2
147
+ );
148
+
149
+ const AGENTS_CONTENT = `# AGENTS.md
150
+
151
+ This is a Trails project. Trails is an agent-native, contract-first TypeScript framework: author a trail once with typed input, Result output, examples, intent, and meta; surface it through CLI, MCP, HTTP, or future WebSocket without rewriting the contract.
152
+
153
+ ## Commands
154
+
155
+ Use the project scripts first:
156
+
157
+ \`\`\`bash
158
+ bun install
159
+ bun run build
160
+ bun test
161
+ bun run typecheck
162
+ bun run lint
163
+ bun run format:check
164
+ bun run warden
165
+ bun run survey
166
+ bun run guide
167
+ \`\`\`
168
+
169
+ ## Lexicon
170
+
171
+ - \`trail\`, not action or handler
172
+ - \`blaze\`, not handler or impl
173
+ - \`topo\`, not registry or collection
174
+ - \`compose\`, not follow
175
+ - \`surface\`, not transport
176
+ - \`resource\`, not service or dependency
177
+ - \`layer\`, for compose-cutting trail wrapping
178
+
179
+ ## Trail Rules
180
+
181
+ - Blazes return \`Result\`; never throw from trail logic.
182
+ - Use \`Result.ok()\` and \`Result.err()\`; branch with \`isOk()\`, \`isErr()\`, or \`match()\`.
183
+ - Keep trail logic surface-agnostic. Do not import CLI, MCP, HTTP, request, or response types into blazes.
184
+ - Public MCP or HTTP trails declare an \`output\` schema.
185
+ - Trails that compose other trails declare \`composes: [...]\` and invoke them with \`ctx.compose(...)\`.
186
+ - Trails that use infrastructure declare \`resources: [...]\` and access them through the resource helpers.
187
+ - Use \`detours\` for recovery strategies instead of inline retry logic.
188
+ - Prefer examples for happy-path coverage, and add focused tests for edge cases.
189
+ `;
190
+
191
+ const CLAUDE_CONTENT = `# CLAUDE.md
192
+
193
+ ## Compatibility Shim
194
+
195
+ Keep shared project guidance in \`./AGENTS.md\`. Only Claude-specific bootstrap notes belong here.
196
+
197
+ ## Agent Instructions
198
+
199
+ @AGENTS.md
200
+ `;
201
+
73
202
  const GITIGNORE_CONTENT = `node_modules/
74
203
  dist/
75
204
  *.tsbuildinfo
76
- .trails/_surface.json
205
+ .trails/cache/
206
+ .trails/state/
207
+ .trails/config.local.js
208
+ .trails/config.local.ts
77
209
  `;
78
210
 
79
- const OXLINTRC_CONTENT = JSON.stringify(
80
- {
81
- extends: ['ultracite'],
211
+ const OXLINT_CONFIG_CONTENT = `import { defineConfig } from 'oxlint';
212
+ import ultracite from 'ultracite/oxlint/core';
213
+
214
+ export default defineConfig({
215
+ extends: [ultracite],
216
+ rules: {
217
+ 'no-warning-comments': [
218
+ 'error',
219
+ {
220
+ location: 'start',
221
+ terms: ['todo:', 'fixme', 'xxx'],
222
+ },
223
+ ],
82
224
  },
83
- null,
84
- 2
85
- );
225
+ });
226
+ `;
86
227
 
87
228
  const OXFMTRC_CONTENT = `{
88
229
  // ultracite defaults
@@ -107,7 +248,7 @@ export const hello = trail('hello', {
107
248
  name: 'Named greeting',
108
249
  },
109
250
  ],
110
- implementation: (input) => {
251
+ blaze: (input) => {
111
252
  const name = input.name ?? 'world';
112
253
  return Result.ok({ message: \`Hello, \${name}!\` });
113
254
  },
@@ -117,14 +258,18 @@ export const hello = trail('hello', {
117
258
  output: z.object({
118
259
  message: z.string(),
119
260
  }),
120
- readOnly: true,
261
+ intent: 'read',
121
262
  });
122
263
  `;
123
264
 
124
265
  const generateEntityTrails = (): string =>
125
- `import { Result, trail } from '@ontrails/core';
266
+ `import { randomUUID } from 'node:crypto';
267
+
268
+ import { NotFoundError, Result, trail } from '@ontrails/core';
126
269
  import { z } from 'zod';
127
270
 
271
+ import { entityStore } from '../store.js';
272
+
128
273
  const entitySchema = z.object({
129
274
  id: z.string(),
130
275
  name: z.string(),
@@ -139,28 +284,85 @@ export const show = trail('entity.show', {
139
284
  name: 'Show entity',
140
285
  },
141
286
  ],
142
- implementation: (input) => {
143
- return Result.ok({ id: input.id, name: 'Example' });
287
+ blaze: (input, ctx) => {
288
+ const store = entityStore.from(ctx);
289
+ const entity = store.get(input.id);
290
+ if (!entity) {
291
+ return Result.err(new NotFoundError(\`Entity "\${input.id}" not found\`));
292
+ }
293
+ return Result.ok(entity);
144
294
  },
145
295
  input: z.object({ id: z.string() }),
146
296
  output: entitySchema,
147
- readOnly: true,
297
+ intent: 'read',
298
+ resources: [entityStore],
148
299
  });
149
300
 
150
301
  export const add = trail('entity.add', {
151
302
  description: 'Add a new entity',
152
303
  examples: [
153
304
  {
154
- expected: { id: '1', name: 'New' },
305
+ expectedMatch: { name: 'New' },
155
306
  input: { name: 'New' },
156
307
  name: 'Add entity',
157
308
  },
158
309
  ],
159
- implementation: (input) => {
160
- return Result.ok({ id: '1', name: input.name });
310
+ blaze: (input, ctx) => {
311
+ const store = entityStore.from(ctx);
312
+ const entity = { id: randomUUID(), name: input.name };
313
+ store.add(entity);
314
+ return Result.ok(entity);
161
315
  },
162
316
  input: z.object({ name: z.string() }),
163
317
  output: entitySchema,
318
+ intent: 'write',
319
+ permit: { scopes: ['entity:write'] },
320
+ resources: [entityStore],
321
+ });
322
+
323
+ export const list = trail('entity.list', {
324
+ description: 'List entities',
325
+ examples: [
326
+ {
327
+ expected: { entities: [{ id: '1', name: 'Example' }] },
328
+ input: {},
329
+ name: 'List entities',
330
+ },
331
+ ],
332
+ blaze: (_input, ctx) => {
333
+ const store = entityStore.from(ctx);
334
+ return Result.ok({ entities: store.list() });
335
+ },
336
+ input: z.object({}),
337
+ output: z.object({
338
+ entities: z.array(entitySchema),
339
+ }),
340
+ intent: 'read',
341
+ resources: [entityStore],
342
+ });
343
+
344
+ export const remove = trail('entity.delete', {
345
+ description: 'Delete an entity by ID',
346
+ examples: [
347
+ {
348
+ expected: { deleted: true, id: '1' },
349
+ input: { id: '1' },
350
+ name: 'Delete entity',
351
+ },
352
+ ],
353
+ blaze: (input, ctx) => {
354
+ const store = entityStore.from(ctx);
355
+ const deleted = store.delete(input.id);
356
+ return Result.ok({ deleted, id: input.id });
357
+ },
358
+ input: z.object({ id: z.string() }),
359
+ output: z.object({
360
+ deleted: z.boolean(),
361
+ id: z.string(),
362
+ }),
363
+ intent: 'destroy',
364
+ permit: { scopes: ['entity:write'] },
365
+ resources: [entityStore],
164
366
  });
165
367
  `;
166
368
 
@@ -177,26 +379,26 @@ export const search = trail('search', {
177
379
  name: 'Search entities',
178
380
  },
179
381
  ],
180
- implementation: () => {
382
+ blaze: () => {
181
383
  return Result.ok({ results: [] });
182
384
  },
183
385
  input: z.object({ query: z.string() }),
184
386
  output: z.object({
185
387
  results: z.array(z.object({ id: z.string(), name: z.string() })),
186
388
  }),
187
- readOnly: true,
389
+ intent: 'read',
188
390
  });
189
391
  `;
190
392
 
191
- const generateOnboardHike = (): string =>
192
- `import { Result, hike } from '@ontrails/core';
393
+ const generateOnboardTrail = (): string =>
394
+ `import { Result, trail } from '@ontrails/core';
193
395
  import { z } from 'zod';
194
396
 
195
- export const onboard = hike('entity.onboard', {
397
+ export const onboard = trail('entity.onboard', {
196
398
  description: 'Onboard a new entity end-to-end',
197
- follows: ['entity.add'],
198
- implementation: async (input, ctx) => {
199
- const result = await ctx.follow('entity.add', { name: input.name });
399
+ composes: ['entity.add'],
400
+ blaze: async (input, ctx) => {
401
+ const result = await ctx.compose('entity.add', { name: input.name });
200
402
  if (result.isErr()) {
201
403
  return result;
202
404
  }
@@ -204,14 +406,16 @@ export const onboard = hike('entity.onboard', {
204
406
  },
205
407
  input: z.object({ name: z.string() }),
206
408
  output: z.object({ onboarded: z.boolean() }),
409
+ intent: 'write',
410
+ permit: { scopes: ['entity:write'] },
207
411
  });
208
412
  `;
209
413
 
210
- const generateEntityEvents = (): string =>
211
- `import { event } from '@ontrails/core';
414
+ const generateEntitySignals = (): string =>
415
+ `import { signal } from '@ontrails/core';
212
416
  import { z } from 'zod';
213
417
 
214
- export const entityUpdated = event('entity.updated', {
418
+ export const entityUpdated = signal('entity.updated', {
215
419
  description: 'Fired when an entity is updated',
216
420
  payload: z.object({
217
421
  entityId: z.string(),
@@ -221,21 +425,49 @@ export const entityUpdated = event('entity.updated', {
221
425
  `;
222
426
 
223
427
  const generateStore = (): string =>
224
- `/** In-memory store for entities. */
428
+ `import { Result, resource } from '@ontrails/core';
225
429
 
226
- interface Entity {
430
+ /** In-memory store for entities. */
431
+
432
+ export interface Entity {
227
433
  readonly id: string;
228
434
  readonly name: string;
229
435
  }
230
436
 
231
- const store = new Map<string, Entity>();
437
+ export interface EntityStore {
438
+ add(entity: Entity): void;
439
+ delete(id: string): boolean;
440
+ get(id: string): Entity | undefined;
441
+ list(): Entity[];
442
+ }
443
+
444
+ const defaultEntities: readonly Entity[] = [{ id: '1', name: 'Example' }];
232
445
 
233
- export const getEntity = (id: string): Entity | undefined => store.get(id);
234
- export const addEntity = (entity: Entity): void => {
235
- store.set(entity.id, entity);
446
+ export const createEntityStore = (
447
+ seed: readonly Entity[] = defaultEntities
448
+ ): EntityStore => {
449
+ const store = new Map(seed.map((entity) => [entity.id, entity] as const));
450
+ return {
451
+ add(entity) {
452
+ store.set(entity.id, entity);
453
+ },
454
+ delete(id) {
455
+ return store.delete(id);
456
+ },
457
+ get(id) {
458
+ return store.get(id);
459
+ },
460
+ list() {
461
+ return Array.from(store.values());
462
+ },
463
+ };
236
464
  };
237
- export const deleteEntity = (id: string): boolean => store.delete(id);
238
- export const listEntities = (): Entity[] => Array.from(store.values());
465
+
466
+ export const entityStore = resource('entity.store', {
467
+ description: 'In-memory entity store for the entity starter.',
468
+ create: () => Result.ok(createEntityStore()),
469
+ mock: createEntityStore,
470
+ });
239
471
  `;
240
472
 
241
473
  const starterImports: Record<
@@ -248,9 +480,10 @@ const starterImports: Record<
248
480
  "import * as entity from './trails/entity.js';",
249
481
  "import * as search from './trails/search.js';",
250
482
  "import * as onboard from './trails/onboard.js';",
251
- "import * as entityEvents from './events/entity-events.js';",
483
+ "import * as entitySignals from './signals/entity-signals.js';",
484
+ "import * as store from './store.js';",
252
485
  ],
253
- modules: ['entity', 'search', 'onboard', 'entityEvents'],
486
+ modules: ['entity', 'search', 'onboard', 'entitySignals', 'store'],
254
487
  },
255
488
  hello: {
256
489
  imports: ["import * as hello from './trails/hello.js';"],
@@ -260,8 +493,11 @@ const starterImports: Record<
260
493
 
261
494
  const generateAppTs = (name: string, starter: Starter): string => {
262
495
  const { imports, modules } = starterImports[starter];
496
+ const appNameLiteral = JSON.stringify(name);
263
497
  const topoArgs =
264
- modules.length > 0 ? `'${name}', ${modules.join(', ')}` : `'${name}'`;
498
+ modules.length > 0
499
+ ? `${appNameLiteral}, ${modules.join(', ')}`
500
+ : appNameLiteral;
265
501
 
266
502
  return [
267
503
  "import { topo } from '@ontrails/core';",
@@ -281,8 +517,8 @@ const starterFileGenerators: Record<Starter, () => [string, string][]> = {
281
517
  entity: () => [
282
518
  ['src/trails/entity.ts', generateEntityTrails()],
283
519
  ['src/trails/search.ts', generateSearchTrail()],
284
- ['src/trails/onboard.ts', generateOnboardHike()],
285
- ['src/events/entity-events.ts', generateEntityEvents()],
520
+ ['src/trails/onboard.ts', generateOnboardTrail()],
521
+ ['src/signals/entity-signals.ts', generateEntitySignals()],
286
522
  ['src/store.ts', generateStore()],
287
523
  ],
288
524
  hello: () => [['src/trails/hello.ts', generateHelloTrail()]],
@@ -294,59 +530,103 @@ const collectScaffoldFiles = (
294
530
  ): Map<string, string> =>
295
531
  new Map([
296
532
  ['package.json', generatePackageJson(name)],
533
+ ['AGENTS.md', AGENTS_CONTENT],
534
+ ['CLAUDE.md', CLAUDE_CONTENT],
297
535
  ['tsconfig.json', TSCONFIG_CONTENT],
536
+ ['tsconfig.tests.json', TSCONFIG_TESTS_CONTENT],
298
537
  ['.gitignore', GITIGNORE_CONTENT],
299
- ['.oxlintrc.json', OXLINTRC_CONTENT],
538
+ ['oxlint.config.ts', OXLINT_CONFIG_CONTENT],
300
539
  ['.oxfmtrc.jsonc', OXFMTRC_CONTENT],
540
+ ['.trails/.gitignore', WORKSPACE_GITIGNORE_CONTENT],
541
+ ['.trails/scaffold.json', generateScaffoldProvenance(starter)],
301
542
  ['src/app.ts', generateAppTs(name, starter)],
302
543
  ...starterFileGenerators[starter](),
303
544
  ]);
304
545
 
305
- const writeScaffoldFiles = async (
306
- projectDir: string,
546
+ const collectScaffoldOperations = (
307
547
  fileMap: Map<string, string>
308
- ): Promise<string[]> => {
309
- const files: string[] = [];
310
- for (const [relativePath, content] of fileMap) {
311
- const fullPath = join(projectDir, relativePath);
312
- mkdirSync(dirname(fullPath), { recursive: true });
313
- await Bun.write(fullPath, content);
314
- files.push(relativePath);
315
- }
316
- return files;
317
- };
548
+ ): ProjectWriteOperation[] =>
549
+ [...fileMap].map(([path, content]) => ({
550
+ content,
551
+ kind: 'write' as const,
552
+ path,
553
+ }));
318
554
 
319
555
  // ---------------------------------------------------------------------------
320
556
  // Trail definition
321
557
  // ---------------------------------------------------------------------------
322
558
 
323
559
  export const createScaffold = trail('create.scaffold', {
324
- description: 'Scaffold a new Trails project',
325
- implementation: async (input) => {
326
- const projectDir = resolve(input.dir ?? '.', input.name);
560
+ blaze: async (input) => {
561
+ const projectDirResult = resolveProjectDir(input.dir ?? '.', input.name);
562
+ if (projectDirResult.isErr()) {
563
+ return projectDirResult;
564
+ }
565
+
566
+ const projectDir = projectDirResult.value;
327
567
  const starter = (input.starter ?? 'hello') as Starter;
568
+ const dryRun = input.dryRun === true;
328
569
  const fileMap = collectScaffoldFiles(input.name, starter);
329
- const files = await writeScaffoldFiles(projectDir, fileMap);
330
- mkdirSync(join(projectDir, '.trails'), { recursive: true });
570
+ const operations = collectScaffoldOperations(fileMap);
571
+ const plannedOperations = dryRun
572
+ ? planProjectOperations(projectDir, operations, { existing: 'preserve' })
573
+ : await applyProjectOperations(projectDir, operations, {
574
+ existing: 'preserve',
575
+ });
576
+ if (plannedOperations.isErr()) {
577
+ return Result.err(plannedOperations.error);
578
+ }
579
+
580
+ const created = dryRun
581
+ ? []
582
+ : plannedOperations.value
583
+ .filter((operation) => operation.kind === 'write')
584
+ .map((operation) => operation.path);
331
585
 
332
586
  return Result.ok({
333
- created: files,
334
- dir: projectDir,
587
+ created,
588
+ dir: resolve(projectDir),
589
+ dryRun,
335
590
  name: input.name,
591
+ plannedOperations: plannedOperations.value,
336
592
  } satisfies ScaffoldResult);
337
593
  },
594
+ description: 'Scaffold a new Trails project',
338
595
  input: z.object({
339
596
  dir: z.string().optional().describe('Parent directory'),
340
- name: z.string().describe('Project name'),
597
+ dryRun: z
598
+ .boolean()
599
+ .default(false)
600
+ .describe('Plan scaffold writes without touching the project directory'),
601
+ name: z
602
+ .string()
603
+ .regex(PROJECT_NAME_PATTERN, PROJECT_NAME_MESSAGE)
604
+ .describe('Project name'),
341
605
  starter: z
342
606
  .enum(['hello', 'entity', 'empty'])
343
607
  .default('hello')
344
608
  .describe('Starter trail'),
345
609
  }),
346
- markers: { internal: true },
610
+ intent: 'write',
347
611
  output: z.object({
348
- created: z.array(z.string()),
612
+ created: z
613
+ .array(z.string())
614
+ .describe('Project-relative paths of files written (empty in dry-run)'),
349
615
  dir: z.string(),
616
+ dryRun: z.boolean(),
350
617
  name: z.string(),
618
+ plannedOperations: z.array(
619
+ z.discriminatedUnion('kind', [
620
+ z.object({ kind: z.literal('mkdir'), path: z.string() }),
621
+ z.object({
622
+ from: z.string(),
623
+ kind: z.literal('rename'),
624
+ to: z.string(),
625
+ }),
626
+ z.object({ kind: z.literal('write'), path: z.string() }),
627
+ ])
628
+ ),
351
629
  }),
630
+ permit: { scopes: ['project:write'] },
631
+ visibility: 'internal',
352
632
  });
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `create.versions` trail -- Sync generated scaffold dependency versions.
3
+ *
4
+ * Derives `apps/trails/src/scaffold-versions.generated.ts` from the root
5
+ * `package.json` catalog and devDependencies. Graduated from
6
+ * `scripts/sync-scaffold-versions.ts`.
7
+ */
8
+
9
+ import { Result, trail, ValidationError } from '@ontrails/core';
10
+ import { z } from 'zod';
11
+
12
+ import { syncScaffoldVersions } from '../scaffold-version-sync.js';
13
+ import { resolveTrailRootDir } from './root-dir.js';
14
+
15
+ const createVersionsInputSchema = z.object({
16
+ check: z
17
+ .boolean()
18
+ .default(false)
19
+ .describe('Verify the generated file is current instead of writing'),
20
+ rootDir: z.string().optional().describe('Workspace root directory'),
21
+ });
22
+
23
+ const createVersionsOutputSchema = z.object({
24
+ generatedPath: z.string(),
25
+ mode: z.enum(['check', 'write']),
26
+ written: z.boolean(),
27
+ });
28
+
29
+ export const createVersionsTrail = trail('create.versions', {
30
+ blaze: async (input, ctx) => {
31
+ const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
32
+ if (rootDirResult.isErr()) {
33
+ return rootDirResult;
34
+ }
35
+
36
+ try {
37
+ return Result.ok(
38
+ await syncScaffoldVersions({
39
+ check: input.check,
40
+ rootDir: rootDirResult.value,
41
+ })
42
+ );
43
+ } catch (error) {
44
+ return Result.err(
45
+ new ValidationError(
46
+ error instanceof Error ? error.message : String(error)
47
+ )
48
+ );
49
+ }
50
+ },
51
+ description: 'Sync generated scaffold dependency versions',
52
+ examples: [
53
+ {
54
+ input: { check: true },
55
+ name: 'Verify generated scaffold versions are current',
56
+ },
57
+ ],
58
+ input: createVersionsInputSchema,
59
+ intent: 'write',
60
+ output: createVersionsOutputSchema,
61
+ permit: { scopes: ['project:write'] },
62
+ });