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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +27 -0
  3. package/package.json +19 -8
  4. package/src/app.ts +17 -7
  5. package/src/clack.ts +1 -1
  6. package/src/cli.ts +304 -10
  7. package/src/completions.ts +240 -0
  8. package/src/load-app-mirror.ts +160 -0
  9. package/src/local-state-io.ts +153 -0
  10. package/src/project-writes.ts +320 -0
  11. package/src/run-collision.ts +125 -0
  12. package/src/run-completions-install.ts +179 -0
  13. package/src/run-example.ts +149 -0
  14. package/src/run-examples.ts +148 -0
  15. package/src/run-quiet.ts +75 -0
  16. package/src/run-trace.ts +273 -0
  17. package/src/run-warden.ts +39 -0
  18. package/src/run-watch.ts +432 -0
  19. package/src/scaffold-versions.generated.ts +12 -0
  20. package/src/trails/add-surface.ts +172 -0
  21. package/src/trails/add-trail.ts +73 -27
  22. package/src/trails/add-verify.ts +68 -23
  23. package/src/trails/completions-complete.ts +165 -0
  24. package/src/trails/completions.ts +47 -0
  25. package/src/trails/create-scaffold.ts +101 -35
  26. package/src/trails/create.ts +87 -74
  27. package/src/trails/dev-clean.ts +31 -22
  28. package/src/trails/dev-reset.ts +9 -3
  29. package/src/trails/dev-stats.ts +28 -20
  30. package/src/trails/dev-support.ts +109 -95
  31. package/src/trails/draft-promote.ts +351 -107
  32. package/src/trails/guide.ts +55 -38
  33. package/src/trails/load-app.ts +712 -38
  34. package/src/trails/root-dir.ts +21 -0
  35. package/src/trails/run-example.ts +482 -0
  36. package/src/trails/run-examples.ts +141 -0
  37. package/src/trails/run.ts +403 -0
  38. package/src/trails/survey.ts +517 -186
  39. package/src/trails/topo-activation.ts +385 -0
  40. package/src/trails/topo-compile.ts +55 -0
  41. package/src/trails/topo-history.ts +14 -11
  42. package/src/trails/topo-output-schemas.ts +175 -0
  43. package/src/trails/topo-pin.ts +25 -16
  44. package/src/trails/topo-read-support.ts +178 -238
  45. package/src/trails/topo-reports.ts +445 -63
  46. package/src/trails/topo-store-support.ts +67 -35
  47. package/src/trails/topo-support.ts +93 -147
  48. package/src/trails/topo-unpin.ts +17 -7
  49. package/src/trails/topo-verify.ts +19 -10
  50. package/src/trails/topo.ts +64 -31
  51. package/src/trails/warden-guide.ts +121 -0
  52. package/src/trails/warden.ts +137 -47
  53. package/src/versions.ts +28 -0
  54. package/.turbo/turbo-build.log +0 -1
  55. package/.turbo/turbo-lint.log +0 -3
  56. package/.turbo/turbo-typecheck.log +0 -1
  57. package/__tests__/examples.test.ts +0 -20
  58. package/dist/bin/trails.d.ts +0 -3
  59. package/dist/bin/trails.d.ts.map +0 -1
  60. package/dist/bin/trails.js +0 -4
  61. package/dist/bin/trails.js.map +0 -1
  62. package/dist/src/app.d.ts +0 -2
  63. package/dist/src/app.d.ts.map +0 -1
  64. package/dist/src/app.js +0 -22
  65. package/dist/src/app.js.map +0 -1
  66. package/dist/src/clack.d.ts +0 -9
  67. package/dist/src/clack.d.ts.map +0 -1
  68. package/dist/src/clack.js +0 -84
  69. package/dist/src/clack.js.map +0 -1
  70. package/dist/src/cli.d.ts +0 -2
  71. package/dist/src/cli.d.ts.map +0 -1
  72. package/dist/src/cli.js +0 -13
  73. package/dist/src/cli.js.map +0 -1
  74. package/dist/src/trails/add-surface.d.ts +0 -13
  75. package/dist/src/trails/add-surface.d.ts.map +0 -1
  76. package/dist/src/trails/add-surface.js +0 -88
  77. package/dist/src/trails/add-surface.js.map +0 -1
  78. package/dist/src/trails/add-trail.d.ts +0 -10
  79. package/dist/src/trails/add-trail.d.ts.map +0 -1
  80. package/dist/src/trails/add-trail.js +0 -77
  81. package/dist/src/trails/add-trail.js.map +0 -1
  82. package/dist/src/trails/add-trailhead.d.ts +0 -13
  83. package/dist/src/trails/add-trailhead.d.ts.map +0 -1
  84. package/dist/src/trails/add-trailhead.js +0 -88
  85. package/dist/src/trails/add-trailhead.js.map +0 -1
  86. package/dist/src/trails/add-verify.d.ts +0 -10
  87. package/dist/src/trails/add-verify.d.ts.map +0 -1
  88. package/dist/src/trails/add-verify.js +0 -67
  89. package/dist/src/trails/add-verify.js.map +0 -1
  90. package/dist/src/trails/create-scaffold.d.ts +0 -15
  91. package/dist/src/trails/create-scaffold.d.ts.map +0 -1
  92. package/dist/src/trails/create-scaffold.js +0 -288
  93. package/dist/src/trails/create-scaffold.js.map +0 -1
  94. package/dist/src/trails/create.d.ts +0 -22
  95. package/dist/src/trails/create.d.ts.map +0 -1
  96. package/dist/src/trails/create.js +0 -121
  97. package/dist/src/trails/create.js.map +0 -1
  98. package/dist/src/trails/dev-clean.d.ts +0 -9
  99. package/dist/src/trails/dev-clean.d.ts.map +0 -1
  100. package/dist/src/trails/dev-clean.js +0 -65
  101. package/dist/src/trails/dev-clean.js.map +0 -1
  102. package/dist/src/trails/dev-reset.d.ts +0 -6
  103. package/dist/src/trails/dev-reset.d.ts.map +0 -1
  104. package/dist/src/trails/dev-reset.js +0 -38
  105. package/dist/src/trails/dev-reset.js.map +0 -1
  106. package/dist/src/trails/dev-stats.d.ts +0 -7
  107. package/dist/src/trails/dev-stats.d.ts.map +0 -1
  108. package/dist/src/trails/dev-stats.js +0 -61
  109. package/dist/src/trails/dev-stats.js.map +0 -1
  110. package/dist/src/trails/dev-support.d.ts +0 -64
  111. package/dist/src/trails/dev-support.d.ts.map +0 -1
  112. package/dist/src/trails/dev-support.js +0 -178
  113. package/dist/src/trails/dev-support.js.map +0 -1
  114. package/dist/src/trails/draft-promote.d.ts +0 -18
  115. package/dist/src/trails/draft-promote.d.ts.map +0 -1
  116. package/dist/src/trails/draft-promote.js +0 -386
  117. package/dist/src/trails/draft-promote.js.map +0 -1
  118. package/dist/src/trails/guide.d.ts +0 -21
  119. package/dist/src/trails/guide.d.ts.map +0 -1
  120. package/dist/src/trails/guide.js +0 -64
  121. package/dist/src/trails/guide.js.map +0 -1
  122. package/dist/src/trails/load-app.d.ts +0 -6
  123. package/dist/src/trails/load-app.d.ts.map +0 -1
  124. package/dist/src/trails/load-app.js +0 -67
  125. package/dist/src/trails/load-app.js.map +0 -1
  126. package/dist/src/trails/project.d.ts +0 -8
  127. package/dist/src/trails/project.d.ts.map +0 -1
  128. package/dist/src/trails/project.js +0 -54
  129. package/dist/src/trails/project.js.map +0 -1
  130. package/dist/src/trails/survey.d.ts +0 -18
  131. package/dist/src/trails/survey.d.ts.map +0 -1
  132. package/dist/src/trails/survey.js +0 -212
  133. package/dist/src/trails/survey.js.map +0 -1
  134. package/dist/src/trails/topo-constants.d.ts +0 -3
  135. package/dist/src/trails/topo-constants.d.ts.map +0 -1
  136. package/dist/src/trails/topo-constants.js +0 -3
  137. package/dist/src/trails/topo-constants.js.map +0 -1
  138. package/dist/src/trails/topo-export.d.ts +0 -18
  139. package/dist/src/trails/topo-export.d.ts.map +0 -1
  140. package/dist/src/trails/topo-export.js +0 -34
  141. package/dist/src/trails/topo-export.js.map +0 -1
  142. package/dist/src/trails/topo-history.d.ts +0 -24
  143. package/dist/src/trails/topo-history.d.ts.map +0 -1
  144. package/dist/src/trails/topo-history.js +0 -33
  145. package/dist/src/trails/topo-history.js.map +0 -1
  146. package/dist/src/trails/topo-pin.d.ts +0 -21
  147. package/dist/src/trails/topo-pin.d.ts.map +0 -1
  148. package/dist/src/trails/topo-pin.js +0 -35
  149. package/dist/src/trails/topo-pin.js.map +0 -1
  150. package/dist/src/trails/topo-read-support.d.ts +0 -54
  151. package/dist/src/trails/topo-read-support.d.ts.map +0 -1
  152. package/dist/src/trails/topo-read-support.js +0 -178
  153. package/dist/src/trails/topo-read-support.js.map +0 -1
  154. package/dist/src/trails/topo-reports.d.ts +0 -50
  155. package/dist/src/trails/topo-reports.d.ts.map +0 -1
  156. package/dist/src/trails/topo-reports.js +0 -122
  157. package/dist/src/trails/topo-reports.js.map +0 -1
  158. package/dist/src/trails/topo-show.d.ts +0 -23
  159. package/dist/src/trails/topo-show.d.ts.map +0 -1
  160. package/dist/src/trails/topo-show.js +0 -53
  161. package/dist/src/trails/topo-show.js.map +0 -1
  162. package/dist/src/trails/topo-store-support.d.ts +0 -13
  163. package/dist/src/trails/topo-store-support.d.ts.map +0 -1
  164. package/dist/src/trails/topo-store-support.js +0 -55
  165. package/dist/src/trails/topo-store-support.js.map +0 -1
  166. package/dist/src/trails/topo-support.d.ts +0 -87
  167. package/dist/src/trails/topo-support.d.ts.map +0 -1
  168. package/dist/src/trails/topo-support.js +0 -165
  169. package/dist/src/trails/topo-support.js.map +0 -1
  170. package/dist/src/trails/topo-unpin.d.ts +0 -15
  171. package/dist/src/trails/topo-unpin.d.ts.map +0 -1
  172. package/dist/src/trails/topo-unpin.js +0 -39
  173. package/dist/src/trails/topo-unpin.js.map +0 -1
  174. package/dist/src/trails/topo-verify.d.ts +0 -5
  175. package/dist/src/trails/topo-verify.d.ts.map +0 -1
  176. package/dist/src/trails/topo-verify.js +0 -28
  177. package/dist/src/trails/topo-verify.js.map +0 -1
  178. package/dist/src/trails/topo.d.ts +0 -5
  179. package/dist/src/trails/topo.d.ts.map +0 -1
  180. package/dist/src/trails/topo.js +0 -67
  181. package/dist/src/trails/topo.js.map +0 -1
  182. package/dist/src/trails/warden.d.ts +0 -19
  183. package/dist/src/trails/warden.d.ts.map +0 -1
  184. package/dist/src/trails/warden.js +0 -89
  185. package/dist/src/trails/warden.js.map +0 -1
  186. package/dist/tsconfig.tsbuildinfo +0 -1
  187. package/src/__tests__/create.test.ts +0 -351
  188. package/src/__tests__/draft-promote.test.ts +0 -144
  189. package/src/__tests__/guide.test.ts +0 -91
  190. package/src/__tests__/load-app.test.ts +0 -58
  191. package/src/__tests__/survey.test.ts +0 -301
  192. package/src/__tests__/topo-dev.test.ts +0 -424
  193. package/src/__tests__/warden.test.ts +0 -74
  194. package/src/trails/add-trailhead.ts +0 -121
  195. package/src/trails/topo-export.ts +0 -39
  196. package/src/trails/topo-show.ts +0 -58
  197. package/tsconfig.json +0 -9
@@ -1,424 +0,0 @@
1
- /* oxlint-disable max-statements */
2
-
3
- import { describe, expect, test } from 'bun:test';
4
- import {
5
- existsSync,
6
- mkdirSync,
7
- readFileSync,
8
- rmSync,
9
- writeFileSync,
10
- } from 'node:fs';
11
- import { join, resolve } from 'node:path';
12
-
13
- import type { Result } from '@ontrails/core';
14
- import { openReadTrailsDb } from '@ontrails/core/internal/trails-db';
15
- import { createDevStore } from '@ontrails/tracker';
16
-
17
- import { devCleanTrail } from '../trails/dev-clean.js';
18
- import { devResetTrail } from '../trails/dev-reset.js';
19
- import { devStatsTrail } from '../trails/dev-stats.js';
20
- import { guideTrail } from '../trails/guide.js';
21
- import { surveyTrail } from '../trails/survey.js';
22
- import { topoExportTrail } from '../trails/topo-export.js';
23
- import { topoHistoryTrail } from '../trails/topo-history.js';
24
- import { topoPinTrail } from '../trails/topo-pin.js';
25
- import { topoShowTrail } from '../trails/topo-show.js';
26
- import { topoTrail } from '../trails/topo.js';
27
- import { topoUnpinTrail } from '../trails/topo-unpin.js';
28
- import { topoVerifyTrail } from '../trails/topo-verify.js';
29
-
30
- const repoTempDir = (): string =>
31
- join(
32
- resolve(import.meta.dir, '../..'),
33
- '.tmp-tests',
34
- `topo-dev-${Date.now()}-${Math.random().toString(36).slice(2)}`
35
- );
36
-
37
- const expectOk = <T>(result: Result<T, Error>): T => {
38
- if (result.isErr()) {
39
- throw result.error;
40
- }
41
- return result.value;
42
- };
43
-
44
- const expectErr = <E extends Error>(result: Result<unknown, E>): E => {
45
- if (result.isOk()) {
46
- throw new Error('expected result to be an error');
47
- }
48
- return result.error;
49
- };
50
-
51
- const moduleInput = { module: './src/app.ts' } as const;
52
-
53
- const writeAppFixture = (dir: string): void => {
54
- mkdirSync(join(dir, 'src'), { recursive: true });
55
- writeFileSync(
56
- join(dir, 'src', 'app.ts'),
57
- `import { Result, provision, topo, trail } from '@ontrails/core';
58
- import { z } from 'zod';
59
-
60
- const hello = trail('hello', {
61
- blaze: async (input) => Result.ok({ message: \`Hello, \${input.name ?? 'world'}!\` }),
62
- crosses: ['goodbye'],
63
- examples: [{ input: {}, name: 'Default greeting' }],
64
- input: z.object({ name: z.string().optional() }),
65
- intent: 'read',
66
- output: z.object({ message: z.string() }),
67
- provisions: [
68
- provision('db.main', {
69
- create: () => Result.ok({ source: 'factory' }),
70
- }),
71
- ],
72
- });
73
-
74
- const goodbye = trail('goodbye', {
75
- blaze: async () => Result.ok({ ok: true }),
76
- input: z.object({}),
77
- intent: 'write',
78
- output: z.object({ ok: z.boolean() }),
79
- });
80
-
81
- const [dbMain] = hello.provisions;
82
- if (!dbMain) {
83
- throw new Error('expected hello to declare db.main');
84
- }
85
-
86
- export const app = topo('fixture-app', { dbMain, goodbye, hello });
87
- `
88
- );
89
- };
90
-
91
- describe('topo and dev trails', () => {
92
- test('topo surfaces current summary, detail, and export/verify flow', async () => {
93
- const dir = repoTempDir();
94
-
95
- try {
96
- writeAppFixture(dir);
97
-
98
- const summary = expectOk(
99
- await topoTrail.blaze(moduleInput, { cwd: dir } as never)
100
- );
101
- expect(summary.app.name).toBe('fixture-app');
102
- expect(summary.list.count).toBe(2);
103
- expect(summary.list.provisionCount).toBe(1);
104
- expect(summary.lockExists).toBe(false);
105
-
106
- const detail = expectOk(
107
- await topoShowTrail.blaze({ ...moduleInput, id: 'hello' }, {
108
- cwd: dir,
109
- } as never)
110
- );
111
- expect(detail.id).toBe('hello');
112
- expect(detail.provisions).toEqual(['db.main']);
113
-
114
- const exportResult = expectOk(
115
- await topoExportTrail.blaze(moduleInput, { cwd: dir } as never)
116
- );
117
- expect(exportResult.hash).toHaveLength(64);
118
- expect(existsSync(join(dir, '.trails', '_trailhead.json'))).toBe(true);
119
- expect(existsSync(join(dir, '.trails', 'trails.lock'))).toBe(true);
120
- expect(
121
- JSON.parse(readFileSync(join(dir, '.trails', 'trails.lock'), 'utf8'))
122
- ).toMatchObject({
123
- hash: exportResult.hash,
124
- version: 1,
125
- });
126
-
127
- writeFileSync(
128
- join(dir, '.trails', 'trailhead.lock'),
129
- readFileSync(join(dir, '.trails', 'trails.lock'), 'utf8')
130
- );
131
- rmSync(join(dir, '.trails', 'trails.lock'));
132
-
133
- const legacySummary = expectOk(
134
- await topoTrail.blaze(moduleInput, { cwd: dir } as never)
135
- );
136
- expect(legacySummary.lockExists).toBe(true);
137
-
138
- const verifyResult = expectOk(
139
- await topoVerifyTrail.blaze(moduleInput, { cwd: dir } as never)
140
- );
141
- expect(verifyResult.stale).toBe(false);
142
-
143
- writeFileSync(join(dir, '.trails', 'trailhead.lock'), 'stale\n');
144
- const verifyError = expectErr(
145
- await topoVerifyTrail.blaze(moduleInput, { cwd: dir } as never)
146
- );
147
- expect(verifyError.message).toContain('trails.lock is stale');
148
- } finally {
149
- rmSync(dir, { force: true, recursive: true });
150
- }
151
- });
152
-
153
- test('survey and guide read current topo state through the shared topo store', async () => {
154
- const dir = repoTempDir();
155
-
156
- try {
157
- writeAppFixture(dir);
158
-
159
- const surveyList = expectOk(
160
- await surveyTrail.blaze({ module: './src/app.ts' }, {
161
- cwd: dir,
162
- } as never)
163
- );
164
- expect(surveyList).toMatchObject({
165
- count: 2,
166
- provisionCount: 1,
167
- });
168
-
169
- const surveyBrief = expectOk(
170
- await surveyTrail.blaze({ brief: true, module: './src/app.ts' }, {
171
- cwd: dir,
172
- } as never)
173
- );
174
- expect(surveyBrief).toMatchObject({
175
- features: {
176
- examples: true,
177
- outputSchemas: true,
178
- provisions: true,
179
- },
180
- name: 'fixture-app',
181
- trails: 2,
182
- });
183
-
184
- const surveyDetail = expectOk(
185
- await surveyTrail.blaze({ module: './src/app.ts', trailId: 'hello' }, {
186
- cwd: dir,
187
- } as never)
188
- );
189
- expect(surveyDetail).toMatchObject({
190
- id: 'hello',
191
- provisions: ['db.main'],
192
- });
193
-
194
- const guideList = expectOk(
195
- await guideTrail.blaze({ module: './src/app.ts' }, {
196
- cwd: dir,
197
- } as never)
198
- );
199
- expect(guideList).toEqual([
200
- {
201
- description: '(no description)',
202
- exampleCount: 0,
203
- id: 'goodbye',
204
- kind: 'trail',
205
- },
206
- {
207
- description: '(no description)',
208
- exampleCount: 1,
209
- id: 'hello',
210
- kind: 'trail',
211
- },
212
- ]);
213
-
214
- const guideDetail = expectOk(
215
- await guideTrail.blaze({ module: './src/app.ts', trailId: 'hello' }, {
216
- cwd: dir,
217
- } as never)
218
- );
219
- expect(guideDetail).toMatchObject({
220
- description: null,
221
- examples: [
222
- {
223
- input: {},
224
- name: 'Default greeting',
225
- },
226
- ],
227
- id: 'hello',
228
- kind: 'trail',
229
- });
230
- } finally {
231
- rmSync(dir, { force: true, recursive: true });
232
- }
233
- });
234
-
235
- test('pinning, history, unpinning, and dev maintenance work against shared trails.db', async () => {
236
- const dir = repoTempDir();
237
-
238
- try {
239
- writeAppFixture(dir);
240
-
241
- const firstPin = expectOk(
242
- await topoPinTrail.blaze({ ...moduleInput, name: 'before-auth' }, {
243
- cwd: dir,
244
- } as never)
245
- );
246
- expect(firstPin.pin.name).toBe('before-auth');
247
- expect(firstPin.pin.saveId).toBe(firstPin.save.id);
248
-
249
- const firstExport = expectOk(
250
- await topoExportTrail.blaze(moduleInput, { cwd: dir } as never)
251
- );
252
- const secondExport = expectOk(
253
- await topoExportTrail.blaze(moduleInput, { cwd: dir } as never)
254
- );
255
- expect(firstExport.hash).toBe(secondExport.hash);
256
- expect(
257
- JSON.parse(readFileSync(join(dir, '.trails', 'trails.lock'), 'utf8'))
258
- ).toMatchObject({
259
- hash: secondExport.hash,
260
- version: 1,
261
- });
262
-
263
- const projectionDb = openReadTrailsDb({ rootDir: dir });
264
- try {
265
- const pinnedRows = projectionDb
266
- .query<{ count: number }, [string]>(
267
- 'SELECT COUNT(*) as count FROM topo_trails WHERE save_id = ?'
268
- )
269
- .get(firstPin.save.id);
270
- const exportedRows = projectionDb
271
- .query<{ count: number }, [string]>(
272
- 'SELECT COUNT(*) as count FROM topo_trails WHERE save_id = ?'
273
- )
274
- .get(firstExport.save.id);
275
- const projectedSaves = projectionDb
276
- .query<{ count: number }, []>(
277
- 'SELECT COUNT(DISTINCT save_id) as count FROM topo_trails'
278
- )
279
- .get();
280
- const cachedSchemas = projectionDb
281
- .query<{ count: number }, []>(
282
- 'SELECT COUNT(*) as count FROM topo_schemas'
283
- )
284
- .get();
285
-
286
- expect(pinnedRows?.count).toBe(2);
287
- expect(exportedRows?.count).toBe(2);
288
- expect(projectedSaves?.count).toBe(3);
289
- expect(cachedSchemas?.count).toBeGreaterThanOrEqual(9);
290
- } finally {
291
- projectionDb.close();
292
- }
293
-
294
- const store = createDevStore({ rootDir: dir });
295
- try {
296
- store.write({
297
- attrs: {},
298
- endedAt: Date.now() - 1000,
299
- id: 'track-1',
300
- kind: 'trail',
301
- name: 'hello',
302
- rootId: 'track-1',
303
- startedAt: Date.now() - 10_000,
304
- status: 'ok',
305
- traceId: 'trace-1',
306
- trailId: 'hello',
307
- trailhead: 'cli',
308
- });
309
- store.write({
310
- attrs: {},
311
- endedAt: Date.now() - 500,
312
- id: 'track-2',
313
- kind: 'trail',
314
- name: 'goodbye',
315
- rootId: 'track-2',
316
- startedAt: Date.now() - 20_000,
317
- status: 'err',
318
- traceId: 'trace-2',
319
- trailId: 'goodbye',
320
- trailhead: 'cli',
321
- });
322
- } finally {
323
- store.close();
324
- }
325
-
326
- const history = expectOk(
327
- await topoHistoryTrail.blaze({}, { cwd: dir } as never)
328
- );
329
- expect(history.pinCount).toBe(1);
330
- expect(history.saveCount).toBeGreaterThanOrEqual(3);
331
- expect(history.pins[0]?.saveId).toBe(firstPin.save.id);
332
- expect(
333
- history.saves.some((save) => save.id === secondExport.save.id)
334
- ).toBe(true);
335
-
336
- const stats = expectOk(
337
- await devStatsTrail.blaze({}, { cwd: dir } as never)
338
- );
339
- expect(stats.topo.pinCount).toBe(1);
340
- expect(stats.tracker.recordCount).toBe(2);
341
-
342
- const cleanPreview = expectOk(
343
- await devCleanTrail.blaze({ dryRun: true, saves: 0, trackAgeMs: 0 }, {
344
- cwd: dir,
345
- } as never)
346
- );
347
- expect(cleanPreview.dryRun).toBe(true);
348
- expect(cleanPreview.removed.topoSaves).toBeGreaterThanOrEqual(2);
349
- expect(cleanPreview.removed.trackRecords).toBe(2);
350
-
351
- const cleanResult = expectOk(
352
- await devCleanTrail.blaze(
353
- { dryRun: false, saves: 0, trackAgeMs: 0, yes: true },
354
- { cwd: dir } as never
355
- )
356
- );
357
- expect(cleanResult.removed.trackRecords).toBe(2);
358
- expect(cleanResult.remaining.pinCount).toBe(1);
359
-
360
- const unpinPreview = expectOk(
361
- await topoUnpinTrail.blaze({ dryRun: true, name: 'before-auth' }, {
362
- cwd: dir,
363
- } as never)
364
- );
365
- expect(unpinPreview.dryRun).toBe(true);
366
- expect(unpinPreview.pin?.name).toBe('before-auth');
367
-
368
- const unpinResult = expectOk(
369
- await topoUnpinTrail.blaze(
370
- { dryRun: false, name: 'before-auth', yes: true },
371
- { cwd: dir } as never
372
- )
373
- );
374
- expect(unpinResult.removed).toBe(true);
375
-
376
- const resetPreview = expectOk(
377
- await devResetTrail.blaze({ dryRun: true }, { cwd: dir } as never)
378
- );
379
- expect(resetPreview.dryRun).toBe(true);
380
- expect(resetPreview.removedFiles).toContain('.trails/trails.db');
381
-
382
- const resetResult = expectOk(
383
- await devResetTrail.blaze({ dryRun: false, yes: true }, {
384
- cwd: dir,
385
- } as never)
386
- );
387
- expect(resetResult.removedFiles).toContain('.trails/trails.db');
388
- expect(existsSync(join(dir, '.trails', 'trails.db'))).toBe(false);
389
- expect(
390
- readFileSync(join(dir, '.trails', 'trails.lock'), 'utf8').length
391
- ).toBeGreaterThan(0);
392
- } finally {
393
- rmSync(dir, { force: true, recursive: true });
394
- }
395
- });
396
-
397
- test('dev clean stays side-effect free when no local state exists', async () => {
398
- const dir = repoTempDir();
399
-
400
- try {
401
- mkdirSync(dir, { recursive: true });
402
-
403
- const preview = expectOk(
404
- await devCleanTrail.blaze({ dryRun: true }, { cwd: dir } as never)
405
- );
406
- expect(preview.dryRun).toBe(true);
407
- expect(preview.removed.topoSaves).toBe(0);
408
- expect(preview.removed.trackRecords).toBe(0);
409
- expect(existsSync(join(dir, '.trails', 'trails.db'))).toBe(false);
410
-
411
- const applied = expectOk(
412
- await devCleanTrail.blaze({ dryRun: false, yes: true }, {
413
- cwd: dir,
414
- } as never)
415
- );
416
- expect(applied.dryRun).toBe(false);
417
- expect(applied.removed.topoSaves).toBe(0);
418
- expect(applied.removed.trackRecords).toBe(0);
419
- expect(existsSync(join(dir, '.trails', 'trails.db'))).toBe(false);
420
- } finally {
421
- rmSync(dir, { force: true, recursive: true });
422
- }
423
- });
424
- });
@@ -1,74 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
-
6
- import { formatWardenReport, runWarden } from '@ontrails/warden';
7
-
8
- const makeTempDir = (): string => {
9
- const dir = join(
10
- tmpdir(),
11
- `trails-warden-test-${Date.now()}-${Math.random().toString(36).slice(2)}`
12
- );
13
- mkdirSync(dir, { recursive: true });
14
- return dir;
15
- };
16
-
17
- describe('trails warden', () => {
18
- test('runs lint + drift checks and produces a report', async () => {
19
- const dir = makeTempDir();
20
- try {
21
- writeFileSync(
22
- join(dir, 'good.ts'),
23
- `trail("hello", {
24
- blaze: async (input, ctx) => {
25
- return Result.ok({ message: "hi" });
26
- }
27
- })`
28
- );
29
-
30
- const report = await runWarden({ rootDir: dir });
31
- expect(report.diagnostics).toBeDefined();
32
- expect(typeof report.errorCount).toBe('number');
33
- expect(typeof report.warnCount).toBe('number');
34
- expect(typeof report.passed).toBe('boolean');
35
- } finally {
36
- rmSync(dir, { force: true, recursive: true });
37
- }
38
- });
39
-
40
- test('lintOnly skips drift detection', async () => {
41
- const dir = makeTempDir();
42
- try {
43
- writeFileSync(join(dir, 'empty.ts'), 'export {}');
44
- const report = await runWarden({ lintOnly: true, rootDir: dir });
45
- expect(report.drift).toBeNull();
46
- } finally {
47
- rmSync(dir, { force: true, recursive: true });
48
- }
49
- });
50
-
51
- test('driftOnly skips lint rules', async () => {
52
- const dir = makeTempDir();
53
- try {
54
- writeFileSync(
55
- join(dir, 'bad.ts'),
56
- `trail("x", {
57
- blaze: async () => { throw new Error("boom"); }
58
- })`
59
- );
60
- const report = await runWarden({ driftOnly: true, rootDir: dir });
61
- expect(report.diagnostics.length).toBe(0);
62
- expect(report.drift).not.toBeNull();
63
- } finally {
64
- rmSync(dir, { force: true, recursive: true });
65
- }
66
- });
67
-
68
- test('formatWardenReport produces human-readable output', async () => {
69
- const report = await runWarden({ rootDir: '/dev/null' });
70
- const output = formatWardenReport(report);
71
- expect(output).toContain('Warden Report');
72
- expect(typeof output).toBe('string');
73
- });
74
- });
@@ -1,121 +0,0 @@
1
- /**
2
- * `add.trailhead` trail -- Add a trailhead to an existing project.
3
- *
4
- * Generates the CLI or MCP entry point 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 { findTopoPath } from './project.js';
14
-
15
- const generateCliEntry = (appImportPath: string): string =>
16
- `import { trailhead } from '@ontrails/cli/commander';
17
-
18
- import { app } from '${appImportPath}';
19
-
20
- trailhead(app);
21
- `;
22
-
23
- const generateMcpEntry = (appImportPath: string): string =>
24
- `import { trailhead } from '@ontrails/mcp';
25
-
26
- import { app } from '${appImportPath}';
27
-
28
- await trailhead(app);
29
- `;
30
-
31
- /** Resolve the entry file for a trailhead. */
32
- const getEntryFile = (trailhead: 'cli' | 'mcp'): string =>
33
- trailhead === 'cli' ? 'src/cli.ts' : 'src/mcp.ts';
34
-
35
- // ---------------------------------------------------------------------------
36
- // Trail definition
37
- // ---------------------------------------------------------------------------
38
-
39
- /** Patch deps and optionally bin in a parsed package.json. */
40
- const patchPkgDeps = (
41
- pkg: Record<string, unknown>,
42
- trailhead: 'cli' | 'mcp',
43
- cwd: string
44
- ): string => {
45
- const depName = trailhead === 'cli' ? '@ontrails/cli' : '@ontrails/mcp';
46
- const deps = (pkg['dependencies'] ?? {}) as Record<string, string>;
47
- deps[depName] = 'workspace:*';
48
- if (trailhead === 'cli') {
49
- deps['commander'] = '^14.0.0';
50
- pkg['bin'] = {
51
- [(pkg['name'] as string | undefined) ?? basename(cwd)]: './src/cli.ts',
52
- };
53
- }
54
- pkg['dependencies'] = Object.fromEntries(
55
- Object.entries(deps).toSorted(([a], [b]) => a.localeCompare(b))
56
- );
57
- return depName;
58
- };
59
-
60
- /** Update package.json with trailhead dependency and CLI bin if needed. */
61
- const updatePkgJsonForTrailhead = async (
62
- cwd: string,
63
- trailhead: 'cli' | 'mcp'
64
- ): Promise<string> => {
65
- const pkgPath = join(cwd, 'package.json');
66
- if (!existsSync(pkgPath)) {
67
- return trailhead === 'cli' ? '@ontrails/cli' : '@ontrails/mcp';
68
- }
69
- const pkg = (await Bun.file(pkgPath).json()) as Record<string, unknown>;
70
- const depName = patchPkgDeps(pkg, trailhead, cwd);
71
- await Bun.write(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
72
- return depName;
73
- };
74
-
75
- /** Create the entry file for a trailhead and return the relative path. */
76
- const writeTrailheadEntry = async (
77
- cwd: string,
78
- trailhead: 'cli' | 'mcp'
79
- ): Promise<string> => {
80
- const entryFile = getEntryFile(trailhead);
81
- const fullEntryPath = join(cwd, entryFile);
82
- const appImport = (await findTopoPath(cwd)) ?? './app.js';
83
- const content =
84
- trailhead === 'cli'
85
- ? generateCliEntry(appImport)
86
- : generateMcpEntry(appImport);
87
-
88
- mkdirSync(dirname(fullEntryPath), { recursive: true });
89
- await Bun.write(fullEntryPath, content);
90
- return entryFile;
91
- };
92
-
93
- export const addTrailhead = trail('add.trailhead', {
94
- blaze: async (input) => {
95
- const cwd = resolve(input.dir ?? '.');
96
- const { trailhead } = input;
97
- const entryFile = getEntryFile(trailhead);
98
-
99
- if (existsSync(join(cwd, entryFile))) {
100
- return Result.err(
101
- new Error(
102
- `${trailhead.toUpperCase()} trailhead already exists. Nothing to do.`
103
- )
104
- );
105
- }
106
-
107
- return Result.ok({
108
- created: await writeTrailheadEntry(cwd, trailhead),
109
- dependency: await updatePkgJsonForTrailhead(cwd, trailhead),
110
- });
111
- },
112
- description: 'Add a trailhead to an existing project',
113
- input: z.object({
114
- dir: z.string().optional().describe('Project directory'),
115
- trailhead: z.enum(['cli', 'mcp']).describe('Trailhead to add'),
116
- }),
117
- output: z.object({
118
- created: z.string(),
119
- dependency: z.string(),
120
- }),
121
- });
@@ -1,39 +0,0 @@
1
- import { trail } from '@ontrails/core';
2
- import { z } from 'zod';
3
-
4
- import { loadApp } from './load-app.js';
5
- import { exportCurrentTopo } from './topo-store-support.js';
6
- import {
7
- DEFAULT_APP_MODULE,
8
- isolatedExampleInput,
9
- topoSaveOutput,
10
- } from './topo-support.js';
11
-
12
- export const topoExportTrail = trail('topo.export', {
13
- blaze: async (input, ctx) => {
14
- const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
15
- const app = await loadApp(input.module, rootDir);
16
- return exportCurrentTopo(app, { rootDir });
17
- },
18
- description: 'Export the current topo to .trails artifacts',
19
- examples: [
20
- {
21
- input: isolatedExampleInput('topo-export'),
22
- name: 'Write the current topo export',
23
- },
24
- ],
25
- input: z.object({
26
- module: z
27
- .string()
28
- .default(DEFAULT_APP_MODULE)
29
- .describe('Path to the app module'),
30
- rootDir: z.string().optional().describe('Workspace root directory'),
31
- }),
32
- intent: 'write',
33
- output: z.object({
34
- hash: z.string(),
35
- lockPath: z.string(),
36
- mapPath: z.string(),
37
- save: topoSaveOutput,
38
- }),
39
- });