@ontrails/trails 1.0.0-beta.14 → 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 (155) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/__tests__/examples.test.ts +29 -4
  4. package/dist/src/app.js +2 -2
  5. package/dist/src/app.js.map +1 -1
  6. package/dist/src/clack.d.ts +1 -1
  7. package/dist/src/clack.js +1 -1
  8. package/dist/src/cli.js +4 -3
  9. package/dist/src/cli.js.map +1 -1
  10. package/dist/src/trails/add-surface.d.ts +3 -3
  11. package/dist/src/trails/add-surface.d.ts.map +1 -1
  12. package/dist/src/trails/add-surface.js +46 -24
  13. package/dist/src/trails/add-surface.js.map +1 -1
  14. package/dist/src/trails/add-trail.d.ts +3 -1
  15. package/dist/src/trails/add-trail.d.ts.map +1 -1
  16. package/dist/src/trails/add-trail.js +38 -11
  17. package/dist/src/trails/add-trail.js.map +1 -1
  18. package/dist/src/trails/add-trailhead.d.ts +1 -1
  19. package/dist/src/trails/add-trailhead.d.ts.map +1 -1
  20. package/dist/src/trails/add-trailhead.js +4 -4
  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 +7 -6
  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 +10 -3
  28. package/dist/src/trails/create-scaffold.js.map +1 -1
  29. package/dist/src/trails/create.d.ts +10 -14
  30. package/dist/src/trails/create.d.ts.map +1 -1
  31. package/dist/src/trails/create.js +42 -37
  32. package/dist/src/trails/create.js.map +1 -1
  33. package/dist/src/trails/dev-clean.d.ts +4 -4
  34. package/dist/src/trails/dev-clean.d.ts.map +1 -1
  35. package/dist/src/trails/dev-clean.js +22 -21
  36. package/dist/src/trails/dev-clean.js.map +1 -1
  37. package/dist/src/trails/dev-reset.d.ts +1 -1
  38. package/dist/src/trails/dev-reset.d.ts.map +1 -1
  39. package/dist/src/trails/dev-reset.js +3 -2
  40. package/dist/src/trails/dev-reset.js.map +1 -1
  41. package/dist/src/trails/dev-stats.d.ts +4 -4
  42. package/dist/src/trails/dev-stats.d.ts.map +1 -1
  43. package/dist/src/trails/dev-stats.js +19 -19
  44. package/dist/src/trails/dev-stats.js.map +1 -1
  45. package/dist/src/trails/dev-support.d.ts +17 -17
  46. package/dist/src/trails/dev-support.d.ts.map +1 -1
  47. package/dist/src/trails/dev-support.js +60 -57
  48. package/dist/src/trails/dev-support.js.map +1 -1
  49. package/dist/src/trails/draft-promote.d.ts +1 -1
  50. package/dist/src/trails/draft-promote.d.ts.map +1 -1
  51. package/dist/src/trails/draft-promote.js +30 -16
  52. package/dist/src/trails/draft-promote.js.map +1 -1
  53. package/dist/src/trails/guide.d.ts +2 -2
  54. package/dist/src/trails/guide.d.ts.map +1 -1
  55. package/dist/src/trails/guide.js +1 -4
  56. package/dist/src/trails/guide.js.map +1 -1
  57. package/dist/src/trails/load-app.d.ts +7 -1
  58. package/dist/src/trails/load-app.d.ts.map +1 -1
  59. package/dist/src/trails/load-app.js +374 -26
  60. package/dist/src/trails/load-app.js.map +1 -1
  61. package/dist/src/trails/survey.d.ts +5 -5
  62. package/dist/src/trails/survey.d.ts.map +1 -1
  63. package/dist/src/trails/survey.js +51 -29
  64. package/dist/src/trails/survey.js.map +1 -1
  65. package/dist/src/trails/topo-export.d.ts +5 -4
  66. package/dist/src/trails/topo-export.d.ts.map +1 -1
  67. package/dist/src/trails/topo-export.js +4 -7
  68. package/dist/src/trails/topo-export.js.map +1 -1
  69. package/dist/src/trails/topo-history.d.ts +6 -10
  70. package/dist/src/trails/topo-history.d.ts.map +1 -1
  71. package/dist/src/trails/topo-history.js +7 -8
  72. package/dist/src/trails/topo-history.js.map +1 -1
  73. package/dist/src/trails/topo-pin.d.ts +5 -9
  74. package/dist/src/trails/topo-pin.d.ts.map +1 -1
  75. package/dist/src/trails/topo-pin.js +5 -9
  76. package/dist/src/trails/topo-pin.js.map +1 -1
  77. package/dist/src/trails/topo-read-support.d.ts +12 -8
  78. package/dist/src/trails/topo-read-support.d.ts.map +1 -1
  79. package/dist/src/trails/topo-read-support.js +71 -82
  80. package/dist/src/trails/topo-read-support.js.map +1 -1
  81. package/dist/src/trails/topo-reports.d.ts +15 -11
  82. package/dist/src/trails/topo-reports.d.ts.map +1 -1
  83. package/dist/src/trails/topo-reports.js +40 -34
  84. package/dist/src/trails/topo-reports.js.map +1 -1
  85. package/dist/src/trails/topo-show.d.ts +4 -4
  86. package/dist/src/trails/topo-show.d.ts.map +1 -1
  87. package/dist/src/trails/topo-show.js +8 -12
  88. package/dist/src/trails/topo-show.js.map +1 -1
  89. package/dist/src/trails/topo-store-support.d.ts +2 -2
  90. package/dist/src/trails/topo-store-support.d.ts.map +1 -1
  91. package/dist/src/trails/topo-store-support.js +22 -22
  92. package/dist/src/trails/topo-store-support.js.map +1 -1
  93. package/dist/src/trails/topo-support.d.ts +19 -30
  94. package/dist/src/trails/topo-support.d.ts.map +1 -1
  95. package/dist/src/trails/topo-support.js +63 -96
  96. package/dist/src/trails/topo-support.js.map +1 -1
  97. package/dist/src/trails/topo-unpin.d.ts +9 -4
  98. package/dist/src/trails/topo-unpin.d.ts.map +1 -1
  99. package/dist/src/trails/topo-unpin.js +9 -4
  100. package/dist/src/trails/topo-unpin.js.map +1 -1
  101. package/dist/src/trails/topo-verify.d.ts +2 -2
  102. package/dist/src/trails/topo-verify.d.ts.map +1 -1
  103. package/dist/src/trails/topo-verify.js +1 -5
  104. package/dist/src/trails/topo-verify.js.map +1 -1
  105. package/dist/src/trails/topo.d.ts +2 -2
  106. package/dist/src/trails/topo.d.ts.map +1 -1
  107. package/dist/src/trails/topo.js +6 -10
  108. package/dist/src/trails/topo.js.map +1 -1
  109. package/dist/src/trails/warden.d.ts +3 -2
  110. package/dist/src/trails/warden.d.ts.map +1 -1
  111. package/dist/src/trails/warden.js +11 -2
  112. package/dist/src/trails/warden.js.map +1 -1
  113. package/dist/src/versions.d.ts +12 -0
  114. package/dist/src/versions.d.ts.map +1 -0
  115. package/dist/src/versions.js +23 -0
  116. package/dist/src/versions.js.map +1 -0
  117. package/dist/tsconfig.tsbuildinfo +1 -1
  118. package/package.json +8 -8
  119. package/src/__tests__/add-trail.test.ts +97 -0
  120. package/src/__tests__/create.test.ts +91 -27
  121. package/src/__tests__/guide.test.ts +10 -5
  122. package/src/__tests__/load-app.test.ts +365 -4
  123. package/src/__tests__/survey.test.ts +143 -67
  124. package/src/__tests__/topo-dev.test.ts +48 -46
  125. package/src/app.ts +2 -2
  126. package/src/clack.ts +1 -1
  127. package/src/cli.ts +4 -3
  128. package/src/trails/add-surface.ts +150 -0
  129. package/src/trails/add-trail.ts +46 -10
  130. package/src/trails/add-verify.ts +11 -6
  131. package/src/trails/create-scaffold.ts +16 -3
  132. package/src/trails/create.ts +76 -71
  133. package/src/trails/dev-clean.ts +25 -21
  134. package/src/trails/dev-reset.ts +3 -2
  135. package/src/trails/dev-stats.ts +22 -19
  136. package/src/trails/dev-support.ts +88 -86
  137. package/src/trails/draft-promote.ts +70 -35
  138. package/src/trails/guide.ts +1 -4
  139. package/src/trails/load-app.ts +511 -32
  140. package/src/trails/survey.ts +60 -35
  141. package/src/trails/topo-export.ts +5 -9
  142. package/src/trails/topo-history.ts +8 -10
  143. package/src/trails/topo-pin.ts +9 -13
  144. package/src/trails/topo-read-support.ts +103 -106
  145. package/src/trails/topo-reports.ts +54 -47
  146. package/src/trails/topo-show.ts +8 -12
  147. package/src/trails/topo-store-support.ts +36 -28
  148. package/src/trails/topo-support.ts +90 -134
  149. package/src/trails/topo-unpin.ts +11 -6
  150. package/src/trails/topo-verify.ts +1 -5
  151. package/src/trails/topo.ts +6 -10
  152. package/src/trails/warden.ts +12 -3
  153. package/src/versions.ts +43 -0
  154. package/tsconfig.tests.json +10 -0
  155. package/src/trails/add-trailhead.ts +0 -121
@@ -0,0 +1,150 @@
1
+ /**
2
+ * `add.surface` trail -- Add a surface to an existing project.
3
+ *
4
+ * Generates surface entry points and updates package.json dependencies.
5
+ */
6
+
7
+ import { existsSync, mkdirSync } from 'node:fs';
8
+ import { basename, dirname, join, resolve } from 'node:path';
9
+
10
+ import { Result, trail } from '@ontrails/core';
11
+ import { z } from 'zod';
12
+
13
+ import {
14
+ ontrailsPackageRange,
15
+ scaffoldDependencyVersions,
16
+ } from '../versions.js';
17
+ import { findTopoPath } from './project.js';
18
+
19
+ type Surface = 'cli' | 'http' | 'mcp';
20
+
21
+ const generateCliEntry = (appImportPath: string): string =>
22
+ `import { surface } from '@ontrails/cli/commander';
23
+
24
+ import { app } from '${appImportPath}';
25
+
26
+ await surface(app);
27
+ `;
28
+
29
+ const generateMcpEntry = (appImportPath: string): string =>
30
+ `import { surface } from '@ontrails/mcp';
31
+
32
+ import { app } from '${appImportPath}';
33
+
34
+ await surface(app);
35
+ `;
36
+
37
+ const generateHttpEntry = (appImportPath: string): string =>
38
+ `import { surface } from '@ontrails/hono';
39
+
40
+ import { app } from '${appImportPath}';
41
+
42
+ await surface(app, { port: 3000 });
43
+ `;
44
+
45
+ const surfaceEntryFiles = {
46
+ cli: 'src/cli.ts',
47
+ http: 'src/http.ts',
48
+ mcp: 'src/mcp.ts',
49
+ } satisfies Record<Surface, string>;
50
+
51
+ const surfaceDependencies = {
52
+ cli: ['@ontrails/cli'],
53
+ http: ['@ontrails/hono', '@ontrails/http'],
54
+ mcp: ['@ontrails/mcp'],
55
+ } satisfies Record<Surface, readonly string[]>;
56
+
57
+ /** Resolve the entry file for a surface. */
58
+ const getEntryFile = (surface: Surface): string => surfaceEntryFiles[surface];
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Trail definition
62
+ // ---------------------------------------------------------------------------
63
+
64
+ /** Patch deps and optionally bin in a parsed package.json. */
65
+ const patchPkgDeps = (
66
+ pkg: Record<string, unknown>,
67
+ surface: Surface,
68
+ cwd: string
69
+ ): string => {
70
+ const [depName = ''] = surfaceDependencies[surface];
71
+ const deps = (pkg['dependencies'] ?? {}) as Record<string, string>;
72
+ for (const dependency of surfaceDependencies[surface]) {
73
+ deps[dependency] = ontrailsPackageRange;
74
+ }
75
+ if (surface === 'cli') {
76
+ deps['commander'] = scaffoldDependencyVersions.commander;
77
+ pkg['bin'] = {
78
+ [(pkg['name'] as string | undefined) ?? basename(cwd)]: './src/cli.ts',
79
+ };
80
+ }
81
+ pkg['dependencies'] = Object.fromEntries(
82
+ Object.entries(deps).toSorted(([a], [b]) => a.localeCompare(b))
83
+ );
84
+ return depName;
85
+ };
86
+
87
+ /** Update package.json with surface dependency and CLI bin if needed. */
88
+ const updatePkgJsonForSurface = async (
89
+ cwd: string,
90
+ surface: Surface
91
+ ): Promise<string> => {
92
+ const pkgPath = join(cwd, 'package.json');
93
+ if (!existsSync(pkgPath)) {
94
+ return surfaceDependencies[surface][0] ?? '';
95
+ }
96
+ const pkg = (await Bun.file(pkgPath).json()) as Record<string, unknown>;
97
+ const depName = patchPkgDeps(pkg, surface, cwd);
98
+ await Bun.write(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
99
+ return depName;
100
+ };
101
+
102
+ /** Create the entry file for a surface and return the relative path. */
103
+ const writeSurfaceEntry = async (
104
+ cwd: string,
105
+ surface: Surface
106
+ ): Promise<string> => {
107
+ const entryFile = getEntryFile(surface);
108
+ const fullEntryPath = join(cwd, entryFile);
109
+ const appImport = (await findTopoPath(cwd)) ?? './app.js';
110
+ const generators = {
111
+ cli: generateCliEntry,
112
+ http: generateHttpEntry,
113
+ mcp: generateMcpEntry,
114
+ } satisfies Record<Surface, (appImportPath: string) => string>;
115
+ const content = generators[surface](appImport);
116
+
117
+ mkdirSync(dirname(fullEntryPath), { recursive: true });
118
+ await Bun.write(fullEntryPath, content);
119
+ return entryFile;
120
+ };
121
+
122
+ export const addSurface = trail('add.surface', {
123
+ blaze: async (input) => {
124
+ const cwd = resolve(input.dir ?? '.');
125
+ const { surface } = input;
126
+ const entryFile = getEntryFile(surface);
127
+
128
+ if (existsSync(join(cwd, entryFile))) {
129
+ return Result.err(
130
+ new Error(
131
+ `${surface.toUpperCase()} surface already exists. Nothing to do.`
132
+ )
133
+ );
134
+ }
135
+
136
+ return Result.ok({
137
+ created: await writeSurfaceEntry(cwd, surface),
138
+ dependency: await updatePkgJsonForSurface(cwd, surface),
139
+ });
140
+ },
141
+ description: 'Add a surface to an existing project',
142
+ input: z.object({
143
+ dir: z.string().optional().describe('Project directory'),
144
+ surface: z.enum(['cli', 'http', 'mcp']).describe('Surface to add'),
145
+ }),
146
+ output: z.object({
147
+ created: z.string(),
148
+ dependency: z.string(),
149
+ }),
150
+ });
@@ -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({
@@ -1,8 +1,11 @@
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 { createIsolatedExampleInput } from './topo-support.js';
6
9
 
7
10
  export const devCleanTrail = trail('dev.clean', {
8
11
  blaze: (input, ctx) => {
@@ -18,19 +21,19 @@ export const devCleanTrail = trail('dev.clean', {
18
21
  return Result.ok(
19
22
  cleanDevState({
20
23
  dryRun: input.dryRun,
21
- maxAge: input.trackAgeMs,
22
- maxRecords: input.tracks,
24
+ maxAge: input.traceAgeMs,
25
+ maxRecords: input.traces,
23
26
  rootDir,
24
- saveRetention: input.saves,
27
+ snapshotRetention: input.snapshots,
25
28
  })
26
29
  );
27
30
  },
28
- description: 'Prune unpinned topo saves and old track records',
31
+ description: 'Prune unpinned topo snapshots and old trace records',
29
32
  examples: [
30
33
  {
31
34
  input: {
32
35
  dryRun: true,
33
- rootDir: isolatedExampleInput('dev-clean').rootDir,
36
+ rootDir: createIsolatedExampleInput('dev-clean').rootDir,
34
37
  },
35
38
  name: 'Preview local cleanup',
36
39
  },
@@ -41,33 +44,34 @@ export const devCleanTrail = trail('dev.clean', {
41
44
  .default(true)
42
45
  .describe('Preview cleanup without changing state'),
43
46
  rootDir: z.string().optional().describe('Workspace root directory'),
44
- saves: z
47
+ snapshots: z
45
48
  .number()
46
- .default(DEFAULT_TOPO_SAVE_RETENTION)
47
- .describe('Unpinned topo saves to retain'),
48
- trackAgeMs: z
49
+ .default(DEFAULT_TOPO_SNAPSHOT_RETENTION)
50
+ .describe('Unpinned topo snapshots to retain'),
51
+ traceAgeMs: z
49
52
  .number()
50
53
  .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'),
54
+ .describe('Maximum retained trace age in milliseconds'),
55
+ traces: z.number().default(10_000).describe('Maximum retained trace count'),
53
56
  yes: z.boolean().default(false).describe('Confirm destructive changes'),
54
57
  }),
55
58
  intent: 'destroy',
56
59
  output: z.object({
57
60
  dryRun: z.boolean(),
58
61
  remaining: z.object({
59
- pinCount: z.number(),
60
- saveCount: z.number(),
61
- trackCount: z.number(),
62
+ pinnedCount: z.number(),
63
+ snapshotCount: z.number(),
64
+ traceCount: z.number(),
62
65
  }),
63
66
  removed: z.object({
64
- topoSaves: z.number(),
65
- trackRecords: z.number(),
67
+ topoSnapshots: z.number(),
68
+ traceRecords: z.number(),
66
69
  }),
67
70
  retention: z.object({
68
- saves: z.number(),
69
- trackAgeMs: z.number(),
70
- tracks: z.number(),
71
+ snapshots: z.number(),
72
+ traceAgeMs: z.number(),
73
+ traces: z.number(),
71
74
  }),
72
75
  }),
76
+ permit: { scopes: ['dev:clean'] },
73
77
  });