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

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 (165) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +29 -0
  3. package/__tests__/examples.test.ts +39 -0
  4. package/dist/src/app.d.ts.map +1 -1
  5. package/dist/src/app.js +12 -1
  6. package/dist/src/app.js.map +1 -1
  7. package/dist/src/cli.js +4 -3
  8. package/dist/src/cli.js.map +1 -1
  9. package/dist/src/trails/add-surface.d.ts +3 -3
  10. package/dist/src/trails/add-surface.d.ts.map +1 -1
  11. package/dist/src/trails/add-surface.js +46 -24
  12. package/dist/src/trails/add-surface.js.map +1 -1
  13. package/dist/src/trails/add-trail.d.ts +3 -1
  14. package/dist/src/trails/add-trail.d.ts.map +1 -1
  15. package/dist/src/trails/add-trail.js +49 -22
  16. package/dist/src/trails/add-trail.js.map +1 -1
  17. package/dist/src/trails/add-trailhead.d.ts +13 -0
  18. package/dist/src/trails/add-trailhead.d.ts.map +1 -0
  19. package/dist/src/trails/add-trailhead.js +88 -0
  20. package/dist/src/trails/add-trailhead.js.map +1 -0
  21. package/dist/src/trails/add-verify.d.ts +1 -1
  22. package/dist/src/trails/add-verify.d.ts.map +1 -1
  23. package/dist/src/trails/add-verify.js +17 -16
  24. package/dist/src/trails/add-verify.js.map +1 -1
  25. package/dist/src/trails/create-scaffold.d.ts +1 -1
  26. package/dist/src/trails/create-scaffold.d.ts.map +1 -1
  27. package/dist/src/trails/create-scaffold.js +34 -27
  28. package/dist/src/trails/create-scaffold.js.map +1 -1
  29. package/dist/src/trails/create.d.ts +9 -13
  30. package/dist/src/trails/create.d.ts.map +1 -1
  31. package/dist/src/trails/create.js +40 -35
  32. package/dist/src/trails/create.js.map +1 -1
  33. package/dist/src/trails/dev-clean.d.ts +9 -0
  34. package/dist/src/trails/dev-clean.d.ts.map +1 -0
  35. package/dist/src/trails/dev-clean.js +66 -0
  36. package/dist/src/trails/dev-clean.js.map +1 -0
  37. package/dist/src/trails/dev-reset.d.ts +6 -0
  38. package/dist/src/trails/dev-reset.d.ts.map +1 -0
  39. package/dist/src/trails/dev-reset.js +39 -0
  40. package/dist/src/trails/dev-reset.js.map +1 -0
  41. package/dist/src/trails/dev-stats.d.ts +7 -0
  42. package/dist/src/trails/dev-stats.d.ts.map +1 -0
  43. package/dist/src/trails/dev-stats.js +61 -0
  44. package/dist/src/trails/dev-stats.js.map +1 -0
  45. package/dist/src/trails/dev-support.d.ts +64 -0
  46. package/dist/src/trails/dev-support.d.ts.map +1 -0
  47. package/dist/src/trails/dev-support.js +181 -0
  48. package/dist/src/trails/dev-support.js.map +1 -0
  49. package/dist/src/trails/draft-promote.d.ts +18 -0
  50. package/dist/src/trails/draft-promote.d.ts.map +1 -0
  51. package/dist/src/trails/draft-promote.js +400 -0
  52. package/dist/src/trails/draft-promote.js.map +1 -0
  53. package/dist/src/trails/guide.d.ts +14 -4
  54. package/dist/src/trails/guide.d.ts.map +1 -1
  55. package/dist/src/trails/guide.js +22 -41
  56. package/dist/src/trails/guide.js.map +1 -1
  57. package/dist/src/trails/load-app.d.ts +9 -1
  58. package/dist/src/trails/load-app.d.ts.map +1 -1
  59. package/dist/src/trails/load-app.js +404 -13
  60. package/dist/src/trails/load-app.js.map +1 -1
  61. package/dist/src/trails/project.d.ts.map +1 -1
  62. package/dist/src/trails/project.js +14 -3
  63. package/dist/src/trails/project.js.map +1 -1
  64. package/dist/src/trails/survey.d.ts +6 -60
  65. package/dist/src/trails/survey.d.ts.map +1 -1
  66. package/dist/src/trails/survey.js +83 -182
  67. package/dist/src/trails/survey.js.map +1 -1
  68. package/dist/src/trails/topo-constants.d.ts +3 -0
  69. package/dist/src/trails/topo-constants.d.ts.map +1 -0
  70. package/dist/src/trails/topo-constants.js +3 -0
  71. package/dist/src/trails/topo-constants.js.map +1 -0
  72. package/dist/src/trails/topo-export.d.ts +19 -0
  73. package/dist/src/trails/topo-export.d.ts.map +1 -0
  74. package/dist/src/trails/topo-export.js +31 -0
  75. package/dist/src/trails/topo-export.js.map +1 -0
  76. package/dist/src/trails/topo-history.d.ts +20 -0
  77. package/dist/src/trails/topo-history.d.ts.map +1 -0
  78. package/dist/src/trails/topo-history.js +32 -0
  79. package/dist/src/trails/topo-history.js.map +1 -0
  80. package/dist/src/trails/topo-pin.d.ts +17 -0
  81. package/dist/src/trails/topo-pin.d.ts.map +1 -0
  82. package/dist/src/trails/topo-pin.js +31 -0
  83. package/dist/src/trails/topo-pin.js.map +1 -0
  84. package/dist/src/trails/topo-read-support.d.ts +58 -0
  85. package/dist/src/trails/topo-read-support.d.ts.map +1 -0
  86. package/dist/src/trails/topo-read-support.js +167 -0
  87. package/dist/src/trails/topo-read-support.js.map +1 -0
  88. package/dist/src/trails/topo-reports.d.ts +54 -0
  89. package/dist/src/trails/topo-reports.d.ts.map +1 -0
  90. package/dist/src/trails/topo-reports.js +128 -0
  91. package/dist/src/trails/topo-reports.js.map +1 -0
  92. package/dist/src/trails/topo-show.d.ts +23 -0
  93. package/dist/src/trails/topo-show.d.ts.map +1 -0
  94. package/dist/src/trails/topo-show.js +49 -0
  95. package/dist/src/trails/topo-show.js.map +1 -0
  96. package/dist/src/trails/topo-store-support.d.ts +13 -0
  97. package/dist/src/trails/topo-store-support.d.ts.map +1 -0
  98. package/dist/src/trails/topo-store-support.js +55 -0
  99. package/dist/src/trails/topo-store-support.js.map +1 -0
  100. package/dist/src/trails/topo-support.d.ts +76 -0
  101. package/dist/src/trails/topo-support.d.ts.map +1 -0
  102. package/dist/src/trails/topo-support.js +132 -0
  103. package/dist/src/trails/topo-support.js.map +1 -0
  104. package/dist/src/trails/topo-unpin.d.ts +20 -0
  105. package/dist/src/trails/topo-unpin.d.ts.map +1 -0
  106. package/dist/src/trails/topo-unpin.js +44 -0
  107. package/dist/src/trails/topo-unpin.js.map +1 -0
  108. package/dist/src/trails/topo-verify.d.ts +5 -0
  109. package/dist/src/trails/topo-verify.d.ts.map +1 -0
  110. package/dist/src/trails/topo-verify.js +24 -0
  111. package/dist/src/trails/topo-verify.js.map +1 -0
  112. package/dist/src/trails/topo.d.ts +5 -0
  113. package/dist/src/trails/topo.d.ts.map +1 -0
  114. package/dist/src/trails/topo.js +63 -0
  115. package/dist/src/trails/topo.js.map +1 -0
  116. package/dist/src/trails/warden.d.ts +3 -2
  117. package/dist/src/trails/warden.d.ts.map +1 -1
  118. package/dist/src/trails/warden.js +37 -27
  119. package/dist/src/trails/warden.js.map +1 -1
  120. package/dist/src/versions.d.ts +12 -0
  121. package/dist/src/versions.d.ts.map +1 -0
  122. package/dist/src/versions.js +23 -0
  123. package/dist/src/versions.js.map +1 -0
  124. package/dist/tsconfig.tsbuildinfo +1 -1
  125. package/package.json +8 -7
  126. package/src/__tests__/add-trail.test.ts +97 -0
  127. package/src/__tests__/create.test.ts +91 -27
  128. package/src/__tests__/draft-promote.test.ts +144 -0
  129. package/src/__tests__/guide.test.ts +10 -5
  130. package/src/__tests__/load-app.test.ts +406 -2
  131. package/src/__tests__/survey.test.ts +221 -60
  132. package/src/__tests__/topo-dev.test.ts +426 -0
  133. package/src/app.ts +24 -2
  134. package/src/clack.ts +1 -1
  135. package/src/cli.ts +4 -3
  136. package/src/trails/add-surface.ts +150 -0
  137. package/src/trails/add-trail.ts +46 -10
  138. package/src/trails/add-verify.ts +11 -6
  139. package/src/trails/create-scaffold.ts +16 -3
  140. package/src/trails/create.ts +76 -71
  141. package/src/trails/dev-clean.ts +77 -0
  142. package/src/trails/dev-reset.ts +45 -0
  143. package/src/trails/dev-stats.ts +67 -0
  144. package/src/trails/dev-support.ts +328 -0
  145. package/src/trails/draft-promote.ts +739 -0
  146. package/src/trails/guide.ts +23 -41
  147. package/src/trails/load-app.ts +556 -14
  148. package/src/trails/project.ts +17 -3
  149. package/src/trails/survey.ts +110 -285
  150. package/src/trails/topo-constants.ts +2 -0
  151. package/src/trails/topo-export.ts +35 -0
  152. package/src/trails/topo-history.ts +38 -0
  153. package/src/trails/topo-pin.ts +38 -0
  154. package/src/trails/topo-read-support.ts +329 -0
  155. package/src/trails/topo-reports.ts +228 -0
  156. package/src/trails/topo-show.ts +54 -0
  157. package/src/trails/topo-store-support.ts +104 -0
  158. package/src/trails/topo-support.ts +230 -0
  159. package/src/trails/topo-unpin.ts +56 -0
  160. package/src/trails/topo-verify.ts +25 -0
  161. package/src/trails/topo.ts +69 -0
  162. package/src/trails/warden.ts +13 -3
  163. package/src/versions.ts +43 -0
  164. package/tsconfig.tests.json +10 -0
  165. package/src/trails/add-trailhead.ts +0 -121
@@ -12,40 +12,53 @@ import { z } from 'zod';
12
12
  // Helpers
13
13
  // ---------------------------------------------------------------------------
14
14
 
15
+ const literal = (value: string): string => JSON.stringify(value);
16
+
17
+ const deriveExampleMessage = (id: string): string => `${id} completed`;
18
+
15
19
  const generateTrailFile = (
16
20
  id: string,
21
+ description: string,
22
+ exampleName: string,
17
23
  intent: 'read' | 'write' | 'destroy'
18
24
  ): string => {
19
25
  const intentLine = intent === 'write' ? '' : `\n intent: '${intent}',`;
26
+ const exampleMessage = deriveExampleMessage(id);
20
27
 
21
28
  return `import { Result, trail } from '@ontrails/core';
22
29
  import { z } from 'zod';
23
30
 
24
31
  export const ${id.replaceAll('.', '_')} = trail('${id}', {
25
- description: 'TODO: describe this trail',
32
+ blaze: async () => {
33
+ return Result.ok({ message: ${literal(exampleMessage)} });
34
+ },
35
+ description: ${literal(description)},
26
36
  examples: [
27
37
  {
38
+ expected: { message: ${literal(exampleMessage)} },
28
39
  input: {},
29
- name: 'TODO: add example',
40
+ name: ${literal(exampleName)},
30
41
  },
31
42
  ],
32
- blaze: async (input) => {
33
- return Result.ok({ message: 'TODO' });
34
- },
35
43
  input: z.object({}),${intentLine}
36
44
  output: z.object({ message: z.string() }),
37
45
  });
38
46
  `;
39
47
  };
40
48
 
41
- const generateTestFile = (id: string): string => {
49
+ const generateTestFile = (id: string, exampleName: string): string => {
42
50
  const moduleName = id.replaceAll('.', '-');
43
51
  const trailName = id.replaceAll('.', '_');
52
+ const exampleMessage = deriveExampleMessage(id);
44
53
  return `import { testTrail } from '@ontrails/testing';
45
54
  import { ${trailName} } from '../src/trails/${moduleName}.js';
46
55
 
47
56
  testTrail(${trailName}, [
48
- { description: 'basic test', input: {}, expectOk: true },
57
+ {
58
+ description: ${literal(exampleName)},
59
+ expectValue: { message: ${literal(exampleMessage)} },
60
+ input: {},
61
+ },
49
62
  ]);
50
63
  `;
51
64
  };
@@ -64,14 +77,26 @@ const writeWithDirs = async (
64
77
  };
65
78
 
66
79
  export const addTrail = trail('add.trail', {
80
+ args: ['id'],
67
81
  blaze: async (input, ctx) => {
68
82
  const { id } = input;
69
83
  const moduleName = id.replaceAll('.', '-');
70
84
  const cwd = resolve(ctx.cwd ?? '.');
71
85
 
72
86
  const files = new Map<string, string>([
73
- [`src/trails/${moduleName}.ts`, generateTrailFile(id, input.intent)],
74
- [`__tests__/${moduleName}.test.ts`, generateTestFile(id)],
87
+ [
88
+ `src/trails/${moduleName}.ts`,
89
+ generateTrailFile(
90
+ id,
91
+ input.description,
92
+ input.exampleName,
93
+ input.intent
94
+ ),
95
+ ],
96
+ [
97
+ `__tests__/${moduleName}.test.ts`,
98
+ generateTestFile(id, input.exampleName),
99
+ ],
75
100
  ]);
76
101
 
77
102
  for (const [relativePath, content] of files) {
@@ -82,7 +107,18 @@ export const addTrail = trail('add.trail', {
82
107
  },
83
108
  description: 'Scaffold a new trail with tests and examples',
84
109
  input: z.object({
85
- id: z.string().describe('Trail ID (e.g., entity.update)'),
110
+ description: z
111
+ .string()
112
+ .min(1, 'Trail description is required')
113
+ .describe('Trail description'),
114
+ exampleName: z
115
+ .string()
116
+ .min(1, 'Starter example name is required')
117
+ .describe('Starter example name'),
118
+ id: z
119
+ .string()
120
+ .min(1, 'Trail ID is required')
121
+ .describe('Trail ID (e.g., entity.update)'),
86
122
  intent: z
87
123
  .enum(['read', 'write', 'destroy'])
88
124
  .default('write')
@@ -8,30 +8,35 @@ import { dirname, join, resolve } from 'node:path';
8
8
  import { Result, trail } from '@ontrails/core';
9
9
  import { z } from 'zod';
10
10
 
11
+ import {
12
+ ontrailsPackageRange,
13
+ scaffoldDependencyVersions,
14
+ } from '../versions.js';
15
+
11
16
  // ---------------------------------------------------------------------------
12
17
  // Content generators
13
18
  // ---------------------------------------------------------------------------
14
19
 
15
20
  const generateTestFile = (): string =>
16
- `import { testAll } from '@ontrails/testing';
21
+ `import { testAllEstablished } from '@ontrails/testing';
17
22
  import { app } from '../src/app.js';
18
23
 
19
- testAll(app);
24
+ testAllEstablished(app);
20
25
  `;
21
26
 
22
27
  const generateLefthookYml = (): string =>
23
28
  `pre-push:
24
29
  commands:
25
30
  warden:
26
- run: bunx trails warden --exit-code
31
+ run: bunx trails warden
27
32
  `;
28
33
 
29
34
  /** Add testing and warden devDependencies to package.json when present. */
30
35
  const patchVerifyDeps = (pkg: Record<string, unknown>): void => {
31
36
  const devDeps = (pkg['devDependencies'] ?? {}) as Record<string, string>;
32
- devDeps['@ontrails/testing'] = 'workspace:*';
33
- devDeps['@ontrails/warden'] = 'workspace:*';
34
- devDeps['lefthook'] = '^2.1.1';
37
+ devDeps['@ontrails/testing'] = ontrailsPackageRange;
38
+ devDeps['@ontrails/warden'] = ontrailsPackageRange;
39
+ devDeps['lefthook'] = scaffoldDependencyVersions.lefthook;
35
40
  pkg['devDependencies'] = Object.fromEntries(
36
41
  Object.entries(devDeps).toSorted(([a], [b]) => a.localeCompare(b))
37
42
  );
@@ -10,6 +10,11 @@ import { dirname, join, resolve } from 'node:path';
10
10
  import { Result, trail } from '@ontrails/core';
11
11
  import { z } from 'zod';
12
12
 
13
+ import {
14
+ ontrailsPackageRange,
15
+ scaffoldDependencyVersions,
16
+ } from '../versions.js';
17
+
13
18
  // ---------------------------------------------------------------------------
14
19
  // Types
15
20
  // ---------------------------------------------------------------------------
@@ -28,14 +33,22 @@ interface ScaffoldResult {
28
33
 
29
34
  const generatePackageJson = (name: string): string => {
30
35
  const deps: Record<string, string> = {
31
- '@ontrails/core': 'workspace:*',
32
- zod: '^4.0.0',
36
+ '@ontrails/core': ontrailsPackageRange,
37
+ zod: scaffoldDependencyVersions.zod,
33
38
  };
34
39
 
35
40
  const pkg: Record<string, unknown> = {
36
41
  dependencies: Object.fromEntries(
37
42
  Object.entries(deps).toSorted(([a], [b]) => a.localeCompare(b))
38
43
  ),
44
+ devDependencies: Object.fromEntries(
45
+ Object.entries({
46
+ '@types/bun': scaffoldDependencyVersions.bunTypes,
47
+ oxlint: scaffoldDependencyVersions.oxlint,
48
+ typescript: scaffoldDependencyVersions.typescript,
49
+ ultracite: scaffoldDependencyVersions.ultracite,
50
+ }).toSorted(([a], [b]) => a.localeCompare(b))
51
+ ),
39
52
  name,
40
53
  scripts: {
41
54
  build: 'tsc -b',
@@ -73,7 +86,7 @@ const TSCONFIG_CONTENT = JSON.stringify(
73
86
  const GITIGNORE_CONTENT = `node_modules/
74
87
  dist/
75
88
  *.tsbuildinfo
76
- .trails/_trailhead.json
89
+ .trails/_surface.json
77
90
  `;
78
91
 
79
92
  const OXLINTRC_CONTENT = JSON.stringify(
@@ -1,11 +1,10 @@
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
8
  import { Result, trail } from '@ontrails/core';
10
9
  import { z } from 'zod';
11
10
 
@@ -14,13 +13,13 @@ import { z } from 'zod';
14
13
  // ---------------------------------------------------------------------------
15
14
 
16
15
  type Starter = 'empty' | 'entity' | 'hello';
17
- type Trailhead = 'cli' | 'mcp';
16
+ type Surface = 'cli' | 'http' | 'mcp';
18
17
 
19
18
  interface CreateInput {
20
19
  readonly dir?: string | undefined;
21
20
  readonly name: string;
22
21
  readonly starter: Starter;
23
- readonly trailheads: readonly Trailhead[];
22
+ readonly surfaces: readonly Surface[];
24
23
  readonly verify: boolean;
25
24
  }
26
25
 
@@ -42,15 +41,20 @@ interface ScaffoldedProject {
42
41
  readonly name: string;
43
42
  }
44
43
 
44
+ interface SurfaceResult {
45
+ readonly created: string;
46
+ readonly dependency: string;
47
+ }
48
+
45
49
  const buildScaffoldInput = (input: ScaffoldRequest) => ({
46
50
  ...(input.dir === undefined ? {} : { dir: input.dir }),
47
51
  name: input.name,
48
52
  starter: input.starter,
49
53
  });
50
54
 
51
- const buildTrailheadInput = (dir: string, trailhead: string) => ({
55
+ const buildSurfaceInput = (dir: string, surface: string) => ({
52
56
  dir,
53
- trailhead,
57
+ surface,
54
58
  });
55
59
 
56
60
  const buildVerifyInput = (input: VerifyRequest) => ({
@@ -58,24 +62,14 @@ const buildVerifyInput = (input: VerifyRequest) => ({
58
62
  name: input.name,
59
63
  });
60
64
 
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[]
65
+ const collectSurfaceFiles = async (
66
+ surfaces: readonly string[],
67
+ addSurface: (surface: string) => Promise<Result<SurfaceResult, Error>>
71
68
  ): Promise<Result<string[], Error>> => {
72
69
  const created: string[] = [];
73
70
 
74
- for (const trailhead of trailheads) {
75
- const result = await cross<{ created: string; dependency: string }>(
76
- 'add.trailhead',
77
- buildTrailheadInput(dir, trailhead)
78
- );
71
+ for (const surface of surfaces) {
72
+ const result = await addSurface(surface);
79
73
  if (result.isErr()) {
80
74
  return Result.err(result.error);
81
75
  }
@@ -86,17 +80,14 @@ const addTrailheadFiles = async (
86
80
  };
87
81
 
88
82
  const collectVerifyFiles = async (
89
- cross: CrossFn,
90
- input: VerifyRequest
83
+ shouldVerify: boolean,
84
+ addVerify: () => Promise<Result<{ created: string[] }, Error>>
91
85
  ): Promise<Result<string[], Error>> => {
92
- if (!input.verify) {
86
+ if (!shouldVerify) {
93
87
  return Result.ok([]);
94
88
  }
95
89
 
96
- const result = await cross<{ created: string[] }>(
97
- 'add.verify',
98
- buildVerifyInput(input)
99
- );
90
+ const result = await addVerify();
100
91
  return result.isErr()
101
92
  ? Result.err(result.error)
102
93
  : Result.ok(result.value.created);
@@ -104,43 +95,9 @@ const collectVerifyFiles = async (
104
95
 
105
96
  const collectCreatedFiles = (
106
97
  scaffolded: readonly string[],
107
- trailheads: readonly string[],
98
+ surfaces: readonly string[],
108
99
  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
- };
100
+ ): string[] => [...scaffolded, ...surfaces, ...verify];
144
101
 
145
102
  // ---------------------------------------------------------------------------
146
103
  // Route definition
@@ -151,9 +108,52 @@ export const createRoute = trail('create', {
151
108
  if (!ctx.cross) {
152
109
  return Result.err(new Error('create route requires ctx.cross'));
153
110
  }
154
- return await runCreate(ctx.cross, input);
111
+ const { cross } = ctx;
112
+
113
+ const scaffolded = await cross<ScaffoldedProject>(
114
+ 'create.scaffold',
115
+ buildScaffoldInput(input)
116
+ );
117
+ if (scaffolded.isErr()) {
118
+ return Result.err(scaffolded.error);
119
+ }
120
+
121
+ const finishCreate = async (): Promise<
122
+ Result<{ created: string[]; dir: string; name: string }, Error>
123
+ > => {
124
+ const surfaceFiles = await collectSurfaceFiles(
125
+ input.surfaces,
126
+ (surface) =>
127
+ cross<SurfaceResult>(
128
+ 'add.surface',
129
+ buildSurfaceInput(scaffolded.value.dir, surface)
130
+ )
131
+ );
132
+ if (surfaceFiles.isErr()) {
133
+ return Result.err(surfaceFiles.error);
134
+ }
135
+
136
+ const verifyFiles = await collectVerifyFiles(input.verify, () =>
137
+ cross<{ created: string[] }>('add.verify', buildVerifyInput(input))
138
+ );
139
+ if (verifyFiles.isErr()) {
140
+ return Result.err(verifyFiles.error);
141
+ }
142
+
143
+ return Result.ok({
144
+ created: collectCreatedFiles(
145
+ scaffolded.value.created,
146
+ surfaceFiles.value,
147
+ verifyFiles.value
148
+ ),
149
+ dir: scaffolded.value.dir,
150
+ name: input.name,
151
+ });
152
+ };
153
+
154
+ return finishCreate();
155
155
  },
156
- crosses: ['create.scaffold', 'add.trailhead', 'add.verify'],
156
+ crosses: ['create.scaffold', 'add.surface', 'add.verify'],
157
157
  description: 'Create a new Trails project',
158
158
  fields: {
159
159
  starter: {
@@ -171,7 +171,7 @@ export const createRoute = trail('create', {
171
171
  { hint: 'Just the structure', label: 'Empty', value: 'empty' },
172
172
  ],
173
173
  },
174
- trailheads: {
174
+ surfaces: {
175
175
  options: [
176
176
  { hint: 'Commander-based command line', label: 'CLI', value: 'cli' },
177
177
  {
@@ -179,6 +179,11 @@ export const createRoute = trail('create', {
179
179
  label: 'MCP',
180
180
  value: 'mcp',
181
181
  },
182
+ {
183
+ hint: 'Hono-powered HTTP endpoints',
184
+ label: 'HTTP',
185
+ value: 'http',
186
+ },
182
187
  ],
183
188
  },
184
189
  },
@@ -189,10 +194,10 @@ export const createRoute = trail('create', {
189
194
  .enum(['hello', 'entity', 'empty'])
190
195
  .default('hello')
191
196
  .describe('Starter trail'),
192
- trailheads: z
193
- .array(z.enum(['cli', 'mcp']))
197
+ surfaces: z
198
+ .array(z.enum(['cli', 'http', 'mcp']))
194
199
  .default(['cli'])
195
- .describe('Trailheads'),
200
+ .describe('Surfaces'),
196
201
  verify: z.boolean().default(true).describe('Include testing + warden'),
197
202
  }),
198
203
  output: z.object({
@@ -0,0 +1,77 @@
1
+ import { Result, ValidationError, trail } from '@ontrails/core';
2
+ import { z } from 'zod';
3
+
4
+ import {
5
+ cleanDevState,
6
+ DEFAULT_TOPO_SNAPSHOT_RETENTION,
7
+ } from './dev-support.js';
8
+ import { createIsolatedExampleInput } from './topo-support.js';
9
+
10
+ export const devCleanTrail = trail('dev.clean', {
11
+ blaze: (input, ctx) => {
12
+ if (input.dryRun !== true && input.yes !== true) {
13
+ return Result.err(
14
+ new ValidationError(
15
+ 'Refusing to clean local state without `--yes` or `--dry-run`.'
16
+ )
17
+ );
18
+ }
19
+
20
+ const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
21
+ return Result.ok(
22
+ cleanDevState({
23
+ dryRun: input.dryRun,
24
+ maxAge: input.traceAgeMs,
25
+ maxRecords: input.traces,
26
+ rootDir,
27
+ snapshotRetention: input.snapshots,
28
+ })
29
+ );
30
+ },
31
+ description: 'Prune unpinned topo snapshots and old trace records',
32
+ examples: [
33
+ {
34
+ input: {
35
+ dryRun: true,
36
+ rootDir: createIsolatedExampleInput('dev-clean').rootDir,
37
+ },
38
+ name: 'Preview local cleanup',
39
+ },
40
+ ],
41
+ input: z.object({
42
+ dryRun: z
43
+ .boolean()
44
+ .default(true)
45
+ .describe('Preview cleanup without changing state'),
46
+ rootDir: z.string().optional().describe('Workspace root directory'),
47
+ snapshots: z
48
+ .number()
49
+ .default(DEFAULT_TOPO_SNAPSHOT_RETENTION)
50
+ .describe('Unpinned topo snapshots to retain'),
51
+ traceAgeMs: z
52
+ .number()
53
+ .default(7 * 24 * 60 * 60 * 1000)
54
+ .describe('Maximum retained trace age in milliseconds'),
55
+ traces: z.number().default(10_000).describe('Maximum retained trace count'),
56
+ yes: z.boolean().default(false).describe('Confirm destructive changes'),
57
+ }),
58
+ intent: 'destroy',
59
+ output: z.object({
60
+ dryRun: z.boolean(),
61
+ remaining: z.object({
62
+ pinnedCount: z.number(),
63
+ snapshotCount: z.number(),
64
+ traceCount: z.number(),
65
+ }),
66
+ removed: z.object({
67
+ topoSnapshots: z.number(),
68
+ traceRecords: z.number(),
69
+ }),
70
+ retention: z.object({
71
+ snapshots: z.number(),
72
+ traceAgeMs: z.number(),
73
+ traces: z.number(),
74
+ }),
75
+ }),
76
+ permit: { scopes: ['dev:clean'] },
77
+ });
@@ -0,0 +1,45 @@
1
+ import { Result, ValidationError, trail } from '@ontrails/core';
2
+ import { z } from 'zod';
3
+
4
+ import { resetDevState } from './dev-support.js';
5
+ import { createIsolatedExampleInput } from './topo-support.js';
6
+
7
+ export const devResetTrail = trail('dev.reset', {
8
+ blaze: (input, ctx) => {
9
+ if (input.dryRun !== true && input.yes !== true) {
10
+ return Result.err(
11
+ new ValidationError(
12
+ 'Refusing to reset local state without `--yes` or `--dry-run`.'
13
+ )
14
+ );
15
+ }
16
+
17
+ const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
18
+ return Result.ok(resetDevState({ dryRun: input.dryRun, rootDir }));
19
+ },
20
+ description: 'Remove local Trails database artifacts',
21
+ examples: [
22
+ {
23
+ input: {
24
+ dryRun: true,
25
+ rootDir: createIsolatedExampleInput('dev-reset').rootDir,
26
+ },
27
+ name: 'Preview local reset',
28
+ },
29
+ ],
30
+ input: z.object({
31
+ dryRun: z
32
+ .boolean()
33
+ .default(true)
34
+ .describe('Preview reset without changing state'),
35
+ rootDir: z.string().optional().describe('Workspace root directory'),
36
+ yes: z.boolean().default(false).describe('Confirm destructive changes'),
37
+ }),
38
+ intent: 'destroy',
39
+ output: z.object({
40
+ dryRun: z.boolean(),
41
+ removedCount: z.number(),
42
+ removedFiles: z.array(z.string()),
43
+ }),
44
+ permit: { scopes: ['dev:reset'] },
45
+ });
@@ -0,0 +1,67 @@
1
+ import { Result, trail } from '@ontrails/core';
2
+ import { z } from 'zod';
3
+
4
+ import {
5
+ buildDevStats,
6
+ DEFAULT_TOPO_SNAPSHOT_RETENTION,
7
+ } from './dev-support.js';
8
+ import { createIsolatedExampleInput } from './topo-support.js';
9
+
10
+ export const devStatsTrail = trail('dev.stats', {
11
+ blaze: (input, ctx) => {
12
+ const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
13
+ return Result.ok(
14
+ buildDevStats({
15
+ maxAge: input.traceAgeMs,
16
+ maxRecords: input.traces,
17
+ rootDir,
18
+ snapshotRetention: input.snapshots,
19
+ })
20
+ );
21
+ },
22
+ description: 'Show local Trails workspace state and retention',
23
+ examples: [
24
+ {
25
+ input: { rootDir: createIsolatedExampleInput('dev-stats').rootDir },
26
+ name: 'Show local dev state',
27
+ },
28
+ ],
29
+ input: z.object({
30
+ rootDir: z.string().optional().describe('Workspace root directory'),
31
+ snapshots: z
32
+ .number()
33
+ .default(DEFAULT_TOPO_SNAPSHOT_RETENTION)
34
+ .describe('Unpinned topo snapshots to retain'),
35
+ traceAgeMs: z
36
+ .number()
37
+ .default(7 * 24 * 60 * 60 * 1000)
38
+ .describe('Maximum retained trace age in milliseconds'),
39
+ traces: z.number().default(10_000).describe('Maximum retained trace count'),
40
+ }),
41
+ intent: 'read',
42
+ output: z.object({
43
+ db: z.object({
44
+ exists: z.boolean(),
45
+ fileSizeBytes: z.number(),
46
+ path: z.string(),
47
+ }),
48
+ lock: z.object({
49
+ exists: z.boolean(),
50
+ fileSizeBytes: z.number(),
51
+ path: z.string(),
52
+ }),
53
+ retention: z.object({
54
+ snapshots: z.number(),
55
+ traceAgeMs: z.number(),
56
+ traces: z.number(),
57
+ }),
58
+ topo: z.object({
59
+ pinnedCount: z.number(),
60
+ prunableSnapshotCount: z.number(),
61
+ snapshotCount: z.number(),
62
+ }),
63
+ tracing: z.object({
64
+ recordCount: z.number(),
65
+ }),
66
+ }),
67
+ });