@ontrails/trails 1.0.0-beta.14 → 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 +13 -0
- package/__tests__/examples.test.ts +29 -4
- package/dist/src/app.js +2 -2
- package/dist/src/app.js.map +1 -1
- package/dist/src/clack.d.ts +1 -1
- package/dist/src/clack.js +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 +38 -11
- package/dist/src/trails/add-trail.js.map +1 -1
- package/dist/src/trails/add-trailhead.d.ts +1 -1
- package/dist/src/trails/add-trailhead.d.ts.map +1 -1
- package/dist/src/trails/add-trailhead.js +4 -4
- 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 +7 -6
- 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 +10 -3
- package/dist/src/trails/create-scaffold.js.map +1 -1
- package/dist/src/trails/create.d.ts +10 -14
- package/dist/src/trails/create.d.ts.map +1 -1
- package/dist/src/trails/create.js +42 -37
- package/dist/src/trails/create.js.map +1 -1
- package/dist/src/trails/dev-clean.d.ts +4 -4
- package/dist/src/trails/dev-clean.d.ts.map +1 -1
- package/dist/src/trails/dev-clean.js +22 -21
- package/dist/src/trails/dev-clean.js.map +1 -1
- package/dist/src/trails/dev-reset.d.ts +1 -1
- package/dist/src/trails/dev-reset.d.ts.map +1 -1
- package/dist/src/trails/dev-reset.js +3 -2
- package/dist/src/trails/dev-reset.js.map +1 -1
- package/dist/src/trails/dev-stats.d.ts +4 -4
- package/dist/src/trails/dev-stats.d.ts.map +1 -1
- package/dist/src/trails/dev-stats.js +19 -19
- package/dist/src/trails/dev-stats.js.map +1 -1
- package/dist/src/trails/dev-support.d.ts +17 -17
- package/dist/src/trails/dev-support.d.ts.map +1 -1
- package/dist/src/trails/dev-support.js +60 -57
- package/dist/src/trails/dev-support.js.map +1 -1
- package/dist/src/trails/draft-promote.d.ts +1 -1
- package/dist/src/trails/draft-promote.d.ts.map +1 -1
- package/dist/src/trails/draft-promote.js +30 -16
- package/dist/src/trails/draft-promote.js.map +1 -1
- package/dist/src/trails/guide.d.ts +2 -2
- package/dist/src/trails/guide.d.ts.map +1 -1
- package/dist/src/trails/guide.js +1 -4
- package/dist/src/trails/guide.js.map +1 -1
- package/dist/src/trails/load-app.d.ts +7 -1
- package/dist/src/trails/load-app.d.ts.map +1 -1
- package/dist/src/trails/load-app.js +374 -26
- package/dist/src/trails/load-app.js.map +1 -1
- package/dist/src/trails/survey.d.ts +5 -5
- package/dist/src/trails/survey.d.ts.map +1 -1
- package/dist/src/trails/survey.js +51 -29
- package/dist/src/trails/survey.js.map +1 -1
- package/dist/src/trails/topo-export.d.ts +5 -4
- package/dist/src/trails/topo-export.d.ts.map +1 -1
- package/dist/src/trails/topo-export.js +4 -7
- package/dist/src/trails/topo-export.js.map +1 -1
- package/dist/src/trails/topo-history.d.ts +6 -10
- package/dist/src/trails/topo-history.d.ts.map +1 -1
- package/dist/src/trails/topo-history.js +7 -8
- package/dist/src/trails/topo-history.js.map +1 -1
- package/dist/src/trails/topo-pin.d.ts +5 -9
- package/dist/src/trails/topo-pin.d.ts.map +1 -1
- package/dist/src/trails/topo-pin.js +5 -9
- package/dist/src/trails/topo-pin.js.map +1 -1
- package/dist/src/trails/topo-read-support.d.ts +12 -8
- package/dist/src/trails/topo-read-support.d.ts.map +1 -1
- package/dist/src/trails/topo-read-support.js +71 -82
- package/dist/src/trails/topo-read-support.js.map +1 -1
- package/dist/src/trails/topo-reports.d.ts +15 -11
- package/dist/src/trails/topo-reports.d.ts.map +1 -1
- package/dist/src/trails/topo-reports.js +40 -34
- package/dist/src/trails/topo-reports.js.map +1 -1
- package/dist/src/trails/topo-show.d.ts +4 -4
- package/dist/src/trails/topo-show.d.ts.map +1 -1
- package/dist/src/trails/topo-show.js +8 -12
- package/dist/src/trails/topo-show.js.map +1 -1
- package/dist/src/trails/topo-store-support.d.ts +2 -2
- package/dist/src/trails/topo-store-support.d.ts.map +1 -1
- package/dist/src/trails/topo-store-support.js +22 -22
- package/dist/src/trails/topo-store-support.js.map +1 -1
- package/dist/src/trails/topo-support.d.ts +19 -30
- package/dist/src/trails/topo-support.d.ts.map +1 -1
- package/dist/src/trails/topo-support.js +63 -96
- package/dist/src/trails/topo-support.js.map +1 -1
- package/dist/src/trails/topo-unpin.d.ts +9 -4
- package/dist/src/trails/topo-unpin.d.ts.map +1 -1
- package/dist/src/trails/topo-unpin.js +9 -4
- package/dist/src/trails/topo-unpin.js.map +1 -1
- package/dist/src/trails/topo-verify.d.ts +2 -2
- package/dist/src/trails/topo-verify.d.ts.map +1 -1
- package/dist/src/trails/topo-verify.js +1 -5
- package/dist/src/trails/topo-verify.js.map +1 -1
- package/dist/src/trails/topo.d.ts +2 -2
- package/dist/src/trails/topo.d.ts.map +1 -1
- package/dist/src/trails/topo.js +6 -10
- package/dist/src/trails/topo.js.map +1 -1
- 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 +11 -2
- 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 -8
- package/src/__tests__/add-trail.test.ts +97 -0
- package/src/__tests__/create.test.ts +91 -27
- package/src/__tests__/guide.test.ts +10 -5
- package/src/__tests__/load-app.test.ts +365 -4
- package/src/__tests__/survey.test.ts +143 -67
- package/src/__tests__/topo-dev.test.ts +48 -46
- package/src/app.ts +2 -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 +25 -21
- package/src/trails/dev-reset.ts +3 -2
- package/src/trails/dev-stats.ts +22 -19
- package/src/trails/dev-support.ts +88 -86
- package/src/trails/draft-promote.ts +70 -35
- package/src/trails/guide.ts +1 -4
- package/src/trails/load-app.ts +511 -32
- package/src/trails/survey.ts +60 -35
- package/src/trails/topo-export.ts +5 -9
- package/src/trails/topo-history.ts +8 -10
- package/src/trails/topo-pin.ts +9 -13
- package/src/trails/topo-read-support.ts +103 -106
- package/src/trails/topo-reports.ts +54 -47
- package/src/trails/topo-show.ts +8 -12
- package/src/trails/topo-store-support.ts +36 -28
- package/src/trails/topo-support.ts +90 -134
- package/src/trails/topo-unpin.ts +11 -6
- package/src/trails/topo-verify.ts +1 -5
- package/src/trails/topo.ts +6 -10
- package/src/trails/warden.ts +12 -3
- package/src/versions.ts +43 -0
- package/tsconfig.tests.json +10 -0
- package/src/trails/add-trailhead.ts +0 -121
|
@@ -1,52 +1,60 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stored-export pipeline for topo persistence.
|
|
3
3
|
*
|
|
4
|
-
* Extracted from topo-support.ts
|
|
5
|
-
* keeping
|
|
4
|
+
* Extracted from topo-support.ts to isolate store persistence concerns,
|
|
5
|
+
* keeping module boundaries clean.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { Topo } from '@ontrails/core';
|
|
8
|
+
import type { Topo, TopoSnapshot } from '@ontrails/core';
|
|
9
9
|
import { InternalError, Result } from '@ontrails/core';
|
|
10
|
-
import type { TopoSaveRecord } from '@ontrails/core/internal/topo-saves';
|
|
11
10
|
import type { StoredTopoExport } from '@ontrails/core/internal/topo-store';
|
|
12
11
|
import {
|
|
12
|
+
createTopoSnapshot,
|
|
13
13
|
getStoredTopoExport,
|
|
14
|
-
persistEstablishedTopoSave,
|
|
15
14
|
} from '@ontrails/core/internal/topo-store';
|
|
16
15
|
import {
|
|
17
16
|
openWriteTrailsDb,
|
|
18
|
-
|
|
17
|
+
deriveTrailsDir,
|
|
19
18
|
} from '@ontrails/core/internal/trails-db';
|
|
20
|
-
import type {
|
|
21
|
-
import {
|
|
19
|
+
import type { SurfaceLock, SurfaceMap } from '@ontrails/schema';
|
|
20
|
+
import { writeSurfaceLock, writeSurfaceMap } from '@ontrails/schema';
|
|
22
21
|
|
|
23
22
|
import type { TopoExportReport } from './topo-support.js';
|
|
24
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
deriveRootDir,
|
|
25
|
+
deriveTopoCounts,
|
|
26
|
+
readGitState,
|
|
27
|
+
} from './topo-support.js';
|
|
25
28
|
|
|
26
29
|
const persistAndReadStoredExport = (
|
|
27
30
|
app: Topo,
|
|
28
31
|
db: ReturnType<typeof openWriteTrailsDb>,
|
|
29
32
|
rootDir: string
|
|
30
|
-
): Result<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
): Result<
|
|
34
|
+
{ snapshot: TopoSnapshot; storedExport: StoredTopoExport },
|
|
35
|
+
Error
|
|
36
|
+
> => {
|
|
37
|
+
const snapshotResult = createTopoSnapshot(db, app, {
|
|
38
|
+
...readGitState(rootDir),
|
|
39
|
+
...deriveTopoCounts(app),
|
|
34
40
|
});
|
|
35
|
-
if (
|
|
36
|
-
return
|
|
41
|
+
if (snapshotResult.isErr()) {
|
|
42
|
+
return snapshotResult;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
|
-
const
|
|
40
|
-
const storedExport = getStoredTopoExport(db,
|
|
45
|
+
const snapshot = snapshotResult.value;
|
|
46
|
+
const storedExport = getStoredTopoExport(db, snapshot.id);
|
|
41
47
|
|
|
42
48
|
if (storedExport === undefined) {
|
|
43
49
|
return Result.err(
|
|
44
|
-
new InternalError(
|
|
50
|
+
new InternalError(
|
|
51
|
+
`Missing stored topo export for snapshot "${snapshot.id}"`
|
|
52
|
+
)
|
|
45
53
|
);
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
return Result.ok({
|
|
49
|
-
|
|
57
|
+
snapshot,
|
|
50
58
|
storedExport,
|
|
51
59
|
});
|
|
52
60
|
};
|
|
@@ -55,17 +63,17 @@ const writeStoredExportArtifacts = async (
|
|
|
55
63
|
storedExport: StoredTopoExport,
|
|
56
64
|
trailsDir: string
|
|
57
65
|
): Promise<Pick<TopoExportReport, 'hash' | 'lockPath' | 'mapPath'>> => {
|
|
58
|
-
const mapPath = await
|
|
59
|
-
JSON.parse(storedExport.
|
|
66
|
+
const mapPath = await writeSurfaceMap(
|
|
67
|
+
JSON.parse(storedExport.surfaceMapJson) as SurfaceMap,
|
|
60
68
|
{ dir: trailsDir }
|
|
61
69
|
);
|
|
62
|
-
const lockPath = await
|
|
63
|
-
JSON.parse(storedExport.lockContent) as
|
|
70
|
+
const lockPath = await writeSurfaceLock(
|
|
71
|
+
JSON.parse(storedExport.lockContent) as SurfaceLock,
|
|
64
72
|
{ dir: trailsDir }
|
|
65
73
|
);
|
|
66
74
|
|
|
67
75
|
return {
|
|
68
|
-
hash: storedExport.
|
|
76
|
+
hash: storedExport.surfaceHash,
|
|
69
77
|
lockPath,
|
|
70
78
|
mapPath,
|
|
71
79
|
};
|
|
@@ -75,7 +83,7 @@ export const exportCurrentTopo = async (
|
|
|
75
83
|
app: Topo,
|
|
76
84
|
options?: { readonly rootDir?: string }
|
|
77
85
|
): Promise<Result<TopoExportReport, Error>> => {
|
|
78
|
-
const rootDir =
|
|
86
|
+
const rootDir = deriveRootDir(options?.rootDir);
|
|
79
87
|
const db = openWriteTrailsDb({ rootDir });
|
|
80
88
|
|
|
81
89
|
try {
|
|
@@ -84,12 +92,12 @@ export const exportCurrentTopo = async (
|
|
|
84
92
|
return persisted;
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
const {
|
|
95
|
+
const { snapshot, storedExport } = persisted.value;
|
|
88
96
|
const artifacts = await writeStoredExportArtifacts(
|
|
89
97
|
storedExport,
|
|
90
|
-
|
|
98
|
+
deriveTrailsDir({ rootDir })
|
|
91
99
|
);
|
|
92
|
-
return Result.ok({ ...artifacts,
|
|
100
|
+
return Result.ok({ ...artifacts, snapshot });
|
|
93
101
|
} finally {
|
|
94
102
|
db.close();
|
|
95
103
|
}
|
|
@@ -3,60 +3,33 @@ import { tmpdir } from 'node:os';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
6
|
-
import type { Topo } from '@ontrails/core';
|
|
7
|
-
import type {
|
|
8
|
-
TopoPinRecord,
|
|
9
|
-
TopoSaveRecord,
|
|
10
|
-
} from '@ontrails/core/internal/topo-saves';
|
|
11
6
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from '@ontrails/core
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
openReadTrailsDb,
|
|
21
|
-
openWriteTrailsDb,
|
|
22
|
-
resolveTrailsDbPath,
|
|
23
|
-
} from '@ontrails/core/internal/trails-db';
|
|
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';
|
|
24
14
|
import { z } from 'zod';
|
|
25
15
|
|
|
26
16
|
import type { BriefReport, SurveyListReport } from './topo-reports.js';
|
|
27
17
|
|
|
28
|
-
/** Output schema for a topo
|
|
29
|
-
export const
|
|
18
|
+
/** Output schema for a topo snapshot record. Shared across topo trails. */
|
|
19
|
+
export const topoSnapshotOutput = z.object({
|
|
30
20
|
createdAt: z.string(),
|
|
31
21
|
gitDirty: z.boolean(),
|
|
32
22
|
gitSha: z.string().optional(),
|
|
33
23
|
id: z.string(),
|
|
34
|
-
|
|
24
|
+
pinnedAs: z.string().optional(),
|
|
25
|
+
resourceCount: z.number(),
|
|
35
26
|
signalCount: z.number(),
|
|
36
27
|
trailCount: z.number(),
|
|
37
28
|
});
|
|
38
29
|
|
|
39
|
-
/** Output schema for a topo pin record. Shared across topo trails. */
|
|
40
|
-
export const topoPinOutput = z.object({
|
|
41
|
-
createdAt: z.string(),
|
|
42
|
-
name: z.string(),
|
|
43
|
-
saveId: z.string(),
|
|
44
|
-
});
|
|
45
|
-
|
|
46
30
|
export const DEFAULT_APP_MODULE = './src/app.ts';
|
|
47
31
|
export const DEFAULT_TOPO_HISTORY_LIMIT = 10;
|
|
48
32
|
export const LOCK_PATH = '.trails/trails.lock';
|
|
49
|
-
export const LEGACY_LOCK_PATH = '.trails/trailhead.lock';
|
|
50
|
-
|
|
51
|
-
/** Resolve the lockfile path, preferring the current name with legacy fallback. */
|
|
52
|
-
export const resolveLockPath = (trailsDir: string): string => {
|
|
53
|
-
const primary = join(trailsDir, 'trails.lock');
|
|
54
|
-
if (existsSync(primary)) {
|
|
55
|
-
return primary;
|
|
56
|
-
}
|
|
57
|
-
const legacy = join(trailsDir, 'trailhead.lock');
|
|
58
|
-
return existsSync(legacy) ? legacy : primary;
|
|
59
|
-
};
|
|
60
33
|
const EXAMPLE_APP_MODULE = fileURLToPath(new URL('../app.ts', import.meta.url));
|
|
61
34
|
|
|
62
35
|
export interface TopoSummaryReport {
|
|
@@ -70,17 +43,16 @@ export interface TopoSummaryReport {
|
|
|
70
43
|
export interface TopoHistoryReport {
|
|
71
44
|
readonly dbPath: string;
|
|
72
45
|
readonly limit: number;
|
|
73
|
-
readonly
|
|
74
|
-
readonly
|
|
75
|
-
readonly
|
|
76
|
-
readonly saves: TopoSaveRecord[];
|
|
46
|
+
readonly pinnedCount: number;
|
|
47
|
+
readonly snapshotCount: number;
|
|
48
|
+
readonly snapshots: TopoSnapshot[];
|
|
77
49
|
}
|
|
78
50
|
|
|
79
51
|
export interface TopoExportReport {
|
|
80
52
|
readonly hash: string;
|
|
81
53
|
readonly lockPath: string;
|
|
82
54
|
readonly mapPath: string;
|
|
83
|
-
readonly
|
|
55
|
+
readonly snapshot: TopoSnapshot;
|
|
84
56
|
}
|
|
85
57
|
|
|
86
58
|
export interface TopoVerifyReport {
|
|
@@ -90,7 +62,7 @@ export interface TopoVerifyReport {
|
|
|
90
62
|
readonly stale: false;
|
|
91
63
|
}
|
|
92
64
|
|
|
93
|
-
export const
|
|
65
|
+
export const deriveRootDir = (cwd?: string): string => cwd ?? process.cwd();
|
|
94
66
|
|
|
95
67
|
const safeGit = (cwd: string, args: readonly string[]): string | undefined => {
|
|
96
68
|
const proc = Bun.spawnSync({
|
|
@@ -105,7 +77,7 @@ const safeGit = (cwd: string, args: readonly string[]): string | undefined => {
|
|
|
105
77
|
return text.length === 0 ? undefined : text;
|
|
106
78
|
};
|
|
107
79
|
|
|
108
|
-
export const
|
|
80
|
+
export const readGitState = (
|
|
109
81
|
rootDir: string
|
|
110
82
|
): { readonly gitDirty: boolean; readonly gitSha?: string } => {
|
|
111
83
|
const gitSha = safeGit(rootDir, ['rev-parse', 'HEAD']);
|
|
@@ -116,10 +88,10 @@ export const currentGitState = (
|
|
|
116
88
|
};
|
|
117
89
|
};
|
|
118
90
|
|
|
119
|
-
export const
|
|
91
|
+
export const deriveTopoCounts = (
|
|
120
92
|
app: Topo
|
|
121
|
-
): Pick<
|
|
122
|
-
|
|
93
|
+
): Pick<TopoSnapshot, 'resourceCount' | 'signalCount' | 'trailCount'> => ({
|
|
94
|
+
resourceCount: app.resources.size,
|
|
123
95
|
signalCount: app.signals.size,
|
|
124
96
|
trailCount: app.trails.size,
|
|
125
97
|
});
|
|
@@ -130,40 +102,39 @@ const emptyTopoHistory = (
|
|
|
130
102
|
): TopoHistoryReport => ({
|
|
131
103
|
dbPath,
|
|
132
104
|
limit,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
saves: [],
|
|
105
|
+
pinnedCount: 0,
|
|
106
|
+
snapshotCount: 0,
|
|
107
|
+
snapshots: [],
|
|
137
108
|
});
|
|
138
109
|
|
|
139
|
-
const
|
|
110
|
+
const collectTopoHistory = (
|
|
140
111
|
dbPath: string,
|
|
141
112
|
limit: number,
|
|
142
|
-
|
|
143
|
-
allSaves: readonly TopoSaveRecord[]
|
|
113
|
+
snapshots: readonly TopoSnapshot[]
|
|
144
114
|
): TopoHistoryReport => ({
|
|
145
115
|
dbPath,
|
|
146
116
|
limit,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
117
|
+
pinnedCount: snapshots.filter((snapshot) => snapshot.pinnedAs !== undefined)
|
|
118
|
+
.length,
|
|
119
|
+
snapshotCount: snapshots.length,
|
|
120
|
+
snapshots: snapshots.slice(0, limit),
|
|
151
121
|
});
|
|
152
122
|
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
db: Parameters<typeof unpinTopoSave>[0]
|
|
123
|
+
const buildSnapshotInput = (
|
|
124
|
+
app: Topo,
|
|
125
|
+
rootDir: string
|
|
157
126
|
): {
|
|
158
|
-
readonly
|
|
159
|
-
readonly
|
|
160
|
-
readonly
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
+
});
|
|
165
136
|
|
|
166
|
-
export const
|
|
137
|
+
export const createIsolatedExampleInput = (
|
|
167
138
|
name: string
|
|
168
139
|
): { readonly module: string; readonly rootDir: string } => {
|
|
169
140
|
const rootDir = join(tmpdir(), 'ontrails-trails-examples', name);
|
|
@@ -175,100 +146,85 @@ export const isolatedExampleInput = (
|
|
|
175
146
|
};
|
|
176
147
|
};
|
|
177
148
|
|
|
178
|
-
export const
|
|
149
|
+
export const createCurrentTopoSnapshot = (
|
|
179
150
|
app: Topo,
|
|
180
151
|
options?: { readonly rootDir?: string }
|
|
181
|
-
):
|
|
182
|
-
const rootDir =
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
});
|
|
190
|
-
if (result.isErr()) {
|
|
191
|
-
throw result.error;
|
|
192
|
-
}
|
|
193
|
-
return result.value;
|
|
194
|
-
} finally {
|
|
195
|
-
db.close();
|
|
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;
|
|
196
160
|
}
|
|
161
|
+
return result.value;
|
|
197
162
|
};
|
|
198
163
|
|
|
199
164
|
export const listTopoHistory = (options?: {
|
|
200
165
|
readonly limit?: number;
|
|
201
166
|
readonly rootDir?: string;
|
|
202
167
|
}): TopoHistoryReport => {
|
|
203
|
-
const rootDir =
|
|
168
|
+
const rootDir = deriveRootDir(options?.rootDir);
|
|
204
169
|
const limit = options?.limit ?? DEFAULT_TOPO_HISTORY_LIMIT;
|
|
205
|
-
const dbPath =
|
|
170
|
+
const dbPath = deriveTrailsDbPath({ rootDir });
|
|
206
171
|
if (!existsSync(dbPath)) {
|
|
207
172
|
return emptyTopoHistory(dbPath, limit);
|
|
208
173
|
}
|
|
209
|
-
const db = openReadTrailsDb({ rootDir });
|
|
210
174
|
|
|
211
|
-
|
|
212
|
-
return collectedTopoHistory(
|
|
213
|
-
dbPath,
|
|
214
|
-
limit,
|
|
215
|
-
listTopoPins(db),
|
|
216
|
-
listTopoSaves(db)
|
|
217
|
-
);
|
|
218
|
-
} finally {
|
|
219
|
-
db.close();
|
|
220
|
-
}
|
|
175
|
+
return collectTopoHistory(dbPath, limit, readTopoSnapshots({ rootDir }));
|
|
221
176
|
};
|
|
222
177
|
|
|
223
|
-
export const
|
|
178
|
+
export const pinCurrentTopoSnapshot = (
|
|
224
179
|
app: Topo,
|
|
225
180
|
input: { readonly name: string; readonly rootDir?: string }
|
|
226
|
-
): { readonly
|
|
227
|
-
const rootDir =
|
|
228
|
-
const
|
|
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
|
+
}
|
|
229
190
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
});
|
|
235
|
-
if (result.isErr()) {
|
|
236
|
-
throw result.error;
|
|
237
|
-
}
|
|
238
|
-
const pin = pinTopoSave(db, {
|
|
239
|
-
name: input.name,
|
|
240
|
-
saveId: result.value.id,
|
|
241
|
-
});
|
|
242
|
-
return { pin, save: result.value };
|
|
243
|
-
} finally {
|
|
244
|
-
db.close();
|
|
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`);
|
|
245
196
|
}
|
|
197
|
+
|
|
198
|
+
return { snapshot };
|
|
246
199
|
};
|
|
247
200
|
|
|
248
|
-
export const
|
|
201
|
+
export const removePinnedTopoSnapshot = (input: {
|
|
249
202
|
readonly dryRun: boolean;
|
|
250
203
|
readonly name: string;
|
|
251
204
|
readonly rootDir?: string;
|
|
252
205
|
}): {
|
|
253
206
|
readonly dryRun: boolean;
|
|
254
|
-
readonly pin?: TopoPinRecord;
|
|
255
207
|
readonly removed: boolean;
|
|
208
|
+
readonly snapshot?: TopoSnapshot;
|
|
256
209
|
} => {
|
|
257
|
-
const rootDir =
|
|
258
|
-
if (!existsSync(
|
|
210
|
+
const rootDir = deriveRootDir(input.rootDir);
|
|
211
|
+
if (!existsSync(deriveTrailsDbPath({ rootDir }))) {
|
|
259
212
|
return { dryRun: input.dryRun, removed: false };
|
|
260
213
|
}
|
|
261
|
-
const db = input.dryRun
|
|
262
|
-
? openReadTrailsDb({ rootDir })
|
|
263
|
-
: openWriteTrailsDb({ rootDir });
|
|
264
214
|
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
db.close();
|
|
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 };
|
|
273
222
|
}
|
|
223
|
+
|
|
224
|
+
const snapshot = unpinTopoSnapshot(input.name, { rootDir });
|
|
225
|
+
return {
|
|
226
|
+
dryRun: false,
|
|
227
|
+
removed: snapshot !== undefined,
|
|
228
|
+
...(snapshot === undefined ? {} : { snapshot }),
|
|
229
|
+
};
|
|
274
230
|
};
|
package/src/trails/topo-unpin.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { Result, ValidationError, trail } from '@ontrails/core';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
createIsolatedExampleInput,
|
|
6
|
+
removePinnedTopoSnapshot,
|
|
7
|
+
topoSnapshotOutput,
|
|
8
8
|
} from './topo-support.js';
|
|
9
9
|
|
|
10
10
|
export const topoUnpinTrail = trail('topo.unpin', {
|
|
@@ -19,14 +19,18 @@ export const topoUnpinTrail = trail('topo.unpin', {
|
|
|
19
19
|
|
|
20
20
|
const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
|
|
21
21
|
return Result.ok(
|
|
22
|
-
|
|
22
|
+
removePinnedTopoSnapshot({
|
|
23
|
+
dryRun: input.dryRun,
|
|
24
|
+
name: input.name,
|
|
25
|
+
rootDir,
|
|
26
|
+
})
|
|
23
27
|
);
|
|
24
28
|
},
|
|
25
29
|
description: 'Remove a named topo pin',
|
|
26
30
|
examples: [
|
|
27
31
|
{
|
|
28
32
|
input: {
|
|
29
|
-
...
|
|
33
|
+
...createIsolatedExampleInput('topo-unpin'),
|
|
30
34
|
dryRun: true,
|
|
31
35
|
name: 'before-auth-refactor',
|
|
32
36
|
},
|
|
@@ -45,7 +49,8 @@ export const topoUnpinTrail = trail('topo.unpin', {
|
|
|
45
49
|
intent: 'destroy',
|
|
46
50
|
output: z.object({
|
|
47
51
|
dryRun: z.boolean(),
|
|
48
|
-
pin: topoPinOutput.optional(),
|
|
49
52
|
removed: z.boolean(),
|
|
53
|
+
snapshot: topoSnapshotOutput.optional(),
|
|
50
54
|
}),
|
|
55
|
+
permit: { scopes: ['topo:delete'] },
|
|
51
56
|
});
|
|
@@ -3,7 +3,6 @@ import { z } from 'zod';
|
|
|
3
3
|
|
|
4
4
|
import { loadApp } from './load-app.js';
|
|
5
5
|
import { verifyCurrentTopo } from './topo-read-support.js';
|
|
6
|
-
import { DEFAULT_APP_MODULE } from './topo-support.js';
|
|
7
6
|
|
|
8
7
|
export const topoVerifyTrail = trail('topo.verify', {
|
|
9
8
|
blaze: async (input, ctx) => {
|
|
@@ -13,10 +12,7 @@ export const topoVerifyTrail = trail('topo.verify', {
|
|
|
13
12
|
},
|
|
14
13
|
description: 'Verify that the committed lockfile matches the current topo',
|
|
15
14
|
input: z.object({
|
|
16
|
-
module: z
|
|
17
|
-
.string()
|
|
18
|
-
.default(DEFAULT_APP_MODULE)
|
|
19
|
-
.describe('Path to the app module'),
|
|
15
|
+
module: z.string().optional().describe('Path to the app module'),
|
|
20
16
|
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
21
17
|
}),
|
|
22
18
|
intent: 'read',
|
package/src/trails/topo.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { z } from 'zod';
|
|
|
3
3
|
|
|
4
4
|
import { loadApp } from './load-app.js';
|
|
5
5
|
import { buildTopoSummary } from './topo-read-support.js';
|
|
6
|
-
import { DEFAULT_APP_MODULE } from './topo-support.js';
|
|
7
6
|
|
|
8
7
|
const summaryOutput = z.object({
|
|
9
8
|
app: z.object({
|
|
@@ -12,11 +11,11 @@ const summaryOutput = z.object({
|
|
|
12
11
|
detours: z.boolean(),
|
|
13
12
|
examples: z.boolean(),
|
|
14
13
|
outputSchemas: z.boolean(),
|
|
15
|
-
|
|
14
|
+
resources: z.boolean(),
|
|
16
15
|
signals: z.boolean(),
|
|
17
16
|
}),
|
|
18
17
|
name: z.string(),
|
|
19
|
-
|
|
18
|
+
resources: z.number(),
|
|
20
19
|
signals: z.number(),
|
|
21
20
|
trails: z.number(),
|
|
22
21
|
version: z.string(),
|
|
@@ -32,13 +31,13 @@ const summaryOutput = z.object({
|
|
|
32
31
|
safety: z.string(),
|
|
33
32
|
})
|
|
34
33
|
),
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
resourceCount: z.number(),
|
|
35
|
+
resources: z.array(
|
|
37
36
|
z.object({
|
|
38
37
|
description: z.string().nullable(),
|
|
39
38
|
health: z.enum(['available', 'none']),
|
|
40
39
|
id: z.string(),
|
|
41
|
-
kind: z.literal('
|
|
40
|
+
kind: z.literal('resource'),
|
|
42
41
|
lifetime: z.literal('singleton'),
|
|
43
42
|
usedBy: z.array(z.string()),
|
|
44
43
|
})
|
|
@@ -62,10 +61,7 @@ export const topoTrail = trail('topo', {
|
|
|
62
61
|
},
|
|
63
62
|
],
|
|
64
63
|
input: z.object({
|
|
65
|
-
module: z
|
|
66
|
-
.string()
|
|
67
|
-
.default(DEFAULT_APP_MODULE)
|
|
68
|
-
.describe('Path to the app module'),
|
|
64
|
+
module: z.string().optional().describe('Path to the app module'),
|
|
69
65
|
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
70
66
|
}),
|
|
71
67
|
intent: 'read',
|
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',
|
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;
|