@ontrails/trails 1.0.0-beta.13 → 1.0.0-beta.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +29 -0
  3. package/__tests__/examples.test.ts +39 -0
  4. package/dist/src/app.d.ts.map +1 -1
  5. package/dist/src/app.js +12 -1
  6. package/dist/src/app.js.map +1 -1
  7. package/dist/src/cli.js +4 -3
  8. package/dist/src/cli.js.map +1 -1
  9. package/dist/src/trails/add-surface.d.ts +3 -3
  10. package/dist/src/trails/add-surface.d.ts.map +1 -1
  11. package/dist/src/trails/add-surface.js +46 -24
  12. package/dist/src/trails/add-surface.js.map +1 -1
  13. package/dist/src/trails/add-trail.d.ts +3 -1
  14. package/dist/src/trails/add-trail.d.ts.map +1 -1
  15. package/dist/src/trails/add-trail.js +49 -22
  16. package/dist/src/trails/add-trail.js.map +1 -1
  17. package/dist/src/trails/add-trailhead.d.ts +13 -0
  18. package/dist/src/trails/add-trailhead.d.ts.map +1 -0
  19. package/dist/src/trails/add-trailhead.js +88 -0
  20. package/dist/src/trails/add-trailhead.js.map +1 -0
  21. package/dist/src/trails/add-verify.d.ts +1 -1
  22. package/dist/src/trails/add-verify.d.ts.map +1 -1
  23. package/dist/src/trails/add-verify.js +17 -16
  24. package/dist/src/trails/add-verify.js.map +1 -1
  25. package/dist/src/trails/create-scaffold.d.ts +1 -1
  26. package/dist/src/trails/create-scaffold.d.ts.map +1 -1
  27. package/dist/src/trails/create-scaffold.js +34 -27
  28. package/dist/src/trails/create-scaffold.js.map +1 -1
  29. package/dist/src/trails/create.d.ts +9 -13
  30. package/dist/src/trails/create.d.ts.map +1 -1
  31. package/dist/src/trails/create.js +40 -35
  32. package/dist/src/trails/create.js.map +1 -1
  33. package/dist/src/trails/dev-clean.d.ts +9 -0
  34. package/dist/src/trails/dev-clean.d.ts.map +1 -0
  35. package/dist/src/trails/dev-clean.js +66 -0
  36. package/dist/src/trails/dev-clean.js.map +1 -0
  37. package/dist/src/trails/dev-reset.d.ts +6 -0
  38. package/dist/src/trails/dev-reset.d.ts.map +1 -0
  39. package/dist/src/trails/dev-reset.js +39 -0
  40. package/dist/src/trails/dev-reset.js.map +1 -0
  41. package/dist/src/trails/dev-stats.d.ts +7 -0
  42. package/dist/src/trails/dev-stats.d.ts.map +1 -0
  43. package/dist/src/trails/dev-stats.js +61 -0
  44. package/dist/src/trails/dev-stats.js.map +1 -0
  45. package/dist/src/trails/dev-support.d.ts +64 -0
  46. package/dist/src/trails/dev-support.d.ts.map +1 -0
  47. package/dist/src/trails/dev-support.js +181 -0
  48. package/dist/src/trails/dev-support.js.map +1 -0
  49. package/dist/src/trails/draft-promote.d.ts +18 -0
  50. package/dist/src/trails/draft-promote.d.ts.map +1 -0
  51. package/dist/src/trails/draft-promote.js +400 -0
  52. package/dist/src/trails/draft-promote.js.map +1 -0
  53. package/dist/src/trails/guide.d.ts +14 -4
  54. package/dist/src/trails/guide.d.ts.map +1 -1
  55. package/dist/src/trails/guide.js +22 -41
  56. package/dist/src/trails/guide.js.map +1 -1
  57. package/dist/src/trails/load-app.d.ts +9 -1
  58. package/dist/src/trails/load-app.d.ts.map +1 -1
  59. package/dist/src/trails/load-app.js +404 -13
  60. package/dist/src/trails/load-app.js.map +1 -1
  61. package/dist/src/trails/project.d.ts.map +1 -1
  62. package/dist/src/trails/project.js +14 -3
  63. package/dist/src/trails/project.js.map +1 -1
  64. package/dist/src/trails/survey.d.ts +6 -60
  65. package/dist/src/trails/survey.d.ts.map +1 -1
  66. package/dist/src/trails/survey.js +83 -182
  67. package/dist/src/trails/survey.js.map +1 -1
  68. package/dist/src/trails/topo-constants.d.ts +3 -0
  69. package/dist/src/trails/topo-constants.d.ts.map +1 -0
  70. package/dist/src/trails/topo-constants.js +3 -0
  71. package/dist/src/trails/topo-constants.js.map +1 -0
  72. package/dist/src/trails/topo-export.d.ts +19 -0
  73. package/dist/src/trails/topo-export.d.ts.map +1 -0
  74. package/dist/src/trails/topo-export.js +31 -0
  75. package/dist/src/trails/topo-export.js.map +1 -0
  76. package/dist/src/trails/topo-history.d.ts +20 -0
  77. package/dist/src/trails/topo-history.d.ts.map +1 -0
  78. package/dist/src/trails/topo-history.js +32 -0
  79. package/dist/src/trails/topo-history.js.map +1 -0
  80. package/dist/src/trails/topo-pin.d.ts +17 -0
  81. package/dist/src/trails/topo-pin.d.ts.map +1 -0
  82. package/dist/src/trails/topo-pin.js +31 -0
  83. package/dist/src/trails/topo-pin.js.map +1 -0
  84. package/dist/src/trails/topo-read-support.d.ts +58 -0
  85. package/dist/src/trails/topo-read-support.d.ts.map +1 -0
  86. package/dist/src/trails/topo-read-support.js +167 -0
  87. package/dist/src/trails/topo-read-support.js.map +1 -0
  88. package/dist/src/trails/topo-reports.d.ts +54 -0
  89. package/dist/src/trails/topo-reports.d.ts.map +1 -0
  90. package/dist/src/trails/topo-reports.js +128 -0
  91. package/dist/src/trails/topo-reports.js.map +1 -0
  92. package/dist/src/trails/topo-show.d.ts +23 -0
  93. package/dist/src/trails/topo-show.d.ts.map +1 -0
  94. package/dist/src/trails/topo-show.js +49 -0
  95. package/dist/src/trails/topo-show.js.map +1 -0
  96. package/dist/src/trails/topo-store-support.d.ts +13 -0
  97. package/dist/src/trails/topo-store-support.d.ts.map +1 -0
  98. package/dist/src/trails/topo-store-support.js +55 -0
  99. package/dist/src/trails/topo-store-support.js.map +1 -0
  100. package/dist/src/trails/topo-support.d.ts +76 -0
  101. package/dist/src/trails/topo-support.d.ts.map +1 -0
  102. package/dist/src/trails/topo-support.js +132 -0
  103. package/dist/src/trails/topo-support.js.map +1 -0
  104. package/dist/src/trails/topo-unpin.d.ts +20 -0
  105. package/dist/src/trails/topo-unpin.d.ts.map +1 -0
  106. package/dist/src/trails/topo-unpin.js +44 -0
  107. package/dist/src/trails/topo-unpin.js.map +1 -0
  108. package/dist/src/trails/topo-verify.d.ts +5 -0
  109. package/dist/src/trails/topo-verify.d.ts.map +1 -0
  110. package/dist/src/trails/topo-verify.js +24 -0
  111. package/dist/src/trails/topo-verify.js.map +1 -0
  112. package/dist/src/trails/topo.d.ts +5 -0
  113. package/dist/src/trails/topo.d.ts.map +1 -0
  114. package/dist/src/trails/topo.js +63 -0
  115. package/dist/src/trails/topo.js.map +1 -0
  116. package/dist/src/trails/warden.d.ts +3 -2
  117. package/dist/src/trails/warden.d.ts.map +1 -1
  118. package/dist/src/trails/warden.js +37 -27
  119. package/dist/src/trails/warden.js.map +1 -1
  120. package/dist/src/versions.d.ts +12 -0
  121. package/dist/src/versions.d.ts.map +1 -0
  122. package/dist/src/versions.js +23 -0
  123. package/dist/src/versions.js.map +1 -0
  124. package/dist/tsconfig.tsbuildinfo +1 -1
  125. package/package.json +8 -7
  126. package/src/__tests__/add-trail.test.ts +97 -0
  127. package/src/__tests__/create.test.ts +91 -27
  128. package/src/__tests__/draft-promote.test.ts +144 -0
  129. package/src/__tests__/guide.test.ts +10 -5
  130. package/src/__tests__/load-app.test.ts +406 -2
  131. package/src/__tests__/survey.test.ts +221 -60
  132. package/src/__tests__/topo-dev.test.ts +426 -0
  133. package/src/app.ts +24 -2
  134. package/src/clack.ts +1 -1
  135. package/src/cli.ts +4 -3
  136. package/src/trails/add-surface.ts +150 -0
  137. package/src/trails/add-trail.ts +46 -10
  138. package/src/trails/add-verify.ts +11 -6
  139. package/src/trails/create-scaffold.ts +16 -3
  140. package/src/trails/create.ts +76 -71
  141. package/src/trails/dev-clean.ts +77 -0
  142. package/src/trails/dev-reset.ts +45 -0
  143. package/src/trails/dev-stats.ts +67 -0
  144. package/src/trails/dev-support.ts +328 -0
  145. package/src/trails/draft-promote.ts +739 -0
  146. package/src/trails/guide.ts +23 -41
  147. package/src/trails/load-app.ts +556 -14
  148. package/src/trails/project.ts +17 -3
  149. package/src/trails/survey.ts +110 -285
  150. package/src/trails/topo-constants.ts +2 -0
  151. package/src/trails/topo-export.ts +35 -0
  152. package/src/trails/topo-history.ts +38 -0
  153. package/src/trails/topo-pin.ts +38 -0
  154. package/src/trails/topo-read-support.ts +329 -0
  155. package/src/trails/topo-reports.ts +228 -0
  156. package/src/trails/topo-show.ts +54 -0
  157. package/src/trails/topo-store-support.ts +104 -0
  158. package/src/trails/topo-support.ts +230 -0
  159. package/src/trails/topo-unpin.ts +56 -0
  160. package/src/trails/topo-verify.ts +25 -0
  161. package/src/trails/topo.ts +69 -0
  162. package/src/trails/warden.ts +13 -3
  163. package/src/versions.ts +43 -0
  164. package/tsconfig.tests.json +10 -0
  165. package/src/trails/add-trailhead.ts +0 -121
@@ -0,0 +1,230 @@
1
+ import { existsSync, mkdirSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ import {
7
+ createTopoSnapshot as persistTopoSnapshot,
8
+ listTopoSnapshots as readTopoSnapshots,
9
+ pinTopoSnapshot,
10
+ unpinTopoSnapshot,
11
+ } from '@ontrails/core';
12
+ import type { Topo, TopoSnapshot } from '@ontrails/core';
13
+ import { deriveTrailsDbPath } from '@ontrails/core/internal/trails-db';
14
+ import { z } from 'zod';
15
+
16
+ import type { BriefReport, SurveyListReport } from './topo-reports.js';
17
+
18
+ /** Output schema for a topo snapshot record. Shared across topo trails. */
19
+ export const topoSnapshotOutput = z.object({
20
+ createdAt: z.string(),
21
+ gitDirty: z.boolean(),
22
+ gitSha: z.string().optional(),
23
+ id: z.string(),
24
+ pinnedAs: z.string().optional(),
25
+ resourceCount: z.number(),
26
+ signalCount: z.number(),
27
+ trailCount: z.number(),
28
+ });
29
+
30
+ export const DEFAULT_APP_MODULE = './src/app.ts';
31
+ export const DEFAULT_TOPO_HISTORY_LIMIT = 10;
32
+ export const LOCK_PATH = '.trails/trails.lock';
33
+ const EXAMPLE_APP_MODULE = fileURLToPath(new URL('../app.ts', import.meta.url));
34
+
35
+ export interface TopoSummaryReport {
36
+ readonly app: BriefReport;
37
+ readonly dbPath: string;
38
+ readonly list: SurveyListReport;
39
+ readonly lockExists: boolean;
40
+ readonly lockPath: string;
41
+ }
42
+
43
+ export interface TopoHistoryReport {
44
+ readonly dbPath: string;
45
+ readonly limit: number;
46
+ readonly pinnedCount: number;
47
+ readonly snapshotCount: number;
48
+ readonly snapshots: TopoSnapshot[];
49
+ }
50
+
51
+ export interface TopoExportReport {
52
+ readonly hash: string;
53
+ readonly lockPath: string;
54
+ readonly mapPath: string;
55
+ readonly snapshot: TopoSnapshot;
56
+ }
57
+
58
+ export interface TopoVerifyReport {
59
+ readonly committedHash: string;
60
+ readonly currentHash: string;
61
+ readonly lockPath: string;
62
+ readonly stale: false;
63
+ }
64
+
65
+ export const deriveRootDir = (cwd?: string): string => cwd ?? process.cwd();
66
+
67
+ const safeGit = (cwd: string, args: readonly string[]): string | undefined => {
68
+ const proc = Bun.spawnSync({
69
+ cmd: ['git', '-C', cwd, ...args],
70
+ stderr: 'ignore',
71
+ stdout: 'pipe',
72
+ });
73
+ if (!proc.success) {
74
+ return undefined;
75
+ }
76
+ const text = Buffer.from(proc.stdout).toString('utf8').trim();
77
+ return text.length === 0 ? undefined : text;
78
+ };
79
+
80
+ export const readGitState = (
81
+ rootDir: string
82
+ ): { readonly gitDirty: boolean; readonly gitSha?: string } => {
83
+ const gitSha = safeGit(rootDir, ['rev-parse', 'HEAD']);
84
+ const status = safeGit(rootDir, ['status', '--porcelain']);
85
+ return {
86
+ gitDirty: (status?.length ?? 0) > 0,
87
+ ...(gitSha === undefined ? {} : { gitSha }),
88
+ };
89
+ };
90
+
91
+ export const deriveTopoCounts = (
92
+ app: Topo
93
+ ): Pick<TopoSnapshot, 'resourceCount' | 'signalCount' | 'trailCount'> => ({
94
+ resourceCount: app.resources.size,
95
+ signalCount: app.signals.size,
96
+ trailCount: app.trails.size,
97
+ });
98
+
99
+ const emptyTopoHistory = (
100
+ dbPath: string,
101
+ limit: number
102
+ ): TopoHistoryReport => ({
103
+ dbPath,
104
+ limit,
105
+ pinnedCount: 0,
106
+ snapshotCount: 0,
107
+ snapshots: [],
108
+ });
109
+
110
+ const collectTopoHistory = (
111
+ dbPath: string,
112
+ limit: number,
113
+ snapshots: readonly TopoSnapshot[]
114
+ ): TopoHistoryReport => ({
115
+ dbPath,
116
+ limit,
117
+ pinnedCount: snapshots.filter((snapshot) => snapshot.pinnedAs !== undefined)
118
+ .length,
119
+ snapshotCount: snapshots.length,
120
+ snapshots: snapshots.slice(0, limit),
121
+ });
122
+
123
+ const buildSnapshotInput = (
124
+ app: Topo,
125
+ rootDir: string
126
+ ): {
127
+ readonly gitDirty: boolean;
128
+ readonly gitSha?: string;
129
+ readonly resourceCount: number;
130
+ readonly signalCount: number;
131
+ readonly trailCount: number;
132
+ } => ({
133
+ ...readGitState(rootDir),
134
+ ...deriveTopoCounts(app),
135
+ });
136
+
137
+ export const createIsolatedExampleInput = (
138
+ name: string
139
+ ): { readonly module: string; readonly rootDir: string } => {
140
+ const rootDir = join(tmpdir(), 'ontrails-trails-examples', name);
141
+ rmSync(rootDir, { force: true, recursive: true });
142
+ mkdirSync(rootDir, { recursive: true });
143
+ return {
144
+ module: EXAMPLE_APP_MODULE,
145
+ rootDir,
146
+ };
147
+ };
148
+
149
+ export const createCurrentTopoSnapshot = (
150
+ app: Topo,
151
+ options?: { readonly rootDir?: string }
152
+ ): TopoSnapshot => {
153
+ const rootDir = deriveRootDir(options?.rootDir);
154
+ const result = persistTopoSnapshot(app, {
155
+ rootDir,
156
+ ...buildSnapshotInput(app, rootDir),
157
+ });
158
+ if (result.isErr()) {
159
+ throw result.error;
160
+ }
161
+ return result.value;
162
+ };
163
+
164
+ export const listTopoHistory = (options?: {
165
+ readonly limit?: number;
166
+ readonly rootDir?: string;
167
+ }): TopoHistoryReport => {
168
+ const rootDir = deriveRootDir(options?.rootDir);
169
+ const limit = options?.limit ?? DEFAULT_TOPO_HISTORY_LIMIT;
170
+ const dbPath = deriveTrailsDbPath({ rootDir });
171
+ if (!existsSync(dbPath)) {
172
+ return emptyTopoHistory(dbPath, limit);
173
+ }
174
+
175
+ return collectTopoHistory(dbPath, limit, readTopoSnapshots({ rootDir }));
176
+ };
177
+
178
+ export const pinCurrentTopoSnapshot = (
179
+ app: Topo,
180
+ input: { readonly name: string; readonly rootDir?: string }
181
+ ): { readonly snapshot: TopoSnapshot } => {
182
+ const rootDir = deriveRootDir(input.rootDir);
183
+ const created = persistTopoSnapshot(app, {
184
+ rootDir,
185
+ ...buildSnapshotInput(app, rootDir),
186
+ });
187
+ if (created.isErr()) {
188
+ throw created.error;
189
+ }
190
+
191
+ const snapshot = pinTopoSnapshot(created.value.id, input.name, {
192
+ rootDir,
193
+ });
194
+ if (snapshot === undefined) {
195
+ throw new Error(`Missing topo snapshot "${created.value.id}" to pin`);
196
+ }
197
+
198
+ return { snapshot };
199
+ };
200
+
201
+ export const removePinnedTopoSnapshot = (input: {
202
+ readonly dryRun: boolean;
203
+ readonly name: string;
204
+ readonly rootDir?: string;
205
+ }): {
206
+ readonly dryRun: boolean;
207
+ readonly removed: boolean;
208
+ readonly snapshot?: TopoSnapshot;
209
+ } => {
210
+ const rootDir = deriveRootDir(input.rootDir);
211
+ if (!existsSync(deriveTrailsDbPath({ rootDir }))) {
212
+ return { dryRun: input.dryRun, removed: false };
213
+ }
214
+
215
+ if (input.dryRun) {
216
+ const snapshot = readTopoSnapshots({ pinned: true, rootDir }).find(
217
+ (candidate) => candidate.pinnedAs === input.name
218
+ );
219
+ return snapshot === undefined
220
+ ? { dryRun: true, removed: false }
221
+ : { dryRun: true, removed: false, snapshot };
222
+ }
223
+
224
+ const snapshot = unpinTopoSnapshot(input.name, { rootDir });
225
+ return {
226
+ dryRun: false,
227
+ removed: snapshot !== undefined,
228
+ ...(snapshot === undefined ? {} : { snapshot }),
229
+ };
230
+ };
@@ -0,0 +1,56 @@
1
+ import { Result, ValidationError, trail } from '@ontrails/core';
2
+ import { z } from 'zod';
3
+
4
+ import {
5
+ createIsolatedExampleInput,
6
+ removePinnedTopoSnapshot,
7
+ topoSnapshotOutput,
8
+ } from './topo-support.js';
9
+
10
+ export const topoUnpinTrail = trail('topo.unpin', {
11
+ blaze: (input, ctx) => {
12
+ if (input.dryRun !== true && input.yes !== true) {
13
+ return Result.err(
14
+ new ValidationError(
15
+ 'Refusing to remove a pin without `--yes` or `--dry-run`.'
16
+ )
17
+ );
18
+ }
19
+
20
+ const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
21
+ return Result.ok(
22
+ removePinnedTopoSnapshot({
23
+ dryRun: input.dryRun,
24
+ name: input.name,
25
+ rootDir,
26
+ })
27
+ );
28
+ },
29
+ description: 'Remove a named topo pin',
30
+ examples: [
31
+ {
32
+ input: {
33
+ ...createIsolatedExampleInput('topo-unpin'),
34
+ dryRun: true,
35
+ name: 'before-auth-refactor',
36
+ },
37
+ name: 'Preview pin removal',
38
+ },
39
+ ],
40
+ input: z.object({
41
+ dryRun: z
42
+ .boolean()
43
+ .default(true)
44
+ .describe('Preview the removal without changing state'),
45
+ name: z.string().describe('Pin name'),
46
+ rootDir: z.string().optional().describe('Workspace root directory'),
47
+ yes: z.boolean().default(false).describe('Confirm destructive changes'),
48
+ }),
49
+ intent: 'destroy',
50
+ output: z.object({
51
+ dryRun: z.boolean(),
52
+ removed: z.boolean(),
53
+ snapshot: topoSnapshotOutput.optional(),
54
+ }),
55
+ permit: { scopes: ['topo:delete'] },
56
+ });
@@ -0,0 +1,25 @@
1
+ import { trail } from '@ontrails/core';
2
+ import { z } from 'zod';
3
+
4
+ import { loadApp } from './load-app.js';
5
+ import { verifyCurrentTopo } from './topo-read-support.js';
6
+
7
+ export const topoVerifyTrail = trail('topo.verify', {
8
+ blaze: async (input, ctx) => {
9
+ const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
10
+ const app = await loadApp(input.module, rootDir);
11
+ return verifyCurrentTopo(app, { rootDir });
12
+ },
13
+ description: 'Verify that the committed lockfile matches the current topo',
14
+ input: z.object({
15
+ module: z.string().optional().describe('Path to the app module'),
16
+ rootDir: z.string().optional().describe('Workspace root directory'),
17
+ }),
18
+ intent: 'read',
19
+ output: z.object({
20
+ committedHash: z.string(),
21
+ currentHash: z.string(),
22
+ lockPath: z.string(),
23
+ stale: z.literal(false),
24
+ }),
25
+ });
@@ -0,0 +1,69 @@
1
+ import { Result, trail } from '@ontrails/core';
2
+ import { z } from 'zod';
3
+
4
+ import { loadApp } from './load-app.js';
5
+ import { buildTopoSummary } from './topo-read-support.js';
6
+
7
+ const summaryOutput = z.object({
8
+ app: z.object({
9
+ contractVersion: z.string(),
10
+ features: z.object({
11
+ detours: z.boolean(),
12
+ examples: z.boolean(),
13
+ outputSchemas: z.boolean(),
14
+ resources: z.boolean(),
15
+ signals: z.boolean(),
16
+ }),
17
+ name: z.string(),
18
+ resources: z.number(),
19
+ signals: z.number(),
20
+ trails: z.number(),
21
+ version: z.string(),
22
+ }),
23
+ dbPath: z.string(),
24
+ list: z.object({
25
+ count: z.number(),
26
+ entries: z.array(
27
+ z.object({
28
+ examples: z.number(),
29
+ id: z.string(),
30
+ kind: z.string(),
31
+ safety: z.string(),
32
+ })
33
+ ),
34
+ resourceCount: z.number(),
35
+ resources: z.array(
36
+ z.object({
37
+ description: z.string().nullable(),
38
+ health: z.enum(['available', 'none']),
39
+ id: z.string(),
40
+ kind: z.literal('resource'),
41
+ lifetime: z.literal('singleton'),
42
+ usedBy: z.array(z.string()),
43
+ })
44
+ ),
45
+ }),
46
+ lockExists: z.boolean(),
47
+ lockPath: z.string(),
48
+ });
49
+
50
+ export const topoTrail = trail('topo', {
51
+ blaze: async (input, ctx) => {
52
+ const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
53
+ const app = await loadApp(input.module, rootDir);
54
+ return Result.ok(buildTopoSummary(app, { rootDir }));
55
+ },
56
+ description: 'Show the current topo summary and entry list',
57
+ examples: [
58
+ {
59
+ input: {},
60
+ name: 'Show the current topo summary',
61
+ },
62
+ ],
63
+ input: z.object({
64
+ module: z.string().optional().describe('Path to the app module'),
65
+ rootDir: z.string().optional().describe('Workspace root directory'),
66
+ }),
67
+ intent: 'read',
68
+ output: summaryOutput,
69
+ });
@@ -23,9 +23,14 @@ import { loadApp } from './load-app.js';
23
23
  export const wardenTrail = trail('warden', {
24
24
  blaze: async (input, ctx) => {
25
25
  const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
26
- // oxlint-disable-next-line prefer-await-to-then -- catch converts rejection to undefined cleanly
27
- const topo = await loadApp('./src/app.ts', rootDir).catch(
28
- (): undefined => undefined
26
+ // oxlint-disable-next-line prefer-await-to-then -- catch preserves graceful degradation
27
+ const topo = await loadApp(input.module, rootDir).catch(
28
+ (error: unknown): undefined => {
29
+ ctx.logger?.warn('Could not load app for topo-aware governance', {
30
+ error: error instanceof Error ? error.message : String(error),
31
+ });
32
+ return undefined;
33
+ }
29
34
  );
30
35
 
31
36
  const report = await runWarden({
@@ -76,6 +81,10 @@ export const wardenTrail = trail('warden', {
76
81
  .default('text')
77
82
  .describe('Output format: text, json, github, or summary'),
78
83
  lintOnly: z.boolean().default(false).describe('Only run lint rules'),
84
+ module: z
85
+ .string()
86
+ .optional()
87
+ .describe('App module path (auto-discovered if omitted)'),
79
88
  rootDir: z.string().optional().describe('Root directory to scan'),
80
89
  }),
81
90
  intent: 'read',
@@ -91,6 +100,7 @@ export const wardenTrail = trail('warden', {
91
100
  ),
92
101
  drift: z
93
102
  .object({
103
+ blockedReason: z.string().optional(),
94
104
  committedHash: z.string().nullable(),
95
105
  currentHash: z.string(),
96
106
  stale: z.boolean(),
@@ -0,0 +1,43 @@
1
+ interface PackageJson {
2
+ readonly catalog?: Record<string, string>;
3
+ readonly dependencies?: Record<string, string>;
4
+ readonly devDependencies?: Record<string, string>;
5
+ readonly version?: string;
6
+ }
7
+
8
+ const readPackageJson = async (url: URL): Promise<PackageJson> =>
9
+ (await Bun.file(url).json()) as PackageJson;
10
+
11
+ const appPackageJson = await readPackageJson(
12
+ new URL('../package.json', import.meta.url)
13
+ );
14
+ const rootPackageJson = await readPackageJson(
15
+ new URL('../../../package.json', import.meta.url)
16
+ );
17
+
18
+ const requireVersion = (value: string | undefined, label: string): string => {
19
+ if (typeof value !== 'string' || value.length === 0) {
20
+ throw new Error(`Missing version for ${label}`);
21
+ }
22
+ return value;
23
+ };
24
+
25
+ const rootCatalog = rootPackageJson.catalog ?? {};
26
+ const rootDevDependencies = rootPackageJson.devDependencies ?? {};
27
+
28
+ export const trailsPackageVersion = requireVersion(
29
+ appPackageJson.version,
30
+ '@ontrails/trails'
31
+ );
32
+
33
+ export const ontrailsPackageRange = `^${trailsPackageVersion}`;
34
+
35
+ export const scaffoldDependencyVersions = {
36
+ bunTypes: requireVersion(rootDevDependencies['@types/bun'], '@types/bun'),
37
+ commander: requireVersion(rootCatalog['commander'], 'commander'),
38
+ lefthook: requireVersion(rootDevDependencies['lefthook'], 'lefthook'),
39
+ oxlint: requireVersion(rootDevDependencies['oxlint'], 'oxlint'),
40
+ typescript: requireVersion(rootDevDependencies['typescript'], 'typescript'),
41
+ ultracite: requireVersion(rootDevDependencies['ultracite'], 'ultracite'),
42
+ zod: requireVersion(rootCatalog['zod'], 'zod'),
43
+ } as const;
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "rootDir": ".",
6
+ "types": ["bun"]
7
+ },
8
+ "include": ["src/**/*.test.ts", "src/__tests__/**/*.ts", "__tests__/**/*.ts"],
9
+ "exclude": []
10
+ }
@@ -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
- });