@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.
- package/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +29 -0
- package/__tests__/examples.test.ts +39 -0
- package/dist/src/app.d.ts.map +1 -1
- package/dist/src/app.js +12 -1
- package/dist/src/app.js.map +1 -1
- package/dist/src/cli.js +4 -3
- package/dist/src/cli.js.map +1 -1
- package/dist/src/trails/add-surface.d.ts +3 -3
- package/dist/src/trails/add-surface.d.ts.map +1 -1
- package/dist/src/trails/add-surface.js +46 -24
- package/dist/src/trails/add-surface.js.map +1 -1
- package/dist/src/trails/add-trail.d.ts +3 -1
- package/dist/src/trails/add-trail.d.ts.map +1 -1
- package/dist/src/trails/add-trail.js +49 -22
- package/dist/src/trails/add-trail.js.map +1 -1
- package/dist/src/trails/add-trailhead.d.ts +13 -0
- package/dist/src/trails/add-trailhead.d.ts.map +1 -0
- package/dist/src/trails/add-trailhead.js +88 -0
- package/dist/src/trails/add-trailhead.js.map +1 -0
- package/dist/src/trails/add-verify.d.ts +1 -1
- package/dist/src/trails/add-verify.d.ts.map +1 -1
- package/dist/src/trails/add-verify.js +17 -16
- package/dist/src/trails/add-verify.js.map +1 -1
- package/dist/src/trails/create-scaffold.d.ts +1 -1
- package/dist/src/trails/create-scaffold.d.ts.map +1 -1
- package/dist/src/trails/create-scaffold.js +34 -27
- package/dist/src/trails/create-scaffold.js.map +1 -1
- package/dist/src/trails/create.d.ts +9 -13
- package/dist/src/trails/create.d.ts.map +1 -1
- package/dist/src/trails/create.js +40 -35
- package/dist/src/trails/create.js.map +1 -1
- package/dist/src/trails/dev-clean.d.ts +9 -0
- package/dist/src/trails/dev-clean.d.ts.map +1 -0
- package/dist/src/trails/dev-clean.js +66 -0
- package/dist/src/trails/dev-clean.js.map +1 -0
- package/dist/src/trails/dev-reset.d.ts +6 -0
- package/dist/src/trails/dev-reset.d.ts.map +1 -0
- package/dist/src/trails/dev-reset.js +39 -0
- package/dist/src/trails/dev-reset.js.map +1 -0
- package/dist/src/trails/dev-stats.d.ts +7 -0
- package/dist/src/trails/dev-stats.d.ts.map +1 -0
- package/dist/src/trails/dev-stats.js +61 -0
- package/dist/src/trails/dev-stats.js.map +1 -0
- package/dist/src/trails/dev-support.d.ts +64 -0
- package/dist/src/trails/dev-support.d.ts.map +1 -0
- package/dist/src/trails/dev-support.js +181 -0
- package/dist/src/trails/dev-support.js.map +1 -0
- package/dist/src/trails/draft-promote.d.ts +18 -0
- package/dist/src/trails/draft-promote.d.ts.map +1 -0
- package/dist/src/trails/draft-promote.js +400 -0
- package/dist/src/trails/draft-promote.js.map +1 -0
- package/dist/src/trails/guide.d.ts +14 -4
- package/dist/src/trails/guide.d.ts.map +1 -1
- package/dist/src/trails/guide.js +22 -41
- package/dist/src/trails/guide.js.map +1 -1
- package/dist/src/trails/load-app.d.ts +9 -1
- package/dist/src/trails/load-app.d.ts.map +1 -1
- package/dist/src/trails/load-app.js +404 -13
- package/dist/src/trails/load-app.js.map +1 -1
- package/dist/src/trails/project.d.ts.map +1 -1
- package/dist/src/trails/project.js +14 -3
- package/dist/src/trails/project.js.map +1 -1
- package/dist/src/trails/survey.d.ts +6 -60
- package/dist/src/trails/survey.d.ts.map +1 -1
- package/dist/src/trails/survey.js +83 -182
- package/dist/src/trails/survey.js.map +1 -1
- package/dist/src/trails/topo-constants.d.ts +3 -0
- package/dist/src/trails/topo-constants.d.ts.map +1 -0
- package/dist/src/trails/topo-constants.js +3 -0
- package/dist/src/trails/topo-constants.js.map +1 -0
- package/dist/src/trails/topo-export.d.ts +19 -0
- package/dist/src/trails/topo-export.d.ts.map +1 -0
- package/dist/src/trails/topo-export.js +31 -0
- package/dist/src/trails/topo-export.js.map +1 -0
- package/dist/src/trails/topo-history.d.ts +20 -0
- package/dist/src/trails/topo-history.d.ts.map +1 -0
- package/dist/src/trails/topo-history.js +32 -0
- package/dist/src/trails/topo-history.js.map +1 -0
- package/dist/src/trails/topo-pin.d.ts +17 -0
- package/dist/src/trails/topo-pin.d.ts.map +1 -0
- package/dist/src/trails/topo-pin.js +31 -0
- package/dist/src/trails/topo-pin.js.map +1 -0
- package/dist/src/trails/topo-read-support.d.ts +58 -0
- package/dist/src/trails/topo-read-support.d.ts.map +1 -0
- package/dist/src/trails/topo-read-support.js +167 -0
- package/dist/src/trails/topo-read-support.js.map +1 -0
- package/dist/src/trails/topo-reports.d.ts +54 -0
- package/dist/src/trails/topo-reports.d.ts.map +1 -0
- package/dist/src/trails/topo-reports.js +128 -0
- package/dist/src/trails/topo-reports.js.map +1 -0
- package/dist/src/trails/topo-show.d.ts +23 -0
- package/dist/src/trails/topo-show.d.ts.map +1 -0
- package/dist/src/trails/topo-show.js +49 -0
- package/dist/src/trails/topo-show.js.map +1 -0
- package/dist/src/trails/topo-store-support.d.ts +13 -0
- package/dist/src/trails/topo-store-support.d.ts.map +1 -0
- package/dist/src/trails/topo-store-support.js +55 -0
- package/dist/src/trails/topo-store-support.js.map +1 -0
- package/dist/src/trails/topo-support.d.ts +76 -0
- package/dist/src/trails/topo-support.d.ts.map +1 -0
- package/dist/src/trails/topo-support.js +132 -0
- package/dist/src/trails/topo-support.js.map +1 -0
- package/dist/src/trails/topo-unpin.d.ts +20 -0
- package/dist/src/trails/topo-unpin.d.ts.map +1 -0
- package/dist/src/trails/topo-unpin.js +44 -0
- package/dist/src/trails/topo-unpin.js.map +1 -0
- package/dist/src/trails/topo-verify.d.ts +5 -0
- package/dist/src/trails/topo-verify.d.ts.map +1 -0
- package/dist/src/trails/topo-verify.js +24 -0
- package/dist/src/trails/topo-verify.js.map +1 -0
- package/dist/src/trails/topo.d.ts +5 -0
- package/dist/src/trails/topo.d.ts.map +1 -0
- package/dist/src/trails/topo.js +63 -0
- package/dist/src/trails/topo.js.map +1 -0
- package/dist/src/trails/warden.d.ts +3 -2
- package/dist/src/trails/warden.d.ts.map +1 -1
- package/dist/src/trails/warden.js +37 -27
- package/dist/src/trails/warden.js.map +1 -1
- package/dist/src/versions.d.ts +12 -0
- package/dist/src/versions.d.ts.map +1 -0
- package/dist/src/versions.js +23 -0
- package/dist/src/versions.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/src/__tests__/add-trail.test.ts +97 -0
- package/src/__tests__/create.test.ts +91 -27
- package/src/__tests__/draft-promote.test.ts +144 -0
- package/src/__tests__/guide.test.ts +10 -5
- package/src/__tests__/load-app.test.ts +406 -2
- package/src/__tests__/survey.test.ts +221 -60
- package/src/__tests__/topo-dev.test.ts +426 -0
- package/src/app.ts +24 -2
- package/src/clack.ts +1 -1
- package/src/cli.ts +4 -3
- package/src/trails/add-surface.ts +150 -0
- package/src/trails/add-trail.ts +46 -10
- package/src/trails/add-verify.ts +11 -6
- package/src/trails/create-scaffold.ts +16 -3
- package/src/trails/create.ts +76 -71
- package/src/trails/dev-clean.ts +77 -0
- package/src/trails/dev-reset.ts +45 -0
- package/src/trails/dev-stats.ts +67 -0
- package/src/trails/dev-support.ts +328 -0
- package/src/trails/draft-promote.ts +739 -0
- package/src/trails/guide.ts +23 -41
- package/src/trails/load-app.ts +556 -14
- package/src/trails/project.ts +17 -3
- package/src/trails/survey.ts +110 -285
- package/src/trails/topo-constants.ts +2 -0
- package/src/trails/topo-export.ts +35 -0
- package/src/trails/topo-history.ts +38 -0
- package/src/trails/topo-pin.ts +38 -0
- package/src/trails/topo-read-support.ts +329 -0
- package/src/trails/topo-reports.ts +228 -0
- package/src/trails/topo-show.ts +54 -0
- package/src/trails/topo-store-support.ts +104 -0
- package/src/trails/topo-support.ts +230 -0
- package/src/trails/topo-unpin.ts +56 -0
- package/src/trails/topo-verify.ts +25 -0
- package/src/trails/topo.ts +69 -0
- package/src/trails/warden.ts +13 -3
- package/src/versions.ts +43 -0
- package/tsconfig.tests.json +10 -0
- 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
|
+
});
|
package/src/trails/warden.ts
CHANGED
|
@@ -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
|
|
27
|
-
const topo = await loadApp(
|
|
28
|
-
(): 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(),
|
package/src/versions.ts
ADDED
|
@@ -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;
|
|
@@ -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
|
-
});
|