@ontrails/trails 1.0.0-beta.14 → 1.0.0-beta.16

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 (197) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +27 -0
  3. package/package.json +19 -8
  4. package/src/app.ts +17 -7
  5. package/src/clack.ts +1 -1
  6. package/src/cli.ts +304 -10
  7. package/src/completions.ts +240 -0
  8. package/src/load-app-mirror.ts +160 -0
  9. package/src/local-state-io.ts +153 -0
  10. package/src/project-writes.ts +320 -0
  11. package/src/run-collision.ts +125 -0
  12. package/src/run-completions-install.ts +179 -0
  13. package/src/run-example.ts +149 -0
  14. package/src/run-examples.ts +148 -0
  15. package/src/run-quiet.ts +75 -0
  16. package/src/run-trace.ts +273 -0
  17. package/src/run-warden.ts +39 -0
  18. package/src/run-watch.ts +432 -0
  19. package/src/scaffold-versions.generated.ts +12 -0
  20. package/src/trails/add-surface.ts +172 -0
  21. package/src/trails/add-trail.ts +73 -27
  22. package/src/trails/add-verify.ts +68 -23
  23. package/src/trails/completions-complete.ts +165 -0
  24. package/src/trails/completions.ts +47 -0
  25. package/src/trails/create-scaffold.ts +101 -35
  26. package/src/trails/create.ts +87 -74
  27. package/src/trails/dev-clean.ts +31 -22
  28. package/src/trails/dev-reset.ts +9 -3
  29. package/src/trails/dev-stats.ts +28 -20
  30. package/src/trails/dev-support.ts +109 -95
  31. package/src/trails/draft-promote.ts +351 -107
  32. package/src/trails/guide.ts +55 -38
  33. package/src/trails/load-app.ts +712 -38
  34. package/src/trails/root-dir.ts +21 -0
  35. package/src/trails/run-example.ts +482 -0
  36. package/src/trails/run-examples.ts +141 -0
  37. package/src/trails/run.ts +403 -0
  38. package/src/trails/survey.ts +517 -186
  39. package/src/trails/topo-activation.ts +385 -0
  40. package/src/trails/topo-compile.ts +55 -0
  41. package/src/trails/topo-history.ts +14 -11
  42. package/src/trails/topo-output-schemas.ts +175 -0
  43. package/src/trails/topo-pin.ts +25 -16
  44. package/src/trails/topo-read-support.ts +178 -238
  45. package/src/trails/topo-reports.ts +445 -63
  46. package/src/trails/topo-store-support.ts +67 -35
  47. package/src/trails/topo-support.ts +93 -147
  48. package/src/trails/topo-unpin.ts +17 -7
  49. package/src/trails/topo-verify.ts +19 -10
  50. package/src/trails/topo.ts +64 -31
  51. package/src/trails/warden-guide.ts +121 -0
  52. package/src/trails/warden.ts +137 -47
  53. package/src/versions.ts +28 -0
  54. package/.turbo/turbo-build.log +0 -1
  55. package/.turbo/turbo-lint.log +0 -3
  56. package/.turbo/turbo-typecheck.log +0 -1
  57. package/__tests__/examples.test.ts +0 -20
  58. package/dist/bin/trails.d.ts +0 -3
  59. package/dist/bin/trails.d.ts.map +0 -1
  60. package/dist/bin/trails.js +0 -4
  61. package/dist/bin/trails.js.map +0 -1
  62. package/dist/src/app.d.ts +0 -2
  63. package/dist/src/app.d.ts.map +0 -1
  64. package/dist/src/app.js +0 -22
  65. package/dist/src/app.js.map +0 -1
  66. package/dist/src/clack.d.ts +0 -9
  67. package/dist/src/clack.d.ts.map +0 -1
  68. package/dist/src/clack.js +0 -84
  69. package/dist/src/clack.js.map +0 -1
  70. package/dist/src/cli.d.ts +0 -2
  71. package/dist/src/cli.d.ts.map +0 -1
  72. package/dist/src/cli.js +0 -13
  73. package/dist/src/cli.js.map +0 -1
  74. package/dist/src/trails/add-surface.d.ts +0 -13
  75. package/dist/src/trails/add-surface.d.ts.map +0 -1
  76. package/dist/src/trails/add-surface.js +0 -88
  77. package/dist/src/trails/add-surface.js.map +0 -1
  78. package/dist/src/trails/add-trail.d.ts +0 -10
  79. package/dist/src/trails/add-trail.d.ts.map +0 -1
  80. package/dist/src/trails/add-trail.js +0 -77
  81. package/dist/src/trails/add-trail.js.map +0 -1
  82. package/dist/src/trails/add-trailhead.d.ts +0 -13
  83. package/dist/src/trails/add-trailhead.d.ts.map +0 -1
  84. package/dist/src/trails/add-trailhead.js +0 -88
  85. package/dist/src/trails/add-trailhead.js.map +0 -1
  86. package/dist/src/trails/add-verify.d.ts +0 -10
  87. package/dist/src/trails/add-verify.d.ts.map +0 -1
  88. package/dist/src/trails/add-verify.js +0 -67
  89. package/dist/src/trails/add-verify.js.map +0 -1
  90. package/dist/src/trails/create-scaffold.d.ts +0 -15
  91. package/dist/src/trails/create-scaffold.d.ts.map +0 -1
  92. package/dist/src/trails/create-scaffold.js +0 -288
  93. package/dist/src/trails/create-scaffold.js.map +0 -1
  94. package/dist/src/trails/create.d.ts +0 -22
  95. package/dist/src/trails/create.d.ts.map +0 -1
  96. package/dist/src/trails/create.js +0 -121
  97. package/dist/src/trails/create.js.map +0 -1
  98. package/dist/src/trails/dev-clean.d.ts +0 -9
  99. package/dist/src/trails/dev-clean.d.ts.map +0 -1
  100. package/dist/src/trails/dev-clean.js +0 -65
  101. package/dist/src/trails/dev-clean.js.map +0 -1
  102. package/dist/src/trails/dev-reset.d.ts +0 -6
  103. package/dist/src/trails/dev-reset.d.ts.map +0 -1
  104. package/dist/src/trails/dev-reset.js +0 -38
  105. package/dist/src/trails/dev-reset.js.map +0 -1
  106. package/dist/src/trails/dev-stats.d.ts +0 -7
  107. package/dist/src/trails/dev-stats.d.ts.map +0 -1
  108. package/dist/src/trails/dev-stats.js +0 -61
  109. package/dist/src/trails/dev-stats.js.map +0 -1
  110. package/dist/src/trails/dev-support.d.ts +0 -64
  111. package/dist/src/trails/dev-support.d.ts.map +0 -1
  112. package/dist/src/trails/dev-support.js +0 -178
  113. package/dist/src/trails/dev-support.js.map +0 -1
  114. package/dist/src/trails/draft-promote.d.ts +0 -18
  115. package/dist/src/trails/draft-promote.d.ts.map +0 -1
  116. package/dist/src/trails/draft-promote.js +0 -386
  117. package/dist/src/trails/draft-promote.js.map +0 -1
  118. package/dist/src/trails/guide.d.ts +0 -21
  119. package/dist/src/trails/guide.d.ts.map +0 -1
  120. package/dist/src/trails/guide.js +0 -64
  121. package/dist/src/trails/guide.js.map +0 -1
  122. package/dist/src/trails/load-app.d.ts +0 -6
  123. package/dist/src/trails/load-app.d.ts.map +0 -1
  124. package/dist/src/trails/load-app.js +0 -67
  125. package/dist/src/trails/load-app.js.map +0 -1
  126. package/dist/src/trails/project.d.ts +0 -8
  127. package/dist/src/trails/project.d.ts.map +0 -1
  128. package/dist/src/trails/project.js +0 -54
  129. package/dist/src/trails/project.js.map +0 -1
  130. package/dist/src/trails/survey.d.ts +0 -18
  131. package/dist/src/trails/survey.d.ts.map +0 -1
  132. package/dist/src/trails/survey.js +0 -212
  133. package/dist/src/trails/survey.js.map +0 -1
  134. package/dist/src/trails/topo-constants.d.ts +0 -3
  135. package/dist/src/trails/topo-constants.d.ts.map +0 -1
  136. package/dist/src/trails/topo-constants.js +0 -3
  137. package/dist/src/trails/topo-constants.js.map +0 -1
  138. package/dist/src/trails/topo-export.d.ts +0 -18
  139. package/dist/src/trails/topo-export.d.ts.map +0 -1
  140. package/dist/src/trails/topo-export.js +0 -34
  141. package/dist/src/trails/topo-export.js.map +0 -1
  142. package/dist/src/trails/topo-history.d.ts +0 -24
  143. package/dist/src/trails/topo-history.d.ts.map +0 -1
  144. package/dist/src/trails/topo-history.js +0 -33
  145. package/dist/src/trails/topo-history.js.map +0 -1
  146. package/dist/src/trails/topo-pin.d.ts +0 -21
  147. package/dist/src/trails/topo-pin.d.ts.map +0 -1
  148. package/dist/src/trails/topo-pin.js +0 -35
  149. package/dist/src/trails/topo-pin.js.map +0 -1
  150. package/dist/src/trails/topo-read-support.d.ts +0 -54
  151. package/dist/src/trails/topo-read-support.d.ts.map +0 -1
  152. package/dist/src/trails/topo-read-support.js +0 -178
  153. package/dist/src/trails/topo-read-support.js.map +0 -1
  154. package/dist/src/trails/topo-reports.d.ts +0 -50
  155. package/dist/src/trails/topo-reports.d.ts.map +0 -1
  156. package/dist/src/trails/topo-reports.js +0 -122
  157. package/dist/src/trails/topo-reports.js.map +0 -1
  158. package/dist/src/trails/topo-show.d.ts +0 -23
  159. package/dist/src/trails/topo-show.d.ts.map +0 -1
  160. package/dist/src/trails/topo-show.js +0 -53
  161. package/dist/src/trails/topo-show.js.map +0 -1
  162. package/dist/src/trails/topo-store-support.d.ts +0 -13
  163. package/dist/src/trails/topo-store-support.d.ts.map +0 -1
  164. package/dist/src/trails/topo-store-support.js +0 -55
  165. package/dist/src/trails/topo-store-support.js.map +0 -1
  166. package/dist/src/trails/topo-support.d.ts +0 -87
  167. package/dist/src/trails/topo-support.d.ts.map +0 -1
  168. package/dist/src/trails/topo-support.js +0 -165
  169. package/dist/src/trails/topo-support.js.map +0 -1
  170. package/dist/src/trails/topo-unpin.d.ts +0 -15
  171. package/dist/src/trails/topo-unpin.d.ts.map +0 -1
  172. package/dist/src/trails/topo-unpin.js +0 -39
  173. package/dist/src/trails/topo-unpin.js.map +0 -1
  174. package/dist/src/trails/topo-verify.d.ts +0 -5
  175. package/dist/src/trails/topo-verify.d.ts.map +0 -1
  176. package/dist/src/trails/topo-verify.js +0 -28
  177. package/dist/src/trails/topo-verify.js.map +0 -1
  178. package/dist/src/trails/topo.d.ts +0 -5
  179. package/dist/src/trails/topo.d.ts.map +0 -1
  180. package/dist/src/trails/topo.js +0 -67
  181. package/dist/src/trails/topo.js.map +0 -1
  182. package/dist/src/trails/warden.d.ts +0 -19
  183. package/dist/src/trails/warden.d.ts.map +0 -1
  184. package/dist/src/trails/warden.js +0 -89
  185. package/dist/src/trails/warden.js.map +0 -1
  186. package/dist/tsconfig.tsbuildinfo +0 -1
  187. package/src/__tests__/create.test.ts +0 -351
  188. package/src/__tests__/draft-promote.test.ts +0 -144
  189. package/src/__tests__/guide.test.ts +0 -91
  190. package/src/__tests__/load-app.test.ts +0 -58
  191. package/src/__tests__/survey.test.ts +0 -301
  192. package/src/__tests__/topo-dev.test.ts +0 -424
  193. package/src/__tests__/warden.test.ts +0 -74
  194. package/src/trails/add-trailhead.ts +0 -121
  195. package/src/trails/topo-export.ts +0 -39
  196. package/src/trails/topo-show.ts +0 -58
  197. package/tsconfig.json +0 -9
@@ -4,12 +4,27 @@
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
+ } from '../versions.js';
27
+
13
28
  // ---------------------------------------------------------------------------
14
29
  // Types
15
30
  // ---------------------------------------------------------------------------
@@ -19,7 +34,9 @@ type Starter = 'empty' | 'entity' | 'hello';
19
34
  interface ScaffoldResult {
20
35
  readonly created: string[];
21
36
  readonly dir: string;
37
+ readonly dryRun: boolean;
22
38
  readonly name: string;
39
+ readonly plannedOperations: PlannedProjectOperation[];
23
40
  }
24
41
 
25
42
  // ---------------------------------------------------------------------------
@@ -28,17 +45,28 @@ interface ScaffoldResult {
28
45
 
29
46
  const generatePackageJson = (name: string): string => {
30
47
  const deps: Record<string, string> = {
31
- '@ontrails/core': 'workspace:*',
32
- zod: '^4.0.0',
48
+ '@ontrails/core': ontrailsPackageRange,
49
+ zod: scaffoldDependencyVersions.zod,
33
50
  };
34
51
 
35
52
  const pkg: Record<string, unknown> = {
36
53
  dependencies: Object.fromEntries(
37
54
  Object.entries(deps).toSorted(([a], [b]) => a.localeCompare(b))
38
55
  ),
56
+ devDependencies: Object.fromEntries(
57
+ Object.entries({
58
+ '@types/bun': scaffoldDependencyVersions.bunTypes,
59
+ oxfmt: scaffoldDependencyVersions.oxfmt,
60
+ oxlint: scaffoldDependencyVersions.oxlint,
61
+ typescript: scaffoldDependencyVersions.typescript,
62
+ ultracite: scaffoldDependencyVersions.ultracite,
63
+ }).toSorted(([a], [b]) => a.localeCompare(b))
64
+ ),
39
65
  name,
40
66
  scripts: {
41
67
  build: 'tsc -b',
68
+ 'format:check': 'bunx ultracite check .',
69
+ 'format:fix': 'bunx ultracite fix .',
42
70
  lint: 'oxlint ./src',
43
71
  test: 'bun test',
44
72
  typecheck: 'tsc --noEmit',
@@ -73,16 +101,19 @@ const TSCONFIG_CONTENT = JSON.stringify(
73
101
  const GITIGNORE_CONTENT = `node_modules/
74
102
  dist/
75
103
  *.tsbuildinfo
76
- .trails/_trailhead.json
104
+ .trails/cache/
105
+ .trails/state/
106
+ .trails/config.local.js
107
+ .trails/config.local.ts
77
108
  `;
78
109
 
79
- const OXLINTRC_CONTENT = JSON.stringify(
80
- {
81
- extends: ['ultracite'],
82
- },
83
- null,
84
- 2
85
- );
110
+ const OXLINT_CONFIG_CONTENT = `import { defineConfig } from 'oxlint';
111
+ import ultracite from 'ultracite/oxlint/core';
112
+
113
+ export default defineConfig({
114
+ extends: [ultracite],
115
+ });
116
+ `;
86
117
 
87
118
  const OXFMTRC_CONTENT = `{
88
119
  // ultracite defaults
@@ -260,8 +291,11 @@ const starterImports: Record<
260
291
 
261
292
  const generateAppTs = (name: string, starter: Starter): string => {
262
293
  const { imports, modules } = starterImports[starter];
294
+ const appNameLiteral = JSON.stringify(name);
263
295
  const topoArgs =
264
- modules.length > 0 ? `'${name}', ${modules.join(', ')}` : `'${name}'`;
296
+ modules.length > 0
297
+ ? `${appNameLiteral}, ${modules.join(', ')}`
298
+ : appNameLiteral;
265
299
 
266
300
  return [
267
301
  "import { topo } from '@ontrails/core';",
@@ -296,25 +330,21 @@ const collectScaffoldFiles = (
296
330
  ['package.json', generatePackageJson(name)],
297
331
  ['tsconfig.json', TSCONFIG_CONTENT],
298
332
  ['.gitignore', GITIGNORE_CONTENT],
299
- ['.oxlintrc.json', OXLINTRC_CONTENT],
333
+ ['oxlint.config.ts', OXLINT_CONFIG_CONTENT],
300
334
  ['.oxfmtrc.jsonc', OXFMTRC_CONTENT],
335
+ ['.trails/.gitignore', WORKSPACE_GITIGNORE_CONTENT],
301
336
  ['src/app.ts', generateAppTs(name, starter)],
302
337
  ...starterFileGenerators[starter](),
303
338
  ]);
304
339
 
305
- const writeScaffoldFiles = async (
306
- projectDir: string,
340
+ const collectScaffoldOperations = (
307
341
  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
- };
342
+ ): ProjectWriteOperation[] =>
343
+ [...fileMap].map(([path, content]) => ({
344
+ content,
345
+ kind: 'write' as const,
346
+ path,
347
+ }));
318
348
 
319
349
  // ---------------------------------------------------------------------------
320
350
  // Trail definition
@@ -322,31 +352,67 @@ const writeScaffoldFiles = async (
322
352
 
323
353
  export const createScaffold = trail('create.scaffold', {
324
354
  blaze: async (input) => {
325
- const projectDir = resolve(input.dir ?? '.', input.name);
355
+ const projectDirResult = resolveProjectDir(input.dir ?? '.', input.name);
356
+ if (projectDirResult.isErr()) {
357
+ return Result.err(projectDirResult.error);
358
+ }
359
+
360
+ const projectDir = projectDirResult.value;
326
361
  const starter = (input.starter ?? 'hello') as Starter;
362
+ const dryRun = input.dryRun === true;
327
363
  const fileMap = collectScaffoldFiles(input.name, starter);
328
- const files = await writeScaffoldFiles(projectDir, fileMap);
329
- mkdirSync(join(projectDir, '.trails'), { recursive: true });
364
+ const operations = collectScaffoldOperations(fileMap);
365
+ const plannedOperations = dryRun
366
+ ? planProjectOperations(projectDir, operations)
367
+ : await applyProjectOperations(projectDir, operations);
368
+ if (plannedOperations.isErr()) {
369
+ return Result.err(plannedOperations.error);
370
+ }
371
+
372
+ const created = dryRun ? [] : [...fileMap.keys()];
330
373
 
331
374
  return Result.ok({
332
- created: files,
333
- dir: projectDir,
375
+ created,
376
+ dir: resolve(projectDir),
377
+ dryRun,
334
378
  name: input.name,
379
+ plannedOperations: plannedOperations.value,
335
380
  } satisfies ScaffoldResult);
336
381
  },
337
382
  description: 'Scaffold a new Trails project',
338
383
  input: z.object({
339
384
  dir: z.string().optional().describe('Parent directory'),
340
- name: z.string().describe('Project name'),
385
+ dryRun: z
386
+ .boolean()
387
+ .default(false)
388
+ .describe('Plan scaffold writes without touching the project directory'),
389
+ name: z
390
+ .string()
391
+ .regex(PROJECT_NAME_PATTERN, PROJECT_NAME_MESSAGE)
392
+ .describe('Project name'),
341
393
  starter: z
342
394
  .enum(['hello', 'entity', 'empty'])
343
395
  .default('hello')
344
396
  .describe('Starter trail'),
345
397
  }),
346
- meta: { internal: true },
347
398
  output: z.object({
348
- created: z.array(z.string()),
399
+ created: z
400
+ .array(z.string())
401
+ .describe('Project-relative paths of files written (empty in dry-run)'),
349
402
  dir: z.string(),
403
+ dryRun: z.boolean(),
350
404
  name: z.string(),
405
+ plannedOperations: z.array(
406
+ z.discriminatedUnion('kind', [
407
+ z.object({ kind: z.literal('mkdir'), path: z.string() }),
408
+ z.object({
409
+ from: z.string(),
410
+ kind: z.literal('rename'),
411
+ to: z.string(),
412
+ }),
413
+ z.object({ kind: z.literal('write'), path: z.string() }),
414
+ ])
415
+ ),
351
416
  }),
417
+ visibility: 'internal',
352
418
  });
@@ -1,26 +1,30 @@
1
1
  /**
2
2
  * `create` route -- Create a new Trails project.
3
3
  *
4
- * Composes create.scaffold, add.trailhead, and add.verify sub-trails
5
- * via ctx.cross().
4
+ * Composes create.scaffold, add.surface, and add.verify sub-trails
5
+ * via ctx.cross.
6
6
  */
7
7
 
8
- import type { CrossFn } from '@ontrails/core';
9
- import { Result, trail } from '@ontrails/core';
8
+ import { InternalError, Result, trail } from '@ontrails/core';
10
9
  import { z } from 'zod';
11
10
 
11
+ import {
12
+ PROJECT_NAME_MESSAGE,
13
+ PROJECT_NAME_PATTERN,
14
+ } from '../project-writes.js';
15
+
12
16
  // ---------------------------------------------------------------------------
13
17
  // Helpers
14
18
  // ---------------------------------------------------------------------------
15
19
 
16
20
  type Starter = 'empty' | 'entity' | 'hello';
17
- type Trailhead = 'cli' | 'mcp';
21
+ type Surface = 'cli' | 'http' | 'mcp';
18
22
 
19
23
  interface CreateInput {
20
24
  readonly dir?: string | undefined;
21
25
  readonly name: string;
22
26
  readonly starter: Starter;
23
- readonly trailheads: readonly Trailhead[];
27
+ readonly surfaces: readonly Surface[];
24
28
  readonly verify: boolean;
25
29
  }
26
30
 
@@ -42,15 +46,20 @@ interface ScaffoldedProject {
42
46
  readonly name: string;
43
47
  }
44
48
 
49
+ interface SurfaceResult {
50
+ readonly created: string;
51
+ readonly dependency: string;
52
+ }
53
+
45
54
  const buildScaffoldInput = (input: ScaffoldRequest) => ({
46
55
  ...(input.dir === undefined ? {} : { dir: input.dir }),
47
56
  name: input.name,
48
57
  starter: input.starter,
49
58
  });
50
59
 
51
- const buildTrailheadInput = (dir: string, trailhead: string) => ({
60
+ const buildSurfaceInput = (dir: string, surface: string) => ({
52
61
  dir,
53
- trailhead,
62
+ surface,
54
63
  });
55
64
 
56
65
  const buildVerifyInput = (input: VerifyRequest) => ({
@@ -58,24 +67,14 @@ const buildVerifyInput = (input: VerifyRequest) => ({
58
67
  name: input.name,
59
68
  });
60
69
 
61
- const scaffoldProject = (
62
- cross: CrossFn,
63
- input: ScaffoldRequest
64
- ): Promise<Result<ScaffoldedProject, Error>> =>
65
- cross('create.scaffold', buildScaffoldInput(input));
66
-
67
- const addTrailheadFiles = async (
68
- cross: CrossFn,
69
- dir: string,
70
- trailheads: readonly string[]
70
+ const collectSurfaceFiles = async (
71
+ surfaces: readonly string[],
72
+ addSurface: (surface: string) => Promise<Result<SurfaceResult, Error>>
71
73
  ): Promise<Result<string[], Error>> => {
72
74
  const created: string[] = [];
73
75
 
74
- for (const trailhead of trailheads) {
75
- const result = await cross<{ created: string; dependency: string }>(
76
- 'add.trailhead',
77
- buildTrailheadInput(dir, trailhead)
78
- );
76
+ for (const surface of surfaces) {
77
+ const result = await addSurface(surface);
79
78
  if (result.isErr()) {
80
79
  return Result.err(result.error);
81
80
  }
@@ -86,17 +85,14 @@ const addTrailheadFiles = async (
86
85
  };
87
86
 
88
87
  const collectVerifyFiles = async (
89
- cross: CrossFn,
90
- input: VerifyRequest
88
+ shouldVerify: boolean,
89
+ addVerify: () => Promise<Result<{ created: string[] }, Error>>
91
90
  ): Promise<Result<string[], Error>> => {
92
- if (!input.verify) {
91
+ if (!shouldVerify) {
93
92
  return Result.ok([]);
94
93
  }
95
94
 
96
- const result = await cross<{ created: string[] }>(
97
- 'add.verify',
98
- buildVerifyInput(input)
99
- );
95
+ const result = await addVerify();
100
96
  return result.isErr()
101
97
  ? Result.err(result.error)
102
98
  : Result.ok(result.value.created);
@@ -104,43 +100,9 @@ const collectVerifyFiles = async (
104
100
 
105
101
  const collectCreatedFiles = (
106
102
  scaffolded: readonly string[],
107
- trailheads: readonly string[],
103
+ surfaces: readonly string[],
108
104
  verify: readonly string[]
109
- ): string[] => [...scaffolded, ...trailheads, ...verify];
110
-
111
- const runCreate = async (
112
- cross: CrossFn,
113
- input: CreateInput
114
- ): Promise<Result<{ created: string[]; dir: string; name: string }, Error>> => {
115
- const scaffolded = await scaffoldProject(cross, input);
116
- if (scaffolded.isErr()) {
117
- return Result.err(scaffolded.error);
118
- }
119
-
120
- const trailheadResults = await addTrailheadFiles(
121
- cross,
122
- scaffolded.value.dir,
123
- input.trailheads
124
- );
125
- if (trailheadResults.isErr()) {
126
- return Result.err(trailheadResults.error);
127
- }
128
-
129
- const verifyFiles = await collectVerifyFiles(cross, input);
130
- if (verifyFiles.isErr()) {
131
- return Result.err(verifyFiles.error);
132
- }
133
-
134
- return Result.ok({
135
- created: collectCreatedFiles(
136
- scaffolded.value.created,
137
- trailheadResults.value,
138
- verifyFiles.value
139
- ),
140
- dir: scaffolded.value.dir,
141
- name: input.name,
142
- });
143
- };
105
+ ): string[] => [...scaffolded, ...surfaces, ...verify];
144
106
 
145
107
  // ---------------------------------------------------------------------------
146
108
  // Route definition
@@ -149,11 +111,54 @@ const runCreate = async (
149
111
  export const createRoute = trail('create', {
150
112
  blaze: async (input: CreateInput, ctx) => {
151
113
  if (!ctx.cross) {
152
- return Result.err(new Error('create route requires ctx.cross'));
114
+ return Result.err(new InternalError('create route requires ctx.cross'));
153
115
  }
154
- return await runCreate(ctx.cross, input);
116
+ const { cross } = ctx;
117
+
118
+ const scaffolded = await cross<ScaffoldedProject>(
119
+ 'create.scaffold',
120
+ buildScaffoldInput(input)
121
+ );
122
+ if (scaffolded.isErr()) {
123
+ return Result.err(scaffolded.error);
124
+ }
125
+
126
+ const finishCreate = async (): Promise<
127
+ Result<{ created: string[]; dir: string; name: string }, Error>
128
+ > => {
129
+ const surfaceFiles = await collectSurfaceFiles(
130
+ input.surfaces,
131
+ (surface) =>
132
+ cross<SurfaceResult>(
133
+ 'add.surface',
134
+ buildSurfaceInput(scaffolded.value.dir, surface)
135
+ )
136
+ );
137
+ if (surfaceFiles.isErr()) {
138
+ return Result.err(surfaceFiles.error);
139
+ }
140
+
141
+ const verifyFiles = await collectVerifyFiles(input.verify, () =>
142
+ cross<{ created: string[] }>('add.verify', buildVerifyInput(input))
143
+ );
144
+ if (verifyFiles.isErr()) {
145
+ return Result.err(verifyFiles.error);
146
+ }
147
+
148
+ return Result.ok({
149
+ created: collectCreatedFiles(
150
+ scaffolded.value.created,
151
+ surfaceFiles.value,
152
+ verifyFiles.value
153
+ ),
154
+ dir: scaffolded.value.dir,
155
+ name: input.name,
156
+ });
157
+ };
158
+
159
+ return finishCreate();
155
160
  },
156
- crosses: ['create.scaffold', 'add.trailhead', 'add.verify'],
161
+ crosses: ['create.scaffold', 'add.surface', 'add.verify'],
157
162
  description: 'Create a new Trails project',
158
163
  fields: {
159
164
  starter: {
@@ -171,7 +176,7 @@ export const createRoute = trail('create', {
171
176
  { hint: 'Just the structure', label: 'Empty', value: 'empty' },
172
177
  ],
173
178
  },
174
- trailheads: {
179
+ surfaces: {
175
180
  options: [
176
181
  { hint: 'Commander-based command line', label: 'CLI', value: 'cli' },
177
182
  {
@@ -179,20 +184,28 @@ export const createRoute = trail('create', {
179
184
  label: 'MCP',
180
185
  value: 'mcp',
181
186
  },
187
+ {
188
+ hint: 'Hono-powered HTTP endpoints',
189
+ label: 'HTTP',
190
+ value: 'http',
191
+ },
182
192
  ],
183
193
  },
184
194
  },
185
195
  input: z.object({
186
196
  dir: z.string().optional().describe('Parent directory'),
187
- name: z.string().describe('Project name'),
197
+ name: z
198
+ .string()
199
+ .regex(PROJECT_NAME_PATTERN, PROJECT_NAME_MESSAGE)
200
+ .describe('Project name'),
188
201
  starter: z
189
202
  .enum(['hello', 'entity', 'empty'])
190
203
  .default('hello')
191
204
  .describe('Starter trail'),
192
- trailheads: z
193
- .array(z.enum(['cli', 'mcp']))
205
+ surfaces: z
206
+ .array(z.enum(['cli', 'http', 'mcp']))
194
207
  .default(['cli'])
195
- .describe('Trailheads'),
208
+ .describe('Surfaces'),
196
209
  verify: z.boolean().default(true).describe('Include testing + warden'),
197
210
  }),
198
211
  output: z.object({
@@ -1,8 +1,12 @@
1
1
  import { Result, ValidationError, trail } from '@ontrails/core';
2
2
  import { z } from 'zod';
3
3
 
4
- import { cleanDevState, DEFAULT_TOPO_SAVE_RETENTION } from './dev-support.js';
5
- import { isolatedExampleInput } from './topo-support.js';
4
+ import {
5
+ cleanDevState,
6
+ DEFAULT_TOPO_SNAPSHOT_RETENTION,
7
+ } from './dev-support.js';
8
+ import { resolveTrailRootDir } from './root-dir.js';
9
+ import { createIsolatedExampleInput } from './topo-support.js';
6
10
 
7
11
  export const devCleanTrail = trail('dev.clean', {
8
12
  blaze: (input, ctx) => {
@@ -14,23 +18,27 @@ export const devCleanTrail = trail('dev.clean', {
14
18
  );
15
19
  }
16
20
 
17
- const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
21
+ const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
22
+ if (rootDirResult.isErr()) {
23
+ return Result.err(rootDirResult.error);
24
+ }
25
+ const rootDir = rootDirResult.value;
18
26
  return Result.ok(
19
27
  cleanDevState({
20
28
  dryRun: input.dryRun,
21
- maxAge: input.trackAgeMs,
22
- maxRecords: input.tracks,
29
+ maxAge: input.traceAgeMs,
30
+ maxRecords: input.traces,
23
31
  rootDir,
24
- saveRetention: input.saves,
32
+ snapshotRetention: input.snapshots,
25
33
  })
26
34
  );
27
35
  },
28
- description: 'Prune unpinned topo saves and old track records',
36
+ description: 'Prune unpinned topo snapshots and old trace records',
29
37
  examples: [
30
38
  {
31
39
  input: {
32
40
  dryRun: true,
33
- rootDir: isolatedExampleInput('dev-clean').rootDir,
41
+ rootDir: createIsolatedExampleInput('dev-clean').rootDir,
34
42
  },
35
43
  name: 'Preview local cleanup',
36
44
  },
@@ -41,33 +49,34 @@ export const devCleanTrail = trail('dev.clean', {
41
49
  .default(true)
42
50
  .describe('Preview cleanup without changing state'),
43
51
  rootDir: z.string().optional().describe('Workspace root directory'),
44
- saves: z
52
+ snapshots: z
45
53
  .number()
46
- .default(DEFAULT_TOPO_SAVE_RETENTION)
47
- .describe('Unpinned topo saves to retain'),
48
- trackAgeMs: z
54
+ .default(DEFAULT_TOPO_SNAPSHOT_RETENTION)
55
+ .describe('Unpinned topo snapshots to retain'),
56
+ traceAgeMs: z
49
57
  .number()
50
58
  .default(7 * 24 * 60 * 60 * 1000)
51
- .describe('Maximum retained track age in milliseconds'),
52
- tracks: z.number().default(10_000).describe('Maximum retained track count'),
59
+ .describe('Maximum retained trace age in milliseconds'),
60
+ traces: z.number().default(10_000).describe('Maximum retained trace count'),
53
61
  yes: z.boolean().default(false).describe('Confirm destructive changes'),
54
62
  }),
55
63
  intent: 'destroy',
56
64
  output: z.object({
57
65
  dryRun: z.boolean(),
58
66
  remaining: z.object({
59
- pinCount: z.number(),
60
- saveCount: z.number(),
61
- trackCount: z.number(),
67
+ pinnedCount: z.number(),
68
+ snapshotCount: z.number(),
69
+ traceCount: z.number(),
62
70
  }),
63
71
  removed: z.object({
64
- topoSaves: z.number(),
65
- trackRecords: z.number(),
72
+ topoSnapshots: z.number(),
73
+ traceRecords: z.number(),
66
74
  }),
67
75
  retention: z.object({
68
- saves: z.number(),
69
- trackAgeMs: z.number(),
70
- tracks: z.number(),
76
+ snapshots: z.number(),
77
+ traceAgeMs: z.number(),
78
+ traces: z.number(),
71
79
  }),
72
80
  }),
81
+ permit: { scopes: ['dev:clean'] },
73
82
  });
@@ -2,7 +2,8 @@ import { Result, ValidationError, trail } from '@ontrails/core';
2
2
  import { z } from 'zod';
3
3
 
4
4
  import { resetDevState } from './dev-support.js';
5
- import { isolatedExampleInput } from './topo-support.js';
5
+ import { resolveTrailRootDir } from './root-dir.js';
6
+ import { createIsolatedExampleInput } from './topo-support.js';
6
7
 
7
8
  export const devResetTrail = trail('dev.reset', {
8
9
  blaze: (input, ctx) => {
@@ -14,7 +15,11 @@ export const devResetTrail = trail('dev.reset', {
14
15
  );
15
16
  }
16
17
 
17
- const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
18
+ const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
19
+ if (rootDirResult.isErr()) {
20
+ return Result.err(rootDirResult.error);
21
+ }
22
+ const rootDir = rootDirResult.value;
18
23
  return Result.ok(resetDevState({ dryRun: input.dryRun, rootDir }));
19
24
  },
20
25
  description: 'Remove local Trails database artifacts',
@@ -22,7 +27,7 @@ export const devResetTrail = trail('dev.reset', {
22
27
  {
23
28
  input: {
24
29
  dryRun: true,
25
- rootDir: isolatedExampleInput('dev-reset').rootDir,
30
+ rootDir: createIsolatedExampleInput('dev-reset').rootDir,
26
31
  },
27
32
  name: 'Preview local reset',
28
33
  },
@@ -41,4 +46,5 @@ export const devResetTrail = trail('dev.reset', {
41
46
  removedCount: z.number(),
42
47
  removedFiles: z.array(z.string()),
43
48
  }),
49
+ permit: { scopes: ['dev:reset'] },
44
50
  });