@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,73 +1,105 @@
1
1
  /**
2
2
  * Stored-export pipeline for topo persistence.
3
3
  *
4
- * Extracted from topo-support.ts so this branch (trl-131) owns its own file,
5
- * keeping absorb routing clean across the stack.
4
+ * Extracted from topo-support.ts to isolate store persistence concerns,
5
+ * keeping module boundaries clean.
6
6
  */
7
7
 
8
+ import { Database } from 'bun:sqlite';
9
+
8
10
  import type { Topo } from '@ontrails/core';
9
- import { InternalError, Result } from '@ontrails/core';
10
- import type { TopoSaveRecord } from '@ontrails/core/internal/topo-saves';
11
- import type { StoredTopoExport } from '@ontrails/core/internal/topo-store';
12
- import {
13
- getStoredTopoExport,
14
- persistEstablishedTopoSave,
15
- } from '@ontrails/core/internal/topo-store';
16
11
  import {
12
+ deriveTrailsDir,
13
+ InternalError,
17
14
  openWriteTrailsDb,
18
- resolveTrailsDir,
19
- } from '@ontrails/core/internal/trails-db';
20
- import type { TrailheadLock, TrailheadMap } from '@ontrails/schema';
21
- import { writeTrailheadLock, writeTrailheadMap } from '@ontrails/schema';
15
+ Result,
16
+ } from '@ontrails/core';
17
+ import type {
18
+ LockManifest,
19
+ TopoGraph,
20
+ TopoSnapshot,
21
+ } from '@ontrails/topographer';
22
+ import type { StoredTopoExport } from '@ontrails/topographer/backend-support';
23
+ import { writeLockManifest, writeTopoGraph } from '@ontrails/topographer';
24
+ import {
25
+ createStoredTopoSnapshot,
26
+ getStoredTopoExport,
27
+ } from '@ontrails/topographer/backend-support';
22
28
 
23
29
  import type { TopoExportReport } from './topo-support.js';
24
- import { currentGitState, resolveRootDir, topoCounts } from './topo-support.js';
30
+ import {
31
+ deriveRootDir,
32
+ deriveTopoCounts,
33
+ readGitState,
34
+ } from './topo-support.js';
25
35
 
26
36
  const persistAndReadStoredExport = (
27
37
  app: Topo,
28
38
  db: ReturnType<typeof openWriteTrailsDb>,
29
39
  rootDir: string
30
- ): Result<{ save: TopoSaveRecord; storedExport: StoredTopoExport }, Error> => {
31
- const saveResult = persistEstablishedTopoSave(db, app, {
32
- ...currentGitState(rootDir),
33
- ...topoCounts(app),
40
+ ): Result<
41
+ { snapshot: TopoSnapshot; storedExport: StoredTopoExport },
42
+ Error
43
+ > => {
44
+ const snapshotResult = createStoredTopoSnapshot(db, app, {
45
+ ...readGitState(rootDir),
46
+ ...deriveTopoCounts(app),
34
47
  });
35
- if (saveResult.isErr()) {
36
- return saveResult;
48
+ if (snapshotResult.isErr()) {
49
+ return snapshotResult;
37
50
  }
38
51
 
39
- const save = saveResult.value;
40
- const storedExport = getStoredTopoExport(db, save.id);
52
+ const snapshot = snapshotResult.value;
53
+ const storedExport = getStoredTopoExport(db, snapshot.id);
41
54
 
42
55
  if (storedExport === undefined) {
43
56
  return Result.err(
44
- new InternalError(`Missing stored topo export for save "${save.id}"`)
57
+ new InternalError(
58
+ `Missing stored topo export for snapshot "${snapshot.id}"`
59
+ )
45
60
  );
46
61
  }
47
62
 
48
63
  return Result.ok({
49
- save,
64
+ snapshot,
50
65
  storedExport,
51
66
  });
52
67
  };
53
68
 
69
+ export const deriveCurrentTopoExport = (
70
+ app: Topo,
71
+ options?: { readonly rootDir?: string }
72
+ ): Result<StoredTopoExport, Error> => {
73
+ const rootDir = deriveRootDir(options?.rootDir);
74
+ const db = new Database(':memory:');
75
+
76
+ try {
77
+ const projected = persistAndReadStoredExport(app, db, rootDir);
78
+ return projected.isErr()
79
+ ? projected
80
+ : Result.ok(projected.value.storedExport);
81
+ } finally {
82
+ db.close();
83
+ }
84
+ };
85
+
54
86
  const writeStoredExportArtifacts = async (
55
87
  storedExport: StoredTopoExport,
56
88
  trailsDir: string
57
- ): Promise<Pick<TopoExportReport, 'hash' | 'lockPath' | 'mapPath'>> => {
58
- const mapPath = await writeTrailheadMap(
59
- JSON.parse(storedExport.trailheadMapJson) as TrailheadMap,
89
+ ): Promise<Pick<TopoExportReport, 'hash' | 'lockPath' | 'topoPath'>> => {
90
+ const topoPath = await writeTopoGraph(
91
+ JSON.parse(storedExport.topoGraphJson) as TopoGraph,
60
92
  { dir: trailsDir }
61
93
  );
62
- const lockPath = await writeTrailheadLock(
63
- JSON.parse(storedExport.lockContent) as TrailheadLock,
94
+ const lockPath = await writeLockManifest(
95
+ JSON.parse(storedExport.lockManifestJson) as LockManifest,
64
96
  { dir: trailsDir }
65
97
  );
66
98
 
67
99
  return {
68
- hash: storedExport.trailheadHash,
100
+ hash: storedExport.topoGraphHash,
69
101
  lockPath,
70
- mapPath,
102
+ topoPath,
71
103
  };
72
104
  };
73
105
 
@@ -75,7 +107,7 @@ export const exportCurrentTopo = async (
75
107
  app: Topo,
76
108
  options?: { readonly rootDir?: string }
77
109
  ): Promise<Result<TopoExportReport, Error>> => {
78
- const rootDir = resolveRootDir(options?.rootDir);
110
+ const rootDir = deriveRootDir(options?.rootDir);
79
111
  const db = openWriteTrailsDb({ rootDir });
80
112
 
81
113
  try {
@@ -84,12 +116,12 @@ export const exportCurrentTopo = async (
84
116
  return persisted;
85
117
  }
86
118
 
87
- const { save, storedExport } = persisted.value;
119
+ const { snapshot, storedExport } = persisted.value;
88
120
  const artifacts = await writeStoredExportArtifacts(
89
121
  storedExport,
90
- resolveTrailsDir({ rootDir })
122
+ deriveTrailsDir({ rootDir })
91
123
  );
92
- return Result.ok({ ...artifacts, save });
124
+ return Result.ok({ ...artifacts, snapshot });
93
125
  } finally {
94
126
  db.close();
95
127
  }
@@ -1,64 +1,44 @@
1
- import { existsSync, mkdirSync, rmSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
1
+ import { existsSync } from 'node:fs';
4
2
  import { fileURLToPath } from 'node:url';
5
3
 
4
+ import { deriveTrailsDbPath } from '@ontrails/core';
6
5
  import type { Topo } from '@ontrails/core';
7
- import type {
8
- TopoPinRecord,
9
- TopoSaveRecord,
10
- } from '@ontrails/core/internal/topo-saves';
11
6
  import {
12
- getTopoPin,
13
- listTopoPins,
14
- listTopoSaves,
15
- pinTopoSave,
16
- unpinTopoSave,
17
- } from '@ontrails/core/internal/topo-saves';
18
- import { persistEstablishedTopoSave } from '@ontrails/core/internal/topo-store';
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/topographer';
12
+ import type { TopoSnapshot } from '@ontrails/topographer';
24
13
  import { z } from 'zod';
25
14
 
15
+ import {
16
+ createIsolatedExampleRoot,
17
+ writeIsolatedExampleAppModule,
18
+ } from '../local-state-io.js';
19
+
20
+ import { requireTrailRootDir } from './root-dir.js';
26
21
  import type { BriefReport, SurveyListReport } from './topo-reports.js';
27
22
 
28
- /** Output schema for a topo save record. Shared across topo trails. */
29
- export const topoSaveOutput = z.object({
23
+ /** Output schema for a topo snapshot record. Shared across topo trails. */
24
+ export const topoSnapshotOutput = z.object({
30
25
  createdAt: z.string(),
31
26
  gitDirty: z.boolean(),
32
27
  gitSha: z.string().optional(),
33
28
  id: z.string(),
34
- provisionCount: z.number(),
29
+ pinnedAs: z.string().optional(),
30
+ resourceCount: z.number(),
35
31
  signalCount: z.number(),
36
32
  trailCount: z.number(),
37
33
  });
38
34
 
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
- export const DEFAULT_APP_MODULE = './src/app.ts';
47
35
  export const DEFAULT_TOPO_HISTORY_LIMIT = 10;
48
36
  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
37
  const EXAMPLE_APP_MODULE = fileURLToPath(new URL('../app.ts', import.meta.url));
61
38
 
39
+ const uniqueExampleRootName = (name: string): string =>
40
+ `${name}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
41
+
62
42
  export interface TopoSummaryReport {
63
43
  readonly app: BriefReport;
64
44
  readonly dbPath: string;
@@ -70,17 +50,16 @@ export interface TopoSummaryReport {
70
50
  export interface TopoHistoryReport {
71
51
  readonly dbPath: string;
72
52
  readonly limit: number;
73
- readonly pinCount: number;
74
- readonly pins: TopoPinRecord[];
75
- readonly saveCount: number;
76
- readonly saves: TopoSaveRecord[];
53
+ readonly pinnedCount: number;
54
+ readonly snapshotCount: number;
55
+ readonly snapshots: TopoSnapshot[];
77
56
  }
78
57
 
79
58
  export interface TopoExportReport {
80
59
  readonly hash: string;
81
60
  readonly lockPath: string;
82
- readonly mapPath: string;
83
- readonly save: TopoSaveRecord;
61
+ readonly snapshot: TopoSnapshot;
62
+ readonly topoPath: string;
84
63
  }
85
64
 
86
65
  export interface TopoVerifyReport {
@@ -90,7 +69,7 @@ export interface TopoVerifyReport {
90
69
  readonly stale: false;
91
70
  }
92
71
 
93
- export const resolveRootDir = (cwd?: string): string => cwd ?? process.cwd();
72
+ export const deriveRootDir = (cwd?: string): string => requireTrailRootDir(cwd);
94
73
 
95
74
  const safeGit = (cwd: string, args: readonly string[]): string | undefined => {
96
75
  const proc = Bun.spawnSync({
@@ -105,7 +84,7 @@ const safeGit = (cwd: string, args: readonly string[]): string | undefined => {
105
84
  return text.length === 0 ? undefined : text;
106
85
  };
107
86
 
108
- export const currentGitState = (
87
+ export const readGitState = (
109
88
  rootDir: string
110
89
  ): { readonly gitDirty: boolean; readonly gitSha?: string } => {
111
90
  const gitSha = safeGit(rootDir, ['rev-parse', 'HEAD']);
@@ -116,10 +95,10 @@ export const currentGitState = (
116
95
  };
117
96
  };
118
97
 
119
- export const topoCounts = (
98
+ export const deriveTopoCounts = (
120
99
  app: Topo
121
- ): Pick<TopoSaveRecord, 'provisionCount' | 'signalCount' | 'trailCount'> => ({
122
- provisionCount: app.provisions.size,
100
+ ): Pick<TopoSnapshot, 'resourceCount' | 'signalCount' | 'trailCount'> => ({
101
+ resourceCount: app.resources.size,
123
102
  signalCount: app.signals.size,
124
103
  trailCount: app.trails.size,
125
104
  });
@@ -130,145 +109,112 @@ const emptyTopoHistory = (
130
109
  ): TopoHistoryReport => ({
131
110
  dbPath,
132
111
  limit,
133
- pinCount: 0,
134
- pins: [],
135
- saveCount: 0,
136
- saves: [],
112
+ pinnedCount: 0,
113
+ snapshotCount: 0,
114
+ snapshots: [],
137
115
  });
138
116
 
139
- const collectedTopoHistory = (
117
+ const collectTopoHistory = (
140
118
  dbPath: string,
141
119
  limit: number,
142
- pins: readonly TopoPinRecord[],
143
- allSaves: readonly TopoSaveRecord[]
120
+ snapshots: readonly TopoSnapshot[]
144
121
  ): TopoHistoryReport => ({
145
122
  dbPath,
146
123
  limit,
147
- pinCount: pins.length,
148
- pins: [...pins],
149
- saveCount: allSaves.length,
150
- saves: allSaves.slice(0, limit),
124
+ pinnedCount: snapshots.filter((snapshot) => snapshot.pinnedAs !== undefined)
125
+ .length,
126
+ snapshotCount: snapshots.length,
127
+ snapshots: snapshots.slice(0, limit),
151
128
  });
152
129
 
153
- const removeTopoPinWithDb = (
154
- input: { readonly dryRun: boolean; readonly name: string },
155
- pin: TopoPinRecord,
156
- db: Parameters<typeof unpinTopoSave>[0]
130
+ const buildSnapshotInput = (
131
+ app: Topo,
132
+ rootDir: string
157
133
  ): {
158
- readonly dryRun: boolean;
159
- readonly pin?: TopoPinRecord;
160
- readonly removed: boolean;
161
- } =>
162
- input.dryRun
163
- ? { dryRun: true, pin, removed: false }
164
- : { dryRun: false, pin, removed: unpinTopoSave(db, input.name) };
134
+ readonly gitDirty: boolean;
135
+ readonly gitSha?: string;
136
+ readonly resourceCount: number;
137
+ readonly signalCount: number;
138
+ readonly trailCount: number;
139
+ } => ({
140
+ ...readGitState(rootDir),
141
+ ...deriveTopoCounts(app),
142
+ });
165
143
 
166
- export const isolatedExampleInput = (
144
+ export const createIsolatedExampleInput = (
167
145
  name: string
168
146
  ): { readonly module: string; readonly rootDir: string } => {
169
- const rootDir = join(tmpdir(), 'ontrails-trails-examples', name);
170
- rmSync(rootDir, { force: true, recursive: true });
171
- mkdirSync(rootDir, { recursive: true });
147
+ const rootDir = createIsolatedExampleRoot(uniqueExampleRootName(name));
172
148
  return {
173
- module: EXAMPLE_APP_MODULE,
149
+ module: writeIsolatedExampleAppModule(rootDir, EXAMPLE_APP_MODULE),
174
150
  rootDir,
175
151
  };
176
152
  };
177
153
 
178
- export const createCurrentTopoSave = (
179
- app: Topo,
180
- options?: { readonly rootDir?: string }
181
- ): TopoSaveRecord => {
182
- const rootDir = resolveRootDir(options?.rootDir);
183
- const db = openWriteTrailsDb({ rootDir });
184
-
185
- try {
186
- const result = persistEstablishedTopoSave(db, app, {
187
- ...currentGitState(rootDir),
188
- ...topoCounts(app),
189
- });
190
- if (result.isErr()) {
191
- throw result.error;
192
- }
193
- return result.value;
194
- } finally {
195
- db.close();
196
- }
197
- };
198
-
199
154
  export const listTopoHistory = (options?: {
200
155
  readonly limit?: number;
201
156
  readonly rootDir?: string;
202
157
  }): TopoHistoryReport => {
203
- const rootDir = resolveRootDir(options?.rootDir);
158
+ const rootDir = deriveRootDir(options?.rootDir);
204
159
  const limit = options?.limit ?? DEFAULT_TOPO_HISTORY_LIMIT;
205
- const dbPath = resolveTrailsDbPath({ rootDir });
160
+ const dbPath = deriveTrailsDbPath({ rootDir });
206
161
  if (!existsSync(dbPath)) {
207
162
  return emptyTopoHistory(dbPath, limit);
208
163
  }
209
- const db = openReadTrailsDb({ rootDir });
210
164
 
211
- try {
212
- return collectedTopoHistory(
213
- dbPath,
214
- limit,
215
- listTopoPins(db),
216
- listTopoSaves(db)
217
- );
218
- } finally {
219
- db.close();
220
- }
165
+ return collectTopoHistory(dbPath, limit, readTopoSnapshots({ rootDir }));
221
166
  };
222
167
 
223
- export const pinCurrentTopo = (
168
+ export const pinCurrentTopoSnapshot = (
224
169
  app: Topo,
225
170
  input: { readonly name: string; readonly rootDir?: string }
226
- ): { readonly pin: TopoPinRecord; readonly save: TopoSaveRecord } => {
227
- const rootDir = resolveRootDir(input.rootDir);
228
- const db = openWriteTrailsDb({ rootDir });
171
+ ): { readonly snapshot: TopoSnapshot } => {
172
+ const rootDir = deriveRootDir(input.rootDir);
173
+ const created = persistTopoSnapshot(app, {
174
+ rootDir,
175
+ ...buildSnapshotInput(app, rootDir),
176
+ });
177
+ if (created.isErr()) {
178
+ throw created.error;
179
+ }
229
180
 
230
- try {
231
- const result = persistEstablishedTopoSave(db, app, {
232
- ...currentGitState(rootDir),
233
- ...topoCounts(app),
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();
181
+ const snapshot = pinTopoSnapshot(created.value.id, input.name, {
182
+ rootDir,
183
+ });
184
+ if (snapshot === undefined) {
185
+ throw new Error(`Missing topo snapshot "${created.value.id}" to pin`);
245
186
  }
187
+
188
+ return { snapshot };
246
189
  };
247
190
 
248
- export const removeTopoPin = (input: {
191
+ export const removePinnedTopoSnapshot = (input: {
249
192
  readonly dryRun: boolean;
250
193
  readonly name: string;
251
194
  readonly rootDir?: string;
252
195
  }): {
253
196
  readonly dryRun: boolean;
254
- readonly pin?: TopoPinRecord;
255
197
  readonly removed: boolean;
198
+ readonly snapshot?: TopoSnapshot;
256
199
  } => {
257
- const rootDir = resolveRootDir(input.rootDir);
258
- if (!existsSync(resolveTrailsDbPath({ rootDir }))) {
200
+ const rootDir = deriveRootDir(input.rootDir);
201
+ if (!existsSync(deriveTrailsDbPath({ rootDir }))) {
259
202
  return { dryRun: input.dryRun, removed: false };
260
203
  }
261
- const db = input.dryRun
262
- ? openReadTrailsDb({ rootDir })
263
- : openWriteTrailsDb({ rootDir });
264
204
 
265
- try {
266
- const pin = getTopoPin(db, input.name);
267
- if (pin === undefined) {
268
- return { dryRun: input.dryRun, removed: false };
269
- }
270
- return removeTopoPinWithDb(input, pin, db);
271
- } finally {
272
- db.close();
205
+ if (input.dryRun) {
206
+ const snapshot = readTopoSnapshots({ pinned: true, rootDir }).find(
207
+ (candidate) => candidate.pinnedAs === input.name
208
+ );
209
+ return snapshot === undefined
210
+ ? { dryRun: true, removed: false }
211
+ : { dryRun: true, removed: false, snapshot };
273
212
  }
213
+
214
+ const snapshot = unpinTopoSnapshot(input.name, { rootDir });
215
+ return {
216
+ dryRun: false,
217
+ removed: snapshot !== undefined,
218
+ ...(snapshot === undefined ? {} : { snapshot }),
219
+ };
274
220
  };
@@ -2,10 +2,11 @@ import { Result, ValidationError, trail } from '@ontrails/core';
2
2
  import { z } from 'zod';
3
3
 
4
4
  import {
5
- isolatedExampleInput,
6
- removeTopoPin,
7
- topoPinOutput,
5
+ createIsolatedExampleInput,
6
+ removePinnedTopoSnapshot,
7
+ topoSnapshotOutput,
8
8
  } from './topo-support.js';
9
+ import { resolveTrailRootDir } from './root-dir.js';
9
10
 
10
11
  export const topoUnpinTrail = trail('topo.unpin', {
11
12
  blaze: (input, ctx) => {
@@ -17,16 +18,24 @@ export const topoUnpinTrail = trail('topo.unpin', {
17
18
  );
18
19
  }
19
20
 
20
- const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
21
+ const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
22
+ if (rootDirResult.isErr()) {
23
+ return Result.err(rootDirResult.error);
24
+ }
25
+ const rootDir = rootDirResult.value;
21
26
  return Result.ok(
22
- removeTopoPin({ dryRun: input.dryRun, name: input.name, rootDir })
27
+ removePinnedTopoSnapshot({
28
+ dryRun: input.dryRun,
29
+ name: input.name,
30
+ rootDir,
31
+ })
23
32
  );
24
33
  },
25
34
  description: 'Remove a named topo pin',
26
35
  examples: [
27
36
  {
28
37
  input: {
29
- ...isolatedExampleInput('topo-unpin'),
38
+ ...createIsolatedExampleInput('topo-unpin'),
30
39
  dryRun: true,
31
40
  name: 'before-auth-refactor',
32
41
  },
@@ -45,7 +54,8 @@ export const topoUnpinTrail = trail('topo.unpin', {
45
54
  intent: 'destroy',
46
55
  output: z.object({
47
56
  dryRun: z.boolean(),
48
- pin: topoPinOutput.optional(),
49
57
  removed: z.boolean(),
58
+ snapshot: topoSnapshotOutput.optional(),
50
59
  }),
60
+ permit: { scopes: ['topo:delete'] },
51
61
  });
@@ -1,22 +1,31 @@
1
- import { trail } from '@ontrails/core';
1
+ import { Result, trail } from '@ontrails/core';
2
2
  import { z } from 'zod';
3
3
 
4
- import { loadApp } from './load-app.js';
4
+ import { tryLoadFreshAppLease } from './load-app.js';
5
+ import { resolveTrailRootDir } from './root-dir.js';
5
6
  import { verifyCurrentTopo } from './topo-read-support.js';
6
- import { DEFAULT_APP_MODULE } from './topo-support.js';
7
7
 
8
8
  export const topoVerifyTrail = trail('topo.verify', {
9
9
  blaze: async (input, ctx) => {
10
- const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
11
- const app = await loadApp(input.module, rootDir);
12
- return verifyCurrentTopo(app, { rootDir });
10
+ const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
11
+ if (rootDirResult.isErr()) {
12
+ return Result.err(rootDirResult.error);
13
+ }
14
+ const rootDir = rootDirResult.value;
15
+ const leaseResult = await tryLoadFreshAppLease(input.module, rootDir);
16
+ if (leaseResult.isErr()) {
17
+ return Result.err(leaseResult.error);
18
+ }
19
+ const lease = leaseResult.value;
20
+ try {
21
+ return await verifyCurrentTopo(lease.app, { rootDir });
22
+ } finally {
23
+ lease.release();
24
+ }
13
25
  },
14
26
  description: 'Verify that the committed lockfile matches the current topo',
15
27
  input: z.object({
16
- module: z
17
- .string()
18
- .default(DEFAULT_APP_MODULE)
19
- .describe('Path to the app module'),
28
+ module: z.string().optional().describe('Path to the app module'),
20
29
  rootDir: z.string().optional().describe('Workspace root directory'),
21
30
  }),
22
31
  intent: 'read',