@ontrails/trails 1.0.0-beta.15 → 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 (201) hide show
  1. package/CHANGELOG.md +197 -2
  2. package/README.md +27 -0
  3. package/package.json +19 -8
  4. package/src/app.ts +15 -5
  5. package/src/cli.ts +303 -10
  6. package/src/completions.ts +240 -0
  7. package/src/load-app-mirror.ts +160 -0
  8. package/src/local-state-io.ts +153 -0
  9. package/src/project-writes.ts +320 -0
  10. package/src/run-collision.ts +125 -0
  11. package/src/run-completions-install.ts +179 -0
  12. package/src/run-example.ts +149 -0
  13. package/src/run-examples.ts +148 -0
  14. package/src/run-quiet.ts +75 -0
  15. package/src/run-trace.ts +273 -0
  16. package/src/run-warden.ts +39 -0
  17. package/src/run-watch.ts +432 -0
  18. package/src/scaffold-versions.generated.ts +12 -0
  19. package/src/trails/add-surface.ts +45 -23
  20. package/src/trails/add-trail.ts +27 -17
  21. package/src/trails/add-verify.ts +57 -17
  22. package/src/trails/completions-complete.ts +165 -0
  23. package/src/trails/completions.ts +47 -0
  24. package/src/trails/create-scaffold.ts +86 -33
  25. package/src/trails/create.ts +11 -3
  26. package/src/trails/dev-clean.ts +6 -1
  27. package/src/trails/dev-reset.ts +6 -1
  28. package/src/trails/dev-stats.ts +6 -1
  29. package/src/trails/dev-support.ts +29 -17
  30. package/src/trails/draft-promote.ts +289 -80
  31. package/src/trails/guide.ts +54 -34
  32. package/src/trails/load-app.ts +251 -56
  33. package/src/trails/root-dir.ts +21 -0
  34. package/src/trails/run-example.ts +482 -0
  35. package/src/trails/run-examples.ts +141 -0
  36. package/src/trails/run.ts +403 -0
  37. package/src/trails/survey.ts +506 -200
  38. package/src/trails/topo-activation.ts +385 -0
  39. package/src/trails/topo-compile.ts +55 -0
  40. package/src/trails/topo-history.ts +6 -1
  41. package/src/trails/topo-output-schemas.ts +175 -0
  42. package/src/trails/topo-pin.ts +19 -6
  43. package/src/trails/topo-read-support.ts +171 -228
  44. package/src/trails/topo-reports.ts +400 -25
  45. package/src/trails/topo-store-support.ts +43 -19
  46. package/src/trails/topo-support.ts +18 -28
  47. package/src/trails/topo-unpin.ts +6 -1
  48. package/src/trails/topo-verify.ts +18 -5
  49. package/src/trails/topo.ts +60 -23
  50. package/src/trails/warden-guide.ts +121 -0
  51. package/src/trails/warden.ts +137 -56
  52. package/src/versions.ts +3 -18
  53. package/.turbo/turbo-build.log +0 -1
  54. package/.turbo/turbo-lint.log +0 -3
  55. package/.turbo/turbo-typecheck.log +0 -1
  56. package/__tests__/examples.test.ts +0 -45
  57. package/dist/bin/trails.d.ts +0 -3
  58. package/dist/bin/trails.d.ts.map +0 -1
  59. package/dist/bin/trails.js +0 -4
  60. package/dist/bin/trails.js.map +0 -1
  61. package/dist/src/app.d.ts +0 -2
  62. package/dist/src/app.d.ts.map +0 -1
  63. package/dist/src/app.js +0 -22
  64. package/dist/src/app.js.map +0 -1
  65. package/dist/src/clack.d.ts +0 -9
  66. package/dist/src/clack.d.ts.map +0 -1
  67. package/dist/src/clack.js +0 -84
  68. package/dist/src/clack.js.map +0 -1
  69. package/dist/src/cli.d.ts +0 -2
  70. package/dist/src/cli.d.ts.map +0 -1
  71. package/dist/src/cli.js +0 -14
  72. package/dist/src/cli.js.map +0 -1
  73. package/dist/src/trails/add-surface.d.ts +0 -13
  74. package/dist/src/trails/add-surface.d.ts.map +0 -1
  75. package/dist/src/trails/add-surface.js +0 -110
  76. package/dist/src/trails/add-surface.js.map +0 -1
  77. package/dist/src/trails/add-trail.d.ts +0 -12
  78. package/dist/src/trails/add-trail.d.ts.map +0 -1
  79. package/dist/src/trails/add-trail.js +0 -104
  80. package/dist/src/trails/add-trail.js.map +0 -1
  81. package/dist/src/trails/add-trailhead.d.ts +0 -13
  82. package/dist/src/trails/add-trailhead.d.ts.map +0 -1
  83. package/dist/src/trails/add-trailhead.js +0 -88
  84. package/dist/src/trails/add-trailhead.js.map +0 -1
  85. package/dist/src/trails/add-verify.d.ts +0 -10
  86. package/dist/src/trails/add-verify.d.ts.map +0 -1
  87. package/dist/src/trails/add-verify.js +0 -68
  88. package/dist/src/trails/add-verify.js.map +0 -1
  89. package/dist/src/trails/create-scaffold.d.ts +0 -15
  90. package/dist/src/trails/create-scaffold.d.ts.map +0 -1
  91. package/dist/src/trails/create-scaffold.js +0 -295
  92. package/dist/src/trails/create-scaffold.js.map +0 -1
  93. package/dist/src/trails/create.d.ts +0 -18
  94. package/dist/src/trails/create.d.ts.map +0 -1
  95. package/dist/src/trails/create.js +0 -126
  96. package/dist/src/trails/create.js.map +0 -1
  97. package/dist/src/trails/dev-clean.d.ts +0 -9
  98. package/dist/src/trails/dev-clean.d.ts.map +0 -1
  99. package/dist/src/trails/dev-clean.js +0 -66
  100. package/dist/src/trails/dev-clean.js.map +0 -1
  101. package/dist/src/trails/dev-reset.d.ts +0 -6
  102. package/dist/src/trails/dev-reset.d.ts.map +0 -1
  103. package/dist/src/trails/dev-reset.js +0 -39
  104. package/dist/src/trails/dev-reset.js.map +0 -1
  105. package/dist/src/trails/dev-stats.d.ts +0 -7
  106. package/dist/src/trails/dev-stats.d.ts.map +0 -1
  107. package/dist/src/trails/dev-stats.js +0 -61
  108. package/dist/src/trails/dev-stats.js.map +0 -1
  109. package/dist/src/trails/dev-support.d.ts +0 -64
  110. package/dist/src/trails/dev-support.d.ts.map +0 -1
  111. package/dist/src/trails/dev-support.js +0 -181
  112. package/dist/src/trails/dev-support.js.map +0 -1
  113. package/dist/src/trails/draft-promote.d.ts +0 -18
  114. package/dist/src/trails/draft-promote.d.ts.map +0 -1
  115. package/dist/src/trails/draft-promote.js +0 -400
  116. package/dist/src/trails/draft-promote.js.map +0 -1
  117. package/dist/src/trails/guide.d.ts +0 -21
  118. package/dist/src/trails/guide.d.ts.map +0 -1
  119. package/dist/src/trails/guide.js +0 -61
  120. package/dist/src/trails/guide.js.map +0 -1
  121. package/dist/src/trails/load-app.d.ts +0 -12
  122. package/dist/src/trails/load-app.d.ts.map +0 -1
  123. package/dist/src/trails/load-app.js +0 -415
  124. package/dist/src/trails/load-app.js.map +0 -1
  125. package/dist/src/trails/project.d.ts +0 -8
  126. package/dist/src/trails/project.d.ts.map +0 -1
  127. package/dist/src/trails/project.js +0 -54
  128. package/dist/src/trails/project.js.map +0 -1
  129. package/dist/src/trails/survey.d.ts +0 -18
  130. package/dist/src/trails/survey.d.ts.map +0 -1
  131. package/dist/src/trails/survey.js +0 -234
  132. package/dist/src/trails/survey.js.map +0 -1
  133. package/dist/src/trails/topo-constants.d.ts +0 -3
  134. package/dist/src/trails/topo-constants.d.ts.map +0 -1
  135. package/dist/src/trails/topo-constants.js +0 -3
  136. package/dist/src/trails/topo-constants.js.map +0 -1
  137. package/dist/src/trails/topo-export.d.ts +0 -19
  138. package/dist/src/trails/topo-export.d.ts.map +0 -1
  139. package/dist/src/trails/topo-export.js +0 -31
  140. package/dist/src/trails/topo-export.js.map +0 -1
  141. package/dist/src/trails/topo-history.d.ts +0 -20
  142. package/dist/src/trails/topo-history.d.ts.map +0 -1
  143. package/dist/src/trails/topo-history.js +0 -32
  144. package/dist/src/trails/topo-history.js.map +0 -1
  145. package/dist/src/trails/topo-pin.d.ts +0 -17
  146. package/dist/src/trails/topo-pin.d.ts.map +0 -1
  147. package/dist/src/trails/topo-pin.js +0 -31
  148. package/dist/src/trails/topo-pin.js.map +0 -1
  149. package/dist/src/trails/topo-read-support.d.ts +0 -58
  150. package/dist/src/trails/topo-read-support.d.ts.map +0 -1
  151. package/dist/src/trails/topo-read-support.js +0 -167
  152. package/dist/src/trails/topo-read-support.js.map +0 -1
  153. package/dist/src/trails/topo-reports.d.ts +0 -54
  154. package/dist/src/trails/topo-reports.d.ts.map +0 -1
  155. package/dist/src/trails/topo-reports.js +0 -128
  156. package/dist/src/trails/topo-reports.js.map +0 -1
  157. package/dist/src/trails/topo-show.d.ts +0 -23
  158. package/dist/src/trails/topo-show.d.ts.map +0 -1
  159. package/dist/src/trails/topo-show.js +0 -49
  160. package/dist/src/trails/topo-show.js.map +0 -1
  161. package/dist/src/trails/topo-store-support.d.ts +0 -13
  162. package/dist/src/trails/topo-store-support.d.ts.map +0 -1
  163. package/dist/src/trails/topo-store-support.js +0 -55
  164. package/dist/src/trails/topo-store-support.js.map +0 -1
  165. package/dist/src/trails/topo-support.d.ts +0 -76
  166. package/dist/src/trails/topo-support.d.ts.map +0 -1
  167. package/dist/src/trails/topo-support.js +0 -132
  168. package/dist/src/trails/topo-support.js.map +0 -1
  169. package/dist/src/trails/topo-unpin.d.ts +0 -20
  170. package/dist/src/trails/topo-unpin.d.ts.map +0 -1
  171. package/dist/src/trails/topo-unpin.js +0 -44
  172. package/dist/src/trails/topo-unpin.js.map +0 -1
  173. package/dist/src/trails/topo-verify.d.ts +0 -5
  174. package/dist/src/trails/topo-verify.d.ts.map +0 -1
  175. package/dist/src/trails/topo-verify.js +0 -24
  176. package/dist/src/trails/topo-verify.js.map +0 -1
  177. package/dist/src/trails/topo.d.ts +0 -5
  178. package/dist/src/trails/topo.d.ts.map +0 -1
  179. package/dist/src/trails/topo.js +0 -63
  180. package/dist/src/trails/topo.js.map +0 -1
  181. package/dist/src/trails/warden.d.ts +0 -20
  182. package/dist/src/trails/warden.d.ts.map +0 -1
  183. package/dist/src/trails/warden.js +0 -98
  184. package/dist/src/trails/warden.js.map +0 -1
  185. package/dist/src/versions.d.ts +0 -12
  186. package/dist/src/versions.d.ts.map +0 -1
  187. package/dist/src/versions.js +0 -23
  188. package/dist/src/versions.js.map +0 -1
  189. package/dist/tsconfig.tsbuildinfo +0 -1
  190. package/src/__tests__/add-trail.test.ts +0 -97
  191. package/src/__tests__/create.test.ts +0 -415
  192. package/src/__tests__/draft-promote.test.ts +0 -144
  193. package/src/__tests__/guide.test.ts +0 -96
  194. package/src/__tests__/load-app.test.ts +0 -419
  195. package/src/__tests__/survey.test.ts +0 -377
  196. package/src/__tests__/topo-dev.test.ts +0 -426
  197. package/src/__tests__/warden.test.ts +0 -74
  198. package/src/trails/topo-export.ts +0 -35
  199. package/src/trails/topo-show.ts +0 -54
  200. package/tsconfig.json +0 -9
  201. package/tsconfig.tests.json +0 -10
@@ -1,415 +0,0 @@
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 { tmpdir } from 'node:os';
10
- import { basename, dirname, join } from 'node:path';
11
-
12
- import { Result } from '@ontrails/core';
13
-
14
- import { addSurface } from '../trails/add-surface.js';
15
- import { addVerify } from '../trails/add-verify.js';
16
- import { createRoute } from '../trails/create.js';
17
- import { createScaffold } from '../trails/create-scaffold.js';
18
- import { isInsideProject } from '../trails/project.js';
19
- import {
20
- ontrailsPackageRange,
21
- scaffoldDependencyVersions,
22
- } from '../versions.js';
23
-
24
- type Starter = 'empty' | 'entity' | 'hello';
25
- type Surface = 'cli' | 'http' | 'mcp';
26
-
27
- const makeTempProject = (): string =>
28
- join(
29
- tmpdir(),
30
- `trails-create-test-${Date.now()}-${Math.random().toString(36).slice(2)}`
31
- );
32
-
33
- const readJson = (dir: string, relativePath: string): Record<string, unknown> =>
34
- JSON.parse(readFileSync(join(dir, relativePath), 'utf8')) as Record<
35
- string,
36
- unknown
37
- >;
38
-
39
- const readText = (dir: string, relativePath: string): string =>
40
- readFileSync(join(dir, relativePath), 'utf8');
41
-
42
- const expectPaths = (
43
- dir: string,
44
- relativePaths: readonly string[],
45
- expected: boolean
46
- ): void => {
47
- for (const relativePath of relativePaths) {
48
- expect(existsSync(join(dir, relativePath))).toBe(expected);
49
- }
50
- };
51
-
52
- const expectContainsAll = (
53
- content: string,
54
- snippets: readonly string[]
55
- ): void => {
56
- for (const snippet of snippets) {
57
- expect(content).toContain(snippet);
58
- }
59
- };
60
-
61
- const expectOk = <T>(result: Result<T, Error>): T => {
62
- if (result.isErr()) {
63
- throw result.error;
64
- }
65
- return result.value;
66
- };
67
-
68
- const expectErr = <T>(result: Result<T, Error>): Error => {
69
- if (result.isOk()) {
70
- throw new Error('Expected error result');
71
- }
72
- return result.error;
73
- };
74
-
75
- const runCross = async (
76
- id: string,
77
- input: unknown
78
- ): Promise<Result<unknown, Error>> => {
79
- switch (id) {
80
- case 'create.scaffold': {
81
- return await createScaffold.blaze(input as never, {} as never);
82
- }
83
- case 'add.surface': {
84
- return await addSurface.blaze(input as never, {} as never);
85
- }
86
- case 'add.verify': {
87
- return await addVerify.blaze(input as never, {} as never);
88
- }
89
- default: {
90
- return Result.err(new Error(`Unknown cross target: ${id}`));
91
- }
92
- }
93
- };
94
-
95
- const runCreate = (
96
- projectDir: string,
97
- overrides?: Partial<{
98
- starter: Starter;
99
- surfaces: readonly Surface[];
100
- verify: boolean;
101
- }>
102
- ) =>
103
- createRoute.blaze(
104
- {
105
- dir: dirname(projectDir),
106
- name: basename(projectDir),
107
- starter: overrides?.starter ?? 'hello',
108
- surfaces: [...(overrides?.surfaces ?? ['cli'])],
109
- verify: overrides?.verify ?? true,
110
- },
111
- { cross: runCross } as never
112
- );
113
-
114
- const setupMinimalProject = (dir: string): void => {
115
- mkdirSync(join(dir, 'src'), { recursive: true });
116
- mkdirSync(join(dir, '.trails'), { recursive: true });
117
- writeFileSync(
118
- join(dir, 'src', 'app.ts'),
119
- "import { topo } from '@ontrails/core';\nexport const app = topo('test');\n"
120
- );
121
- writeFileSync(
122
- join(dir, 'package.json'),
123
- JSON.stringify(
124
- {
125
- dependencies: { '@ontrails/core': ontrailsPackageRange },
126
- name: 'test',
127
- },
128
- null,
129
- 2
130
- )
131
- );
132
- };
133
-
134
- const assertDefaultProjectFiles = (dir: string): void => {
135
- expectPaths(
136
- dir,
137
- [
138
- 'package.json',
139
- 'tsconfig.json',
140
- '.gitignore',
141
- '.oxlintrc.json',
142
- '.oxfmtrc.jsonc',
143
- 'src/app.ts',
144
- '.trails',
145
- 'src/cli.ts',
146
- 'src/trails/hello.ts',
147
- '__tests__/examples.test.ts',
148
- 'lefthook.yml',
149
- ],
150
- true
151
- );
152
- };
153
-
154
- const assertCliPackage = (dir: string): void => {
155
- const pkg = readJson(dir, 'package.json');
156
- expect(pkg['name']).toBe(basename(dir));
157
-
158
- const deps = pkg['dependencies'] as Record<string, string>;
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
- };
173
-
174
- const assertGeneratedToolingDeps = (dir: string): void => {
175
- const pkg = readJson(dir, 'package.json');
176
- const devDeps = pkg['devDependencies'] as Record<string, string>;
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);
181
- };
182
-
183
- const assertHelloApp = (dir: string): void => {
184
- expectContainsAll(readText(dir, 'src/app.ts'), [
185
- 'topo',
186
- `'${basename(dir)}'`,
187
- 'hello',
188
- ]);
189
- expectContainsAll(readText(dir, 'src/trails/hello.ts'), [
190
- "import { Result, trail } from '@ontrails/core'",
191
- 'return Result.ok({ message:',
192
- ]);
193
- };
194
-
195
- const assertEntityStarter = (dir: string): void => {
196
- expectPaths(
197
- dir,
198
- [
199
- 'src/trails/entity.ts',
200
- 'src/trails/search.ts',
201
- 'src/trails/onboard.ts',
202
- 'src/signals/entity-signals.ts',
203
- 'src/store.ts',
204
- ],
205
- true
206
- );
207
- expectPaths(dir, ['src/trails/hello.ts'], false);
208
- expectContainsAll(readText(dir, 'src/app.ts'), [
209
- "import * as entity from './trails/entity.js'",
210
- "import * as search from './trails/search.js'",
211
- "import * as onboard from './trails/onboard.js'",
212
- "import * as entitySignals from './signals/entity-signals.js'",
213
- ]);
214
- expectContainsAll(readText(dir, 'src/trails/entity.ts'), [
215
- "import { Result, trail } from '@ontrails/core'",
216
- 'return Result.ok({ id: input.id, name:',
217
- "return Result.ok({ id: '1', name: input.name })",
218
- ]);
219
- expectContainsAll(readText(dir, 'src/trails/search.ts'), [
220
- "import { Result, trail } from '@ontrails/core'",
221
- 'return Result.ok({ results: [] })',
222
- ]);
223
- expectContainsAll(readText(dir, 'src/trails/onboard.ts'), [
224
- "import { Result, trail } from '@ontrails/core'",
225
- 'return Result.ok({ onboarded: true })',
226
- ]);
227
- };
228
-
229
- const assertMcpSurface = (dir: string): void => {
230
- expectPaths(dir, ['src/mcp.ts'], true);
231
- expectPaths(dir, ['src/cli.ts'], false);
232
- expectContainsAll(readText(dir, 'src/mcp.ts'), [
233
- "import { surface } from '@ontrails/mcp'",
234
- 'await surface(app)',
235
- ]);
236
-
237
- const deps = readJson(dir, 'package.json')['dependencies'] as Record<
238
- string,
239
- string
240
- >;
241
- expect(deps['@ontrails/mcp']).toBe(ontrailsPackageRange);
242
- expect(deps['@ontrails/cli']).toBeUndefined();
243
- };
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
-
260
- const assertVerifySkipped = (dir: string): void => {
261
- expectPaths(dir, ['__tests__/examples.test.ts', 'lefthook.yml'], false);
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();
268
- };
269
-
270
- const assertEmptyStarter = (dir: string): void => {
271
- expectPaths(dir, ['src/trails/.gitkeep'], true);
272
- expectPaths(dir, ['src/trails/hello.ts'], false);
273
- const appContent = readText(dir, 'src/app.ts');
274
- expect(appContent).toContain(`topo('${basename(dir)}')`);
275
- expect(appContent).not.toContain('import * as');
276
- };
277
-
278
- const withTempProject = async (
279
- assertion: (dir: string) => Promise<void>
280
- ): Promise<void> => {
281
- const dir = makeTempProject();
282
- try {
283
- await assertion(dir);
284
- } finally {
285
- rmSync(dir, { force: true, recursive: true });
286
- }
287
- };
288
-
289
- describe('trails create', () => {
290
- describe('create mode', () => {
291
- test('generates project structure with defaults', async () => {
292
- await withTempProject(async (dir) => {
293
- expectOk(await runCreate(dir));
294
- assertDefaultProjectFiles(dir);
295
- assertCliPackage(dir);
296
- assertVerifyPackage(dir);
297
- assertGeneratedToolingDeps(dir);
298
- assertHelloApp(dir);
299
- });
300
- });
301
-
302
- test('generates with entity starter', async () => {
303
- await withTempProject(async (dir) => {
304
- expectOk(await runCreate(dir, { starter: 'entity' }));
305
- assertEntityStarter(dir);
306
- });
307
- });
308
-
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 () => {
317
- await withTempProject(async (dir) => {
318
- expectOk(await runCreate(dir, { surfaces: ['http'] }));
319
- assertHttpSurface(dir);
320
- });
321
- });
322
-
323
- test('skips verification when verify is false', async () => {
324
- await withTempProject(async (dir) => {
325
- expectOk(await runCreate(dir, { verify: false }));
326
- assertVerifySkipped(dir);
327
- });
328
- });
329
-
330
- test('generates with empty starter', async () => {
331
- await withTempProject(async (dir) => {
332
- expectOk(await runCreate(dir, { starter: 'empty' }));
333
- assertEmptyStarter(dir);
334
- });
335
- });
336
- });
337
-
338
- describe('add-surface mode', () => {
339
- test('adds MCP to existing project', async () => {
340
- await withTempProject(async (dir) => {
341
- setupMinimalProject(dir);
342
- const result = expectOk(
343
- await addSurface.blaze({ dir, surface: 'mcp' }, {} as never)
344
- );
345
-
346
- expect(result.created).toBe('src/mcp.ts');
347
- expect(result.dependency).toBe('@ontrails/mcp');
348
- expectPaths(dir, ['src/mcp.ts'], true);
349
- expectContainsAll(readText(dir, 'src/mcp.ts'), [
350
- "import { surface } from '@ontrails/mcp'",
351
- ]);
352
- const deps = readJson(dir, 'package.json')['dependencies'] as Record<
353
- string,
354
- string
355
- >;
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);
370
- });
371
- });
372
-
373
- test('detects existing surface entrypoint', async () => {
374
- await withTempProject(async (dir) => {
375
- mkdirSync(join(dir, 'src'), { recursive: true });
376
- mkdirSync(join(dir, '.trails'), { recursive: true });
377
- writeFileSync(join(dir, 'src', 'mcp.ts'), 'existing content');
378
-
379
- const error = expectErr(
380
- await addSurface.blaze({ dir, surface: 'mcp' }, {} as never)
381
- );
382
- expect(error.message).toBe(
383
- 'MCP surface already exists. Nothing to do.'
384
- );
385
- });
386
- });
387
- });
388
- });
389
-
390
- describe('isInsideProject', () => {
391
- test('detects .trails directory', async () => {
392
- await withTempProject(async (dir) => {
393
- mkdirSync(join(dir, '.trails'), { recursive: true });
394
- expect(await isInsideProject(dir)).toBe(true);
395
- });
396
- });
397
-
398
- test('detects topo in src/', async () => {
399
- await withTempProject(async (dir) => {
400
- mkdirSync(join(dir, 'src'), { recursive: true });
401
- writeFileSync(
402
- join(dir, 'src', 'app.ts'),
403
- "import { topo } from '@ontrails/core';\nexport const app = topo('app');\n"
404
- );
405
- expect(await isInsideProject(dir)).toBe(true);
406
- });
407
- });
408
-
409
- test('returns false for empty directory', async () => {
410
- await withTempProject(async (dir) => {
411
- mkdirSync(dir, { recursive: true });
412
- expect(await isInsideProject(dir)).toBe(false);
413
- });
414
- });
415
- });
@@ -1,144 +0,0 @@
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,96 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
-
3
- import type { AnyTrail } from '@ontrails/core';
4
- import { ConflictError, trail, topo, Result } from '@ontrails/core';
5
- import { z } from 'zod';
6
-
7
- // ---------------------------------------------------------------------------
8
- // Test fixtures
9
- // ---------------------------------------------------------------------------
10
-
11
- const helloTrail = trail('hello', {
12
- blaze: (input) => {
13
- const name = input.name ?? 'world';
14
- return Result.ok({ message: `Hello, ${name}!` });
15
- },
16
- description: 'Say hello',
17
- detours: [
18
- {
19
- on: ConflictError,
20
- /* oxlint-disable-next-line require-await -- test stub */
21
- recover: async () => Result.ok({ message: 'recovered' }),
22
- },
23
- ],
24
- examples: [
25
- {
26
- expected: { message: 'Hello, world!' },
27
- input: {},
28
- name: 'Default greeting',
29
- },
30
- {
31
- expected: { message: 'Hello, Trails!' },
32
- input: { name: 'Trails' },
33
- name: 'Named greeting',
34
- },
35
- ],
36
- input: z.object({ name: z.string().optional() }),
37
- intent: 'read',
38
- output: z.object({ message: z.string() }),
39
- });
40
-
41
- const app = topo('test-app', { hello: helloTrail });
42
-
43
- // ---------------------------------------------------------------------------
44
- // Tests
45
- // ---------------------------------------------------------------------------
46
-
47
- describe('trails guide', () => {
48
- test('lists trails with descriptions', () => {
49
- const items = app.list();
50
- expect(items.length).toBe(1);
51
-
52
- const [hello] = items;
53
- expect(hello).toBeDefined();
54
- expect(hello?.id).toBe('hello');
55
-
56
- expect((hello as AnyTrail).description).toBe('Say hello');
57
- });
58
-
59
- test('trail detail includes examples', () => {
60
- const item = app.get('hello') as AnyTrail;
61
- expect(item).toBeDefined();
62
- expect(item.examples).toBeDefined();
63
- expect(item.examples?.length).toBe(2);
64
- expect(item.examples?.[0]?.name).toBe('Default greeting');
65
- });
66
-
67
- test('JSON output for trail is valid', () => {
68
- const t = app.get('hello') as AnyTrail;
69
- expect(t).toBeDefined();
70
-
71
- const json = JSON.stringify({
72
- description: t.description,
73
- detours: t.detours,
74
- examples: t.examples,
75
- id: t.id,
76
- kind: t.kind,
77
- });
78
-
79
- const parsed = JSON.parse(json) as Record<string, unknown>;
80
- expect(parsed['id']).toBe('hello');
81
- expect(parsed['kind']).toBe('trail');
82
- expect(parsed['description']).toBe('Say hello');
83
- });
84
-
85
- test('non-existent trail returns undefined from topo', () => {
86
- const item = app.get('does-not-exist');
87
- expect(item).toBeUndefined();
88
- });
89
-
90
- test('detours are accessible on trail', () => {
91
- const t = app.get('hello') as AnyTrail;
92
- expect(t).toBeDefined();
93
- expect(t.detours).toHaveLength(1);
94
- expect(t.detours[0]?.on).toBe(ConflictError);
95
- });
96
- });