@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,89 +0,0 @@
1
- /**
2
- * `warden` trail -- Governance checks.
3
- *
4
- * Thin wrapper around `runWarden` and `formatWardenReport` from @ontrails/warden.
5
- */
6
- import { Result, trail } from '@ontrails/core';
7
- import { formatGitHubAnnotations, formatJson, formatSummary, formatWardenReport, runWarden, } from '@ontrails/warden';
8
- import { z } from 'zod';
9
- import { loadApp } from './load-app.js';
10
- // ---------------------------------------------------------------------------
11
- // Trail definition
12
- // ---------------------------------------------------------------------------
13
- export const wardenTrail = trail('warden', {
14
- blaze: async (input, ctx) => {
15
- const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
16
- // oxlint-disable-next-line prefer-await-to-then -- catch converts rejection to undefined cleanly
17
- const topo = await loadApp('./src/app.ts', rootDir).catch(() => undefined);
18
- const report = await runWarden({
19
- driftOnly: input.driftOnly,
20
- lintOnly: input.lintOnly,
21
- rootDir,
22
- topo,
23
- });
24
- const formatters = {
25
- github: formatGitHubAnnotations,
26
- json: formatJson,
27
- summary: formatSummary,
28
- text: formatWardenReport,
29
- };
30
- const formatter = formatters[input.format] ?? formatWardenReport;
31
- const formatted = formatter(report);
32
- return Result.ok({
33
- diagnostics: report.diagnostics,
34
- drift: report.drift,
35
- errorCount: report.errorCount,
36
- formatted,
37
- passed: report.passed,
38
- warnCount: report.warnCount,
39
- });
40
- },
41
- description: 'Run governance checks (lint + drift)',
42
- examples: [
43
- {
44
- input: {
45
- driftOnly: false,
46
- lintOnly: false,
47
- },
48
- name: 'Default warden run',
49
- },
50
- {
51
- input: {
52
- format: 'github',
53
- },
54
- name: 'GitHub Actions annotations',
55
- },
56
- ],
57
- input: z.object({
58
- driftOnly: z.boolean().default(false).describe('Only run drift detection'),
59
- format: z
60
- .enum(['text', 'json', 'github', 'summary'])
61
- .default('text')
62
- .describe('Output format: text, json, github, or summary'),
63
- lintOnly: z.boolean().default(false).describe('Only run lint rules'),
64
- rootDir: z.string().optional().describe('Root directory to scan'),
65
- }),
66
- intent: 'read',
67
- output: z.object({
68
- diagnostics: z.array(z.object({
69
- filePath: z.string(),
70
- line: z.number(),
71
- message: z.string(),
72
- rule: z.string(),
73
- severity: z.enum(['error', 'warn']),
74
- })),
75
- drift: z
76
- .object({
77
- blockedReason: z.string().optional(),
78
- committedHash: z.string().nullable(),
79
- currentHash: z.string(),
80
- stale: z.boolean(),
81
- })
82
- .nullable(),
83
- errorCount: z.number(),
84
- formatted: z.string(),
85
- passed: z.boolean(),
86
- warnCount: z.number(),
87
- }),
88
- });
89
- //# sourceMappingURL=warden.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"warden.js","sourceRoot":"","sources":["../../../src/trails/warden.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EACL,uBAAuB,EACvB,UAAU,EACV,aAAa,EACb,kBAAkB,EAClB,SAAS,GACV,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAExC,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,CAAC,QAAQ,EAAE;IACzC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QAC1B,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,GAAG,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;QAC1D,iGAAiG;QACjG,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,KAAK,CACvD,GAAc,EAAE,CAAC,SAAS,CAC3B,CAAC;QAEF,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC;YAC7B,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,OAAO;YACP,IAAI;SACL,CAAC,CAAC;QAEH,MAAM,UAAU,GAAiD;YAC/D,MAAM,EAAE,uBAAuB;YAC/B,IAAI,EAAE,UAAU;YAChB,OAAO,EAAE,aAAa;YACtB,IAAI,EAAE,kBAAkB;SACzB,CAAC;QACF,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,kBAAkB,CAAC;QACjE,MAAM,SAAS,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;QAEpC,OAAO,MAAM,CAAC,EAAE,CAAC;YACf,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,SAAS;YACT,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,SAAS,EAAE,MAAM,CAAC,SAAS;SAC5B,CAAC,CAAC;IACL,CAAC;IACD,WAAW,EAAE,sCAAsC;IACnD,QAAQ,EAAE;QACR;YACE,KAAK,EAAE;gBACL,SAAS,EAAE,KAAK;gBAChB,QAAQ,EAAE,KAAK;aAChB;YACD,IAAI,EAAE,oBAAoB;SAC3B;QACD;YACE,KAAK,EAAE;gBACL,MAAM,EAAE,QAAQ;aACjB;YACD,IAAI,EAAE,4BAA4B;SACnC;KACF;IACD,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC;QACd,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,0BAA0B,CAAC;QAC1E,MAAM,EAAE,CAAC;aACN,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;aAC3C,OAAO,CAAC,MAAM,CAAC;aACf,QAAQ,CAAC,+CAA+C,CAAC;QAC5D,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,qBAAqB,CAAC;QACpE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wBAAwB,CAAC;KAClE,CAAC;IACF,MAAM,EAAE,MAAM;IACd,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,WAAW,EAAE,CAAC,CAAC,KAAK,CAClB,CAAC,CAAC,MAAM,CAAC;YACP,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;YACpB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;YAChB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;YACnB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;YAChB,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;SACpC,CAAC,CACH;QACD,KAAK,EAAE,CAAC;aACL,MAAM,CAAC;YACN,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;YACpC,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;YACpC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;YACvB,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE;SACnB,CAAC;aACD,QAAQ,EAAE;QACb,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;QACtB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;QACrB,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE;QACnB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;KACtB,CAAC;CACH,CAAC,CAAC"}
@@ -1 +0,0 @@
1
- {"root":["../src/app.ts","../src/clack.ts","../src/cli.ts","../src/trails/add-trail.ts","../src/trails/add-trailhead.ts","../src/trails/add-verify.ts","../src/trails/create-scaffold.ts","../src/trails/create.ts","../src/trails/dev-clean.ts","../src/trails/dev-reset.ts","../src/trails/dev-stats.ts","../src/trails/dev-support.ts","../src/trails/draft-promote.ts","../src/trails/guide.ts","../src/trails/load-app.ts","../src/trails/project.ts","../src/trails/survey.ts","../src/trails/topo-constants.ts","../src/trails/topo-export.ts","../src/trails/topo-history.ts","../src/trails/topo-pin.ts","../src/trails/topo-read-support.ts","../src/trails/topo-reports.ts","../src/trails/topo-show.ts","../src/trails/topo-store-support.ts","../src/trails/topo-support.ts","../src/trails/topo-unpin.ts","../src/trails/topo-verify.ts","../src/trails/topo.ts","../src/trails/warden.ts","../bin/trails.ts"],"version":"5.9.3"}
@@ -1,351 +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 { addTrailhead } from '../trails/add-trailhead.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
-
20
- type Starter = 'empty' | 'entity' | 'hello';
21
- type Trailhead = 'cli' | 'mcp';
22
-
23
- const makeTempProject = (): string =>
24
- join(
25
- tmpdir(),
26
- `trails-create-test-${Date.now()}-${Math.random().toString(36).slice(2)}`
27
- );
28
-
29
- const readJson = (dir: string, relativePath: string): Record<string, unknown> =>
30
- JSON.parse(readFileSync(join(dir, relativePath), 'utf8')) as Record<
31
- string,
32
- unknown
33
- >;
34
-
35
- const readText = (dir: string, relativePath: string): string =>
36
- readFileSync(join(dir, relativePath), 'utf8');
37
-
38
- const expectPaths = (
39
- dir: string,
40
- relativePaths: readonly string[],
41
- expected: boolean
42
- ): void => {
43
- for (const relativePath of relativePaths) {
44
- expect(existsSync(join(dir, relativePath))).toBe(expected);
45
- }
46
- };
47
-
48
- const expectContainsAll = (
49
- content: string,
50
- snippets: readonly string[]
51
- ): void => {
52
- for (const snippet of snippets) {
53
- expect(content).toContain(snippet);
54
- }
55
- };
56
-
57
- const expectOk = <T>(result: Result<T, Error>): T => {
58
- if (result.isErr()) {
59
- throw result.error;
60
- }
61
- return result.value;
62
- };
63
-
64
- const expectErr = <T>(result: Result<T, Error>): Error => {
65
- if (result.isOk()) {
66
- throw new Error('Expected error result');
67
- }
68
- return result.error;
69
- };
70
-
71
- const runCross = async (
72
- id: string,
73
- input: unknown
74
- ): Promise<Result<unknown, Error>> => {
75
- switch (id) {
76
- case 'create.scaffold': {
77
- return await createScaffold.blaze(input as never, {} as never);
78
- }
79
- case 'add.trailhead': {
80
- return await addTrailhead.blaze(input as never, {} as never);
81
- }
82
- case 'add.verify': {
83
- return await addVerify.blaze(input as never, {} as never);
84
- }
85
- default: {
86
- return Result.err(new Error(`Unknown cross target: ${id}`));
87
- }
88
- }
89
- };
90
-
91
- const runCreate = (
92
- projectDir: string,
93
- overrides?: Partial<{
94
- starter: Starter;
95
- trailheads: readonly Trailhead[];
96
- verify: boolean;
97
- }>
98
- ) =>
99
- createRoute.blaze(
100
- {
101
- dir: dirname(projectDir),
102
- name: basename(projectDir),
103
- starter: overrides?.starter ?? 'hello',
104
- trailheads: [...(overrides?.trailheads ?? ['cli'])],
105
- verify: overrides?.verify ?? true,
106
- },
107
- { cross: runCross } as never
108
- );
109
-
110
- const setupMinimalProject = (dir: string): void => {
111
- mkdirSync(join(dir, 'src'), { recursive: true });
112
- mkdirSync(join(dir, '.trails'), { recursive: true });
113
- writeFileSync(
114
- join(dir, 'src', 'app.ts'),
115
- "import { topo } from '@ontrails/core';\nexport const app = topo('test');\n"
116
- );
117
- writeFileSync(
118
- join(dir, 'package.json'),
119
- JSON.stringify(
120
- { dependencies: { '@ontrails/core': 'workspace:*' }, name: 'test' },
121
- null,
122
- 2
123
- )
124
- );
125
- };
126
-
127
- const assertDefaultProjectFiles = (dir: string): void => {
128
- expectPaths(
129
- dir,
130
- [
131
- 'package.json',
132
- 'tsconfig.json',
133
- '.gitignore',
134
- '.oxlintrc.json',
135
- '.oxfmtrc.jsonc',
136
- 'src/app.ts',
137
- '.trails',
138
- 'src/cli.ts',
139
- 'src/trails/hello.ts',
140
- '__tests__/examples.test.ts',
141
- 'lefthook.yml',
142
- ],
143
- true
144
- );
145
- };
146
-
147
- const assertCliPackage = (dir: string): void => {
148
- const pkg = readJson(dir, 'package.json');
149
- expect(pkg['name']).toBe(basename(dir));
150
-
151
- 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();
155
-
156
- const devDeps = pkg['devDependencies'] as Record<string, string>;
157
- expect(devDeps['@ontrails/testing']).toBe('workspace:*');
158
- expect(devDeps['@ontrails/warden']).toBe('workspace:*');
159
- };
160
-
161
- const assertHelloApp = (dir: string): void => {
162
- expectContainsAll(readText(dir, 'src/app.ts'), [
163
- 'topo',
164
- `'${basename(dir)}'`,
165
- 'hello',
166
- ]);
167
- expectContainsAll(readText(dir, 'src/trails/hello.ts'), [
168
- "import { Result, trail } from '@ontrails/core'",
169
- 'return Result.ok({ message:',
170
- ]);
171
- };
172
-
173
- const assertEntityStarter = (dir: string): void => {
174
- expectPaths(
175
- dir,
176
- [
177
- 'src/trails/entity.ts',
178
- 'src/trails/search.ts',
179
- 'src/trails/onboard.ts',
180
- 'src/signals/entity-signals.ts',
181
- 'src/store.ts',
182
- ],
183
- true
184
- );
185
- expectPaths(dir, ['src/trails/hello.ts'], false);
186
- expectContainsAll(readText(dir, 'src/app.ts'), [
187
- "import * as entity from './trails/entity.js'",
188
- "import * as search from './trails/search.js'",
189
- "import * as onboard from './trails/onboard.js'",
190
- "import * as entitySignals from './signals/entity-signals.js'",
191
- ]);
192
- expectContainsAll(readText(dir, 'src/trails/entity.ts'), [
193
- "import { Result, trail } from '@ontrails/core'",
194
- 'return Result.ok({ id: input.id, name:',
195
- "return Result.ok({ id: '1', name: input.name })",
196
- ]);
197
- expectContainsAll(readText(dir, 'src/trails/search.ts'), [
198
- "import { Result, trail } from '@ontrails/core'",
199
- 'return Result.ok({ results: [] })',
200
- ]);
201
- expectContainsAll(readText(dir, 'src/trails/onboard.ts'), [
202
- "import { Result, trail } from '@ontrails/core'",
203
- 'return Result.ok({ onboarded: true })',
204
- ]);
205
- };
206
-
207
- const assertMcpTrailhead = (dir: string): void => {
208
- expectPaths(dir, ['src/mcp.ts'], true);
209
- expectPaths(dir, ['src/cli.ts'], false);
210
- expectContainsAll(readText(dir, 'src/mcp.ts'), [
211
- "import { trailhead } from '@ontrails/mcp'",
212
- 'await trailhead(app)',
213
- ]);
214
-
215
- const deps = readJson(dir, 'package.json')['dependencies'] as Record<
216
- string,
217
- string
218
- >;
219
- expect(deps['@ontrails/mcp']).toBe('workspace:*');
220
- expect(deps['@ontrails/cli']).toBeUndefined();
221
- };
222
-
223
- const assertVerifySkipped = (dir: string): void => {
224
- expectPaths(dir, ['__tests__/examples.test.ts', 'lefthook.yml'], false);
225
- expect(readJson(dir, 'package.json')['devDependencies']).toBeUndefined();
226
- };
227
-
228
- const assertEmptyStarter = (dir: string): void => {
229
- expectPaths(dir, ['src/trails/.gitkeep'], true);
230
- expectPaths(dir, ['src/trails/hello.ts'], false);
231
- const appContent = readText(dir, 'src/app.ts');
232
- expect(appContent).toContain(`topo('${basename(dir)}')`);
233
- expect(appContent).not.toContain('import * as');
234
- };
235
-
236
- const withTempProject = async (
237
- assertion: (dir: string) => Promise<void>
238
- ): Promise<void> => {
239
- const dir = makeTempProject();
240
- try {
241
- await assertion(dir);
242
- } finally {
243
- rmSync(dir, { force: true, recursive: true });
244
- }
245
- };
246
-
247
- describe('trails create', () => {
248
- describe('create mode', () => {
249
- test('generates project structure with defaults', async () => {
250
- await withTempProject(async (dir) => {
251
- expectOk(await runCreate(dir));
252
- assertDefaultProjectFiles(dir);
253
- assertCliPackage(dir);
254
- assertHelloApp(dir);
255
- });
256
- });
257
-
258
- test('generates with entity starter', async () => {
259
- await withTempProject(async (dir) => {
260
- expectOk(await runCreate(dir, { starter: 'entity' }));
261
- assertEntityStarter(dir);
262
- });
263
- });
264
-
265
- test('generates with MCP trailhead', async () => {
266
- await withTempProject(async (dir) => {
267
- expectOk(await runCreate(dir, { trailheads: ['mcp'] }));
268
- assertMcpTrailhead(dir);
269
- });
270
- });
271
-
272
- test('skips verification when verify is false', async () => {
273
- await withTempProject(async (dir) => {
274
- expectOk(await runCreate(dir, { verify: false }));
275
- assertVerifySkipped(dir);
276
- });
277
- });
278
-
279
- test('generates with empty starter', async () => {
280
- await withTempProject(async (dir) => {
281
- expectOk(await runCreate(dir, { starter: 'empty' }));
282
- assertEmptyStarter(dir);
283
- });
284
- });
285
- });
286
-
287
- describe('add-trailhead mode', () => {
288
- test('adds MCP to existing project', async () => {
289
- await withTempProject(async (dir) => {
290
- setupMinimalProject(dir);
291
- const result = expectOk(
292
- await addTrailhead.blaze({ dir, trailhead: 'mcp' }, {} as never)
293
- );
294
-
295
- expect(result.created).toBe('src/mcp.ts');
296
- expect(result.dependency).toBe('@ontrails/mcp');
297
- expectPaths(dir, ['src/mcp.ts'], true);
298
- expectContainsAll(readText(dir, 'src/mcp.ts'), [
299
- "import { trailhead } from '@ontrails/mcp'",
300
- ]);
301
- const deps = readJson(dir, 'package.json')['dependencies'] as Record<
302
- string,
303
- string
304
- >;
305
- expect(deps['@ontrails/mcp']).toBe('workspace:*');
306
- });
307
- });
308
-
309
- test('detects existing trailhead entrypoint', async () => {
310
- await withTempProject(async (dir) => {
311
- mkdirSync(join(dir, 'src'), { recursive: true });
312
- mkdirSync(join(dir, '.trails'), { recursive: true });
313
- writeFileSync(join(dir, 'src', 'mcp.ts'), 'existing content');
314
-
315
- const error = expectErr(
316
- await addTrailhead.blaze({ dir, trailhead: 'mcp' }, {} as never)
317
- );
318
- expect(error.message).toBe(
319
- 'MCP trailhead already exists. Nothing to do.'
320
- );
321
- });
322
- });
323
- });
324
- });
325
-
326
- describe('isInsideProject', () => {
327
- test('detects .trails directory', async () => {
328
- await withTempProject(async (dir) => {
329
- mkdirSync(join(dir, '.trails'), { recursive: true });
330
- expect(await isInsideProject(dir)).toBe(true);
331
- });
332
- });
333
-
334
- test('detects topo in src/', async () => {
335
- await withTempProject(async (dir) => {
336
- mkdirSync(join(dir, 'src'), { recursive: true });
337
- writeFileSync(
338
- join(dir, 'src', 'app.ts'),
339
- "import { topo } from '@ontrails/core';\nexport const app = topo('app');\n"
340
- );
341
- expect(await isInsideProject(dir)).toBe(true);
342
- });
343
- });
344
-
345
- test('returns false for empty directory', async () => {
346
- await withTempProject(async (dir) => {
347
- mkdirSync(dir, { recursive: true });
348
- expect(await isInsideProject(dir)).toBe(false);
349
- });
350
- });
351
- });
@@ -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,91 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
-
3
- import type { AnyTrail } from '@ontrails/core';
4
- import { 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
- NotFoundError: ['search'],
19
- },
20
- examples: [
21
- {
22
- expected: { message: 'Hello, world!' },
23
- input: {},
24
- name: 'Default greeting',
25
- },
26
- {
27
- expected: { message: 'Hello, Trails!' },
28
- input: { name: 'Trails' },
29
- name: 'Named greeting',
30
- },
31
- ],
32
- input: z.object({ name: z.string().optional() }),
33
- intent: 'read',
34
- output: z.object({ message: z.string() }),
35
- });
36
-
37
- const app = topo('test-app', { hello: helloTrail });
38
-
39
- // ---------------------------------------------------------------------------
40
- // Tests
41
- // ---------------------------------------------------------------------------
42
-
43
- describe('trails guide', () => {
44
- test('lists trails with descriptions', () => {
45
- const items = app.list();
46
- expect(items.length).toBe(1);
47
-
48
- const [hello] = items;
49
- expect(hello).toBeDefined();
50
- expect(hello?.id).toBe('hello');
51
-
52
- expect((hello as AnyTrail).description).toBe('Say hello');
53
- });
54
-
55
- test('trail detail includes examples', () => {
56
- const item = app.get('hello') as AnyTrail;
57
- expect(item).toBeDefined();
58
- expect(item.examples).toBeDefined();
59
- expect(item.examples?.length).toBe(2);
60
- expect(item.examples?.[0]?.name).toBe('Default greeting');
61
- });
62
-
63
- test('JSON output for trail is valid', () => {
64
- const t = app.get('hello') as AnyTrail;
65
- expect(t).toBeDefined();
66
-
67
- const json = JSON.stringify({
68
- description: t.description,
69
- detours: t.detours,
70
- examples: t.examples,
71
- id: t.id,
72
- kind: t.kind,
73
- });
74
-
75
- const parsed = JSON.parse(json) as Record<string, unknown>;
76
- expect(parsed['id']).toBe('hello');
77
- expect(parsed['kind']).toBe('trail');
78
- expect(parsed['description']).toBe('Say hello');
79
- });
80
-
81
- test('non-existent trail returns undefined from topo', () => {
82
- const item = app.get('does-not-exist');
83
- expect(item).toBeUndefined();
84
- });
85
-
86
- test('detours are accessible on trail', () => {
87
- const t = app.get('hello') as AnyTrail;
88
- expect(t).toBeDefined();
89
- expect(t.detours?.['NotFoundError']).toEqual(['search']);
90
- });
91
- });