@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/trails",
3
- "version": "1.0.0-beta.13",
3
+ "version": "1.0.0-beta.15",
4
4
  "bin": {
5
5
  "trails": "./bin/trails.ts"
6
6
  },
@@ -14,15 +14,16 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@clack/prompts": "^1.1.0",
17
- "@ontrails/cli": "^1.0.0-beta.12",
18
- "@ontrails/core": "^1.0.0-beta.12",
19
- "@ontrails/logging": "^1.0.0-beta.12",
20
- "@ontrails/schema": "^1.0.0-beta.12",
21
- "@ontrails/warden": "^1.0.0-beta.12",
17
+ "@ontrails/cli": "^1.0.0-beta.14",
18
+ "@ontrails/core": "^1.0.0-beta.14",
19
+ "@ontrails/logging": "^1.0.0-beta.14",
20
+ "@ontrails/schema": "^1.0.0-beta.14",
21
+ "@ontrails/tracing": "^1.0.0-beta.14",
22
+ "@ontrails/warden": "^1.0.0-beta.14",
22
23
  "commander": "^14.0.3",
23
24
  "zod": "^4.3.5"
24
25
  },
25
26
  "devDependencies": {
26
- "@ontrails/testing": "^1.0.0-beta.12"
27
+ "@ontrails/testing": "^1.0.0-beta.14"
27
28
  }
28
29
  }
@@ -0,0 +1,97 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+
5
+ import type { Result } from '@ontrails/core';
6
+ import { ValidationError, validateInput } from '@ontrails/core';
7
+
8
+ import { addTrail } from '../trails/add-trail.js';
9
+
10
+ const repoTempDir = (): string =>
11
+ join(
12
+ resolve(import.meta.dir, '../..'),
13
+ '.tmp-tests',
14
+ `add-trail-${Date.now()}-${Math.random().toString(36).slice(2)}`
15
+ );
16
+
17
+ const expectOk = <T>(result: Result<T, Error>): T => {
18
+ if (result.isErr()) {
19
+ throw result.error;
20
+ }
21
+ return result.value;
22
+ };
23
+
24
+ const expectValidationError = (
25
+ result: Result<unknown, Error>
26
+ ): ValidationError => {
27
+ if (result.isOk()) {
28
+ throw new Error('Expected validation error');
29
+ }
30
+ expect(result.error).toBeInstanceOf(ValidationError);
31
+ return result.error as ValidationError;
32
+ };
33
+
34
+ const readGeneratedFile = (dir: string, relativePath: string): string => {
35
+ const filePath = join(dir, relativePath);
36
+ expect(existsSync(filePath)).toBe(true);
37
+ return readFileSync(filePath, 'utf8');
38
+ };
39
+
40
+ const assertGeneratedScaffold = (dir: string): void => {
41
+ const trailSource = readGeneratedFile(dir, 'src/trails/entity-prepare.ts');
42
+ const testSource = readGeneratedFile(dir, '__tests__/entity-prepare.test.ts');
43
+
44
+ expect(trailSource).toContain('description: "Prepare an entity for export"');
45
+ expect(trailSource).toContain('name: "Prepare a draft entity"');
46
+ expect(trailSource).toContain(
47
+ 'expected: { message: "entity.prepare completed" }'
48
+ );
49
+ expect(testSource).toContain('description: "Prepare a draft entity"');
50
+ expect(testSource).toContain(
51
+ 'expectValue: { message: "entity.prepare completed" }'
52
+ );
53
+ expect(trailSource).not.toContain('TODO');
54
+ expect(testSource).not.toContain('TODO');
55
+ };
56
+
57
+ describe('add.trail', () => {
58
+ test('requires authored description and example metadata', () => {
59
+ const error = expectValidationError(
60
+ validateInput(addTrail.input, {
61
+ id: 'entity.prepare',
62
+ intent: 'write',
63
+ })
64
+ );
65
+
66
+ expect(error.message).toContain('description');
67
+ expect(error.message).toContain('exampleName');
68
+ });
69
+
70
+ test('writes starter files without TODO placeholders', async () => {
71
+ const dir = repoTempDir();
72
+
73
+ try {
74
+ mkdirSync(dir, { recursive: true });
75
+
76
+ const result = expectOk(
77
+ await addTrail.blaze(
78
+ {
79
+ description: 'Prepare an entity for export',
80
+ exampleName: 'Prepare a draft entity',
81
+ id: 'entity.prepare',
82
+ intent: 'write',
83
+ },
84
+ { cwd: dir } as never
85
+ )
86
+ );
87
+
88
+ expect(result.created).toEqual([
89
+ 'src/trails/entity-prepare.ts',
90
+ '__tests__/entity-prepare.test.ts',
91
+ ]);
92
+ assertGeneratedScaffold(dir);
93
+ } finally {
94
+ rmSync(dir, { force: true, recursive: true });
95
+ }
96
+ });
97
+ });
@@ -11,14 +11,18 @@ import { basename, dirname, join } from 'node:path';
11
11
 
12
12
  import { Result } from '@ontrails/core';
13
13
 
14
- import { addTrailhead } from '../trails/add-trailhead.js';
14
+ import { addSurface } from '../trails/add-surface.js';
15
15
  import { addVerify } from '../trails/add-verify.js';
16
16
  import { createRoute } from '../trails/create.js';
17
17
  import { createScaffold } from '../trails/create-scaffold.js';
18
18
  import { isInsideProject } from '../trails/project.js';
19
+ import {
20
+ ontrailsPackageRange,
21
+ scaffoldDependencyVersions,
22
+ } from '../versions.js';
19
23
 
20
24
  type Starter = 'empty' | 'entity' | 'hello';
21
- type Trailhead = 'cli' | 'mcp';
25
+ type Surface = 'cli' | 'http' | 'mcp';
22
26
 
23
27
  const makeTempProject = (): string =>
24
28
  join(
@@ -76,8 +80,8 @@ const runCross = async (
76
80
  case 'create.scaffold': {
77
81
  return await createScaffold.blaze(input as never, {} as never);
78
82
  }
79
- case 'add.trailhead': {
80
- return await addTrailhead.blaze(input as never, {} as never);
83
+ case 'add.surface': {
84
+ return await addSurface.blaze(input as never, {} as never);
81
85
  }
82
86
  case 'add.verify': {
83
87
  return await addVerify.blaze(input as never, {} as never);
@@ -92,7 +96,7 @@ const runCreate = (
92
96
  projectDir: string,
93
97
  overrides?: Partial<{
94
98
  starter: Starter;
95
- trailheads: readonly Trailhead[];
99
+ surfaces: readonly Surface[];
96
100
  verify: boolean;
97
101
  }>
98
102
  ) =>
@@ -101,7 +105,7 @@ const runCreate = (
101
105
  dir: dirname(projectDir),
102
106
  name: basename(projectDir),
103
107
  starter: overrides?.starter ?? 'hello',
104
- trailheads: [...(overrides?.trailheads ?? ['cli'])],
108
+ surfaces: [...(overrides?.surfaces ?? ['cli'])],
105
109
  verify: overrides?.verify ?? true,
106
110
  },
107
111
  { cross: runCross } as never
@@ -117,7 +121,10 @@ const setupMinimalProject = (dir: string): void => {
117
121
  writeFileSync(
118
122
  join(dir, 'package.json'),
119
123
  JSON.stringify(
120
- { dependencies: { '@ontrails/core': 'workspace:*' }, name: 'test' },
124
+ {
125
+ dependencies: { '@ontrails/core': ontrailsPackageRange },
126
+ name: 'test',
127
+ },
121
128
  null,
122
129
  2
123
130
  )
@@ -149,13 +156,28 @@ const assertCliPackage = (dir: string): void => {
149
156
  expect(pkg['name']).toBe(basename(dir));
150
157
 
151
158
  const deps = pkg['dependencies'] as Record<string, string>;
152
- expect(deps['@ontrails/core']).toBe('workspace:*');
153
- expect(deps['@ontrails/cli']).toBe('workspace:*');
154
- expect(deps['commander']).toBeDefined();
159
+ expect(deps['@ontrails/core']).toBe(ontrailsPackageRange);
160
+ expect(deps['@ontrails/cli']).toBe(ontrailsPackageRange);
161
+ expect(deps['commander']).toBe(scaffoldDependencyVersions.commander);
162
+ };
163
+
164
+ const assertVerifyPackage = (dir: string): void => {
165
+ const pkg = readJson(dir, 'package.json');
166
+ const devDeps = pkg['devDependencies'] as Record<string, string>;
167
+ expect(devDeps['@ontrails/testing']).toBe(ontrailsPackageRange);
168
+ expect(devDeps['@ontrails/warden']).toBe(ontrailsPackageRange);
169
+ expect(devDeps['lefthook']).toBe(scaffoldDependencyVersions.lefthook);
170
+ expect(readText(dir, 'lefthook.yml')).toContain('bunx trails warden');
171
+ expect(readText(dir, 'lefthook.yml')).not.toContain('--exit-code');
172
+ };
155
173
 
174
+ const assertGeneratedToolingDeps = (dir: string): void => {
175
+ const pkg = readJson(dir, 'package.json');
156
176
  const devDeps = pkg['devDependencies'] as Record<string, string>;
157
- expect(devDeps['@ontrails/testing']).toBe('workspace:*');
158
- expect(devDeps['@ontrails/warden']).toBe('workspace:*');
177
+ expect(devDeps['@types/bun']).toBe(scaffoldDependencyVersions.bunTypes);
178
+ expect(devDeps['oxlint']).toBe(scaffoldDependencyVersions.oxlint);
179
+ expect(devDeps['typescript']).toBe(scaffoldDependencyVersions.typescript);
180
+ expect(devDeps['ultracite']).toBe(scaffoldDependencyVersions.ultracite);
159
181
  };
160
182
 
161
183
  const assertHelloApp = (dir: string): void => {
@@ -204,25 +226,45 @@ const assertEntityStarter = (dir: string): void => {
204
226
  ]);
205
227
  };
206
228
 
207
- const assertMcpTrailhead = (dir: string): void => {
229
+ const assertMcpSurface = (dir: string): void => {
208
230
  expectPaths(dir, ['src/mcp.ts'], true);
209
231
  expectPaths(dir, ['src/cli.ts'], false);
210
232
  expectContainsAll(readText(dir, 'src/mcp.ts'), [
211
- "import { trailhead } from '@ontrails/mcp'",
212
- 'await trailhead(app)',
233
+ "import { surface } from '@ontrails/mcp'",
234
+ 'await surface(app)',
213
235
  ]);
214
236
 
215
237
  const deps = readJson(dir, 'package.json')['dependencies'] as Record<
216
238
  string,
217
239
  string
218
240
  >;
219
- expect(deps['@ontrails/mcp']).toBe('workspace:*');
241
+ expect(deps['@ontrails/mcp']).toBe(ontrailsPackageRange);
220
242
  expect(deps['@ontrails/cli']).toBeUndefined();
221
243
  };
222
244
 
245
+ const assertHttpSurface = (dir: string): void => {
246
+ expectPaths(dir, ['src/http.ts'], true);
247
+ expectContainsAll(readText(dir, 'src/http.ts'), [
248
+ "import { surface } from '@ontrails/hono'",
249
+ 'await surface(app, { port: 3000 })',
250
+ ]);
251
+
252
+ const deps = readJson(dir, 'package.json')['dependencies'] as Record<
253
+ string,
254
+ string
255
+ >;
256
+ expect(deps['@ontrails/hono']).toBe(ontrailsPackageRange);
257
+ expect(deps['@ontrails/http']).toBe(ontrailsPackageRange);
258
+ };
259
+
223
260
  const assertVerifySkipped = (dir: string): void => {
224
261
  expectPaths(dir, ['__tests__/examples.test.ts', 'lefthook.yml'], false);
225
- expect(readJson(dir, 'package.json')['devDependencies']).toBeUndefined();
262
+ const devDeps = readJson(dir, 'package.json')['devDependencies'] as Record<
263
+ string,
264
+ string
265
+ >;
266
+ expect(devDeps['@ontrails/testing']).toBeUndefined();
267
+ expect(devDeps['@ontrails/warden']).toBeUndefined();
226
268
  };
227
269
 
228
270
  const assertEmptyStarter = (dir: string): void => {
@@ -251,6 +293,8 @@ describe('trails create', () => {
251
293
  expectOk(await runCreate(dir));
252
294
  assertDefaultProjectFiles(dir);
253
295
  assertCliPackage(dir);
296
+ assertVerifyPackage(dir);
297
+ assertGeneratedToolingDeps(dir);
254
298
  assertHelloApp(dir);
255
299
  });
256
300
  });
@@ -262,10 +306,17 @@ describe('trails create', () => {
262
306
  });
263
307
  });
264
308
 
265
- test('generates with MCP trailhead', async () => {
309
+ test('generates with MCP surface', async () => {
310
+ await withTempProject(async (dir) => {
311
+ expectOk(await runCreate(dir, { surfaces: ['mcp'] }));
312
+ assertMcpSurface(dir);
313
+ });
314
+ });
315
+
316
+ test('generates with HTTP surface', async () => {
266
317
  await withTempProject(async (dir) => {
267
- expectOk(await runCreate(dir, { trailheads: ['mcp'] }));
268
- assertMcpTrailhead(dir);
318
+ expectOk(await runCreate(dir, { surfaces: ['http'] }));
319
+ assertHttpSurface(dir);
269
320
  });
270
321
  });
271
322
 
@@ -284,39 +335,52 @@ describe('trails create', () => {
284
335
  });
285
336
  });
286
337
 
287
- describe('add-trailhead mode', () => {
338
+ describe('add-surface mode', () => {
288
339
  test('adds MCP to existing project', async () => {
289
340
  await withTempProject(async (dir) => {
290
341
  setupMinimalProject(dir);
291
342
  const result = expectOk(
292
- await addTrailhead.blaze({ dir, trailhead: 'mcp' }, {} as never)
343
+ await addSurface.blaze({ dir, surface: 'mcp' }, {} as never)
293
344
  );
294
345
 
295
346
  expect(result.created).toBe('src/mcp.ts');
296
347
  expect(result.dependency).toBe('@ontrails/mcp');
297
348
  expectPaths(dir, ['src/mcp.ts'], true);
298
349
  expectContainsAll(readText(dir, 'src/mcp.ts'), [
299
- "import { trailhead } from '@ontrails/mcp'",
350
+ "import { surface } from '@ontrails/mcp'",
300
351
  ]);
301
352
  const deps = readJson(dir, 'package.json')['dependencies'] as Record<
302
353
  string,
303
354
  string
304
355
  >;
305
- expect(deps['@ontrails/mcp']).toBe('workspace:*');
356
+ expect(deps['@ontrails/mcp']).toBe(ontrailsPackageRange);
357
+ });
358
+ });
359
+
360
+ test('adds HTTP to existing project', async () => {
361
+ await withTempProject(async (dir) => {
362
+ setupMinimalProject(dir);
363
+ const result = expectOk(
364
+ await addSurface.blaze({ dir, surface: 'http' }, {} as never)
365
+ );
366
+
367
+ expect(result.created).toBe('src/http.ts');
368
+ expect(result.dependency).toBe('@ontrails/hono');
369
+ assertHttpSurface(dir);
306
370
  });
307
371
  });
308
372
 
309
- test('detects existing trailhead entrypoint', async () => {
373
+ test('detects existing surface entrypoint', async () => {
310
374
  await withTempProject(async (dir) => {
311
375
  mkdirSync(join(dir, 'src'), { recursive: true });
312
376
  mkdirSync(join(dir, '.trails'), { recursive: true });
313
377
  writeFileSync(join(dir, 'src', 'mcp.ts'), 'existing content');
314
378
 
315
379
  const error = expectErr(
316
- await addTrailhead.blaze({ dir, trailhead: 'mcp' }, {} as never)
380
+ await addSurface.blaze({ dir, surface: 'mcp' }, {} as never)
317
381
  );
318
382
  expect(error.message).toBe(
319
- 'MCP trailhead already exists. Nothing to do.'
383
+ 'MCP surface already exists. Nothing to do.'
320
384
  );
321
385
  });
322
386
  });
@@ -0,0 +1,144 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ rmSync,
7
+ writeFileSync,
8
+ } from 'node:fs';
9
+ import { join, resolve } from 'node:path';
10
+
11
+ import type { Result } from '@ontrails/core';
12
+ import { ValidationError } from '@ontrails/core';
13
+
14
+ import { draftPromoteTrail } from '../trails/draft-promote.js';
15
+
16
+ const repoTempDir = (): string =>
17
+ join(
18
+ resolve(import.meta.dir, '../..'),
19
+ '.tmp-tests',
20
+ `draft-promote-${Date.now()}-${Math.random().toString(36).slice(2)}`
21
+ );
22
+
23
+ const expectOk = <T>(result: Result<T, Error>): T => {
24
+ if (result.isErr()) {
25
+ throw result.error;
26
+ }
27
+ return result.value;
28
+ };
29
+
30
+ const expectErr = <E extends Error>(result: Result<unknown, E>): E => {
31
+ if (result.isOk()) {
32
+ throw new Error('expected result to be an error');
33
+ }
34
+ return result.error;
35
+ };
36
+
37
+ const writeDraftPromoteFixture = (dir: string): void => {
38
+ mkdirSync(join(dir, 'src'), { recursive: true });
39
+
40
+ writeFileSync(
41
+ join(dir, 'src', 'app.ts'),
42
+ `import { topo } from '@ontrails/core';
43
+ import { draftPrepare } from './_draft.prepare.js';
44
+ import { exportTrail } from './export.js';
45
+
46
+ export const app = topo('draft-test', { draftPrepare, exportTrail });
47
+ `
48
+ );
49
+
50
+ writeFileSync(
51
+ join(dir, 'src', '_draft.prepare.ts'),
52
+ `import { Result, trail } from '@ontrails/core';
53
+ import { z } from 'zod';
54
+
55
+ export const draftPrepare = trail('_draft.entity.prepare', {
56
+ blaze: async () => Result.ok({ ready: true }),
57
+ input: z.object({}),
58
+ output: z.object({ ready: z.boolean() }),
59
+ });
60
+ `
61
+ );
62
+
63
+ writeFileSync(
64
+ join(dir, 'src', 'export.ts'),
65
+ `import { Result, trail } from '@ontrails/core';
66
+ import { z } from 'zod';
67
+
68
+ export const exportTrail = trail('entity.export', {
69
+ blaze: async () => Result.ok({ exported: true }),
70
+ crosses: ['_draft.entity.prepare'],
71
+ input: z.object({}),
72
+ output: z.object({ exported: z.boolean() }),
73
+ });
74
+ `
75
+ );
76
+ };
77
+
78
+ const expectDraftPromoteResults = (dir: string): void => {
79
+ expect(existsSync(join(dir, 'src', '_draft.prepare.ts'))).toBe(false);
80
+ expect(existsSync(join(dir, 'src', 'prepare.ts'))).toBe(true);
81
+ expect(readFileSync(join(dir, 'src', 'prepare.ts'), 'utf8')).toContain(
82
+ "trail('entity.prepare'"
83
+ );
84
+ expect(readFileSync(join(dir, 'src', 'export.ts'), 'utf8')).toContain(
85
+ "crosses: ['entity.prepare']"
86
+ );
87
+ expect(readFileSync(join(dir, 'src', 'app.ts'), 'utf8')).toContain(
88
+ "from './prepare.js'"
89
+ );
90
+ };
91
+
92
+ describe('draft.promote', () => {
93
+ test('promotes draft ids, renames files, and updates imports', async () => {
94
+ const dir = repoTempDir();
95
+
96
+ try {
97
+ writeDraftPromoteFixture(dir);
98
+
99
+ const result = expectOk(
100
+ await draftPromoteTrail.blaze(
101
+ {
102
+ fromId: '_draft.entity.prepare',
103
+ renameFiles: true,
104
+ rootDir: dir,
105
+ toId: 'entity.prepare',
106
+ },
107
+ { cwd: dir } as never
108
+ )
109
+ );
110
+
111
+ expect(result.promotedEstablished).toBe(true);
112
+ expect(result.remainingDraftIds).toEqual([]);
113
+ expect(result.updatedFiles).toEqual(
114
+ expect.arrayContaining(['src/app.ts', 'src/export.ts'])
115
+ );
116
+ expect(result.renamedFiles).toEqual([
117
+ {
118
+ from: 'src/_draft.prepare.ts',
119
+ to: 'src/prepare.ts',
120
+ },
121
+ ]);
122
+ expectDraftPromoteResults(dir);
123
+ } finally {
124
+ rmSync(dir, { force: true, recursive: true });
125
+ }
126
+ });
127
+
128
+ test('returns ValidationError when rootDir does not exist', async () => {
129
+ const error = expectErr(
130
+ await draftPromoteTrail.blaze(
131
+ {
132
+ fromId: '_draft.entity.prepare',
133
+ renameFiles: true,
134
+ rootDir: join(repoTempDir(), 'missing'),
135
+ toId: 'entity.prepare',
136
+ },
137
+ { cwd: process.cwd() } as never
138
+ )
139
+ );
140
+
141
+ expect(error).toBeInstanceOf(ValidationError);
142
+ expect(error.message).toContain('rootDir does not exist');
143
+ });
144
+ });
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
2
 
3
3
  import type { AnyTrail } from '@ontrails/core';
4
- import { trail, topo, Result } from '@ontrails/core';
4
+ import { ConflictError, trail, topo, Result } from '@ontrails/core';
5
5
  import { z } from 'zod';
6
6
 
7
7
  // ---------------------------------------------------------------------------
@@ -14,9 +14,13 @@ const helloTrail = trail('hello', {
14
14
  return Result.ok({ message: `Hello, ${name}!` });
15
15
  },
16
16
  description: 'Say hello',
17
- detours: {
18
- NotFoundError: ['search'],
19
- },
17
+ detours: [
18
+ {
19
+ on: ConflictError,
20
+ /* oxlint-disable-next-line require-await -- test stub */
21
+ recover: async () => Result.ok({ message: 'recovered' }),
22
+ },
23
+ ],
20
24
  examples: [
21
25
  {
22
26
  expected: { message: 'Hello, world!' },
@@ -86,6 +90,7 @@ describe('trails guide', () => {
86
90
  test('detours are accessible on trail', () => {
87
91
  const t = app.get('hello') as AnyTrail;
88
92
  expect(t).toBeDefined();
89
- expect(t.detours?.['NotFoundError']).toEqual(['search']);
93
+ expect(t.detours).toHaveLength(1);
94
+ expect(t.detours[0]?.on).toBe(ConflictError);
90
95
  });
91
96
  });