@ontrails/trails 1.0.0-beta.3 → 1.0.0-beta.32

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 (158) hide show
  1. package/CHANGELOG.md +975 -0
  2. package/README.md +27 -0
  3. package/package.json +30 -7
  4. package/src/app.ts +125 -2
  5. package/src/cli.ts +345 -11
  6. package/src/completions.ts +240 -0
  7. package/src/lifecycle-source-io.ts +33 -0
  8. package/src/load-app-mirror.ts +202 -0
  9. package/src/local-state-io.ts +173 -0
  10. package/src/mcp-app.ts +40 -0
  11. package/src/mcp-options.ts +77 -0
  12. package/src/mcp.ts +8 -0
  13. package/src/project-writes.ts +377 -0
  14. package/src/regrade/config.ts +152 -0
  15. package/src/release/bindings.ts +39 -0
  16. package/src/release/check.ts +844 -0
  17. package/src/release/config.ts +63 -0
  18. package/src/release/contract-facts.ts +425 -0
  19. package/src/release/index.ts +125 -0
  20. package/src/release/native-bun-publish.ts +651 -0
  21. package/src/release/native-bun-registry.ts +754 -0
  22. package/src/release/pack-coherence.ts +455 -0
  23. package/src/release/packed-artifacts-smoke.ts +236 -0
  24. package/src/release/policy.ts +1684 -0
  25. package/src/release/semver.ts +104 -0
  26. package/src/release/smoke.ts +46 -0
  27. package/src/release/wayfinder-dogfood-smoke.ts +760 -0
  28. package/src/retired-topo-command.ts +36 -0
  29. package/src/run-adapter-check.ts +76 -0
  30. package/src/run-collision.ts +126 -0
  31. package/src/run-completions-install.ts +179 -0
  32. package/src/run-example.ts +149 -0
  33. package/src/run-examples.ts +148 -0
  34. package/src/run-quiet.ts +75 -0
  35. package/src/run-release-check.ts +74 -0
  36. package/src/run-schema.ts +41 -0
  37. package/src/run-trace.ts +273 -0
  38. package/src/run-warden.ts +39 -0
  39. package/src/run-watch.ts +432 -0
  40. package/src/run-wayfind-outline.ts +166 -0
  41. package/src/scaffold-version-sync.ts +183 -0
  42. package/src/scaffold-versions.generated.ts +12 -0
  43. package/src/trails/adapter-check.ts +244 -0
  44. package/src/trails/add-surface.ts +94 -40
  45. package/src/trails/add-trail.ts +79 -41
  46. package/src/trails/add-verify.ts +95 -25
  47. package/src/trails/compile.ts +59 -0
  48. package/src/trails/completions-complete.ts +165 -0
  49. package/src/trails/completions.ts +47 -0
  50. package/src/trails/create-adapter.ts +1024 -0
  51. package/src/trails/create-scaffold.ts +399 -104
  52. package/src/trails/create-versions.ts +62 -0
  53. package/src/trails/create.ts +186 -72
  54. package/src/trails/deprecate.ts +59 -0
  55. package/src/trails/dev-clean.ts +82 -0
  56. package/src/trails/dev-reset.ts +50 -0
  57. package/src/trails/dev-stats.ts +72 -0
  58. package/src/trails/dev-support.ts +360 -0
  59. package/src/trails/doctor.ts +77 -0
  60. package/src/trails/draft-promote.ts +949 -0
  61. package/src/trails/guide.ts +71 -69
  62. package/src/trails/load-app.ts +1142 -16
  63. package/src/trails/operator-context.ts +66 -0
  64. package/src/trails/project.ts +17 -3
  65. package/src/trails/regrade.ts +292 -0
  66. package/src/trails/release-check.ts +105 -0
  67. package/src/trails/release-smoke.ts +48 -0
  68. package/src/trails/revise.ts +53 -0
  69. package/src/trails/root-dir.ts +21 -0
  70. package/src/trails/run-example.ts +475 -0
  71. package/src/trails/run-examples.ts +129 -0
  72. package/src/trails/run.ts +434 -0
  73. package/src/trails/scaffold-json.ts +58 -0
  74. package/src/trails/survey.ts +896 -227
  75. package/src/trails/topo-activation.ts +14 -0
  76. package/src/trails/topo-constants.ts +2 -0
  77. package/src/trails/topo-history.ts +47 -0
  78. package/src/trails/topo-output-schemas.ts +259 -0
  79. package/src/trails/topo-pin.ts +38 -0
  80. package/src/trails/topo-read-support.ts +363 -0
  81. package/src/trails/topo-reports.ts +809 -0
  82. package/src/trails/topo-store-support.ts +307 -0
  83. package/src/trails/topo-support.ts +219 -0
  84. package/src/trails/topo-unpin.ts +61 -0
  85. package/src/trails/topo.ts +92 -0
  86. package/src/trails/validate.ts +27 -0
  87. package/src/trails/version-lifecycle-support.ts +935 -0
  88. package/src/trails/warden-guide.ts +134 -0
  89. package/src/trails/warden.ts +198 -58
  90. package/src/trails/wayfind.ts +1001 -0
  91. package/src/versions.ts +31 -0
  92. package/.turbo/turbo-build.log +0 -1
  93. package/.turbo/turbo-lint.log +0 -3
  94. package/.turbo/turbo-typecheck.log +0 -1
  95. package/__tests__/examples.test.ts +0 -6
  96. package/dist/bin/trails.d.ts +0 -3
  97. package/dist/bin/trails.d.ts.map +0 -1
  98. package/dist/bin/trails.js +0 -4
  99. package/dist/bin/trails.js.map +0 -1
  100. package/dist/src/app.d.ts +0 -2
  101. package/dist/src/app.d.ts.map +0 -1
  102. package/dist/src/app.js +0 -11
  103. package/dist/src/app.js.map +0 -1
  104. package/dist/src/clack.d.ts +0 -9
  105. package/dist/src/clack.d.ts.map +0 -1
  106. package/dist/src/clack.js +0 -84
  107. package/dist/src/clack.js.map +0 -1
  108. package/dist/src/cli.d.ts +0 -2
  109. package/dist/src/cli.d.ts.map +0 -1
  110. package/dist/src/cli.js +0 -13
  111. package/dist/src/cli.js.map +0 -1
  112. package/dist/src/trails/add-surface.d.ts +0 -13
  113. package/dist/src/trails/add-surface.d.ts.map +0 -1
  114. package/dist/src/trails/add-surface.js +0 -88
  115. package/dist/src/trails/add-surface.js.map +0 -1
  116. package/dist/src/trails/add-trail.d.ts +0 -11
  117. package/dist/src/trails/add-trail.d.ts.map +0 -1
  118. package/dist/src/trails/add-trail.js +0 -85
  119. package/dist/src/trails/add-trail.js.map +0 -1
  120. package/dist/src/trails/add-verify.d.ts +0 -10
  121. package/dist/src/trails/add-verify.d.ts.map +0 -1
  122. package/dist/src/trails/add-verify.js +0 -67
  123. package/dist/src/trails/add-verify.js.map +0 -1
  124. package/dist/src/trails/create-scaffold.d.ts +0 -15
  125. package/dist/src/trails/create-scaffold.d.ts.map +0 -1
  126. package/dist/src/trails/create-scaffold.js +0 -288
  127. package/dist/src/trails/create-scaffold.js.map +0 -1
  128. package/dist/src/trails/create.d.ts +0 -22
  129. package/dist/src/trails/create.d.ts.map +0 -1
  130. package/dist/src/trails/create.js +0 -121
  131. package/dist/src/trails/create.js.map +0 -1
  132. package/dist/src/trails/guide.d.ts +0 -11
  133. package/dist/src/trails/guide.d.ts.map +0 -1
  134. package/dist/src/trails/guide.js +0 -80
  135. package/dist/src/trails/guide.js.map +0 -1
  136. package/dist/src/trails/load-app.d.ts +0 -4
  137. package/dist/src/trails/load-app.d.ts.map +0 -1
  138. package/dist/src/trails/load-app.js +0 -24
  139. package/dist/src/trails/load-app.js.map +0 -1
  140. package/dist/src/trails/project.d.ts +0 -8
  141. package/dist/src/trails/project.d.ts.map +0 -1
  142. package/dist/src/trails/project.js +0 -43
  143. package/dist/src/trails/project.js.map +0 -1
  144. package/dist/src/trails/survey.d.ts +0 -33
  145. package/dist/src/trails/survey.d.ts.map +0 -1
  146. package/dist/src/trails/survey.js +0 -225
  147. package/dist/src/trails/survey.js.map +0 -1
  148. package/dist/src/trails/warden.d.ts +0 -19
  149. package/dist/src/trails/warden.d.ts.map +0 -1
  150. package/dist/src/trails/warden.js +0 -88
  151. package/dist/src/trails/warden.js.map +0 -1
  152. package/dist/tsconfig.tsbuildinfo +0 -1
  153. package/src/__tests__/create.test.ts +0 -349
  154. package/src/__tests__/guide.test.ts +0 -91
  155. package/src/__tests__/load-app.test.ts +0 -15
  156. package/src/__tests__/survey.test.ts +0 -161
  157. package/src/__tests__/warden.test.ts +0 -74
  158. package/tsconfig.json +0 -9
@@ -1,37 +1,1163 @@
1
- import { isAbsolute, resolve } from 'node:path';
2
- import { pathToFileURL } from 'node:url';
1
+ import {
2
+ existsSync,
3
+ readdirSync,
4
+ readFileSync,
5
+ realpathSync,
6
+ statSync,
7
+ symlinkSync,
8
+ } from 'node:fs';
9
+ import {
10
+ dirname,
11
+ extname,
12
+ isAbsolute,
13
+ join,
14
+ relative,
15
+ resolve,
16
+ } from 'node:path';
17
+ import { fileURLToPath, pathToFileURL } from 'node:url';
3
18
 
4
- import type { Topo } from '@ontrails/core';
19
+ import {
20
+ deriveSafePath,
21
+ isTrailsError,
22
+ listWorkspacePackages,
23
+ PermissionError,
24
+ Result,
25
+ ValidationError,
26
+ } from '@ontrails/core';
27
+ import type { CliCommandAliasInput, Topo } from '@ontrails/core';
28
+ import { findAppModule } from '@ontrails/cli';
29
+
30
+ import {
31
+ createLoadAppMirrorRootPath,
32
+ ensureLoadAppMirrorDirectory,
33
+ LOAD_APP_MIRROR_ENTRY_PREFIX,
34
+ LOAD_APP_MIRROR_PARENT_DIRNAME,
35
+ removeLoadAppMirrorRootQuietly,
36
+ resolveLoadAppMirrorFilePath,
37
+ writeLoadAppMirrorFile,
38
+ } from '../load-app-mirror.js';
5
39
 
6
40
  const URL_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]*:/;
7
41
 
8
- /** Resolve a module path from cwd so CLI defaults behave like shell paths. */
9
- const resolveModuleSpecifier = (modulePath: string, cwd: string): string => {
10
- if (URL_SCHEME.test(modulePath)) {
11
- return modulePath;
42
+ type TranspilerLoader = 'ts' | 'tsx' | 'js' | 'jsx';
43
+
44
+ /** Extension → Bun.Transpiler loader, so JSX-bearing files parse correctly. */
45
+ const LOADER_BY_EXTENSION: Record<string, TranspilerLoader> = {
46
+ '.cjs': 'js',
47
+ '.cts': 'ts',
48
+ '.js': 'js',
49
+ '.jsx': 'jsx',
50
+ '.mjs': 'js',
51
+ '.mts': 'ts',
52
+ '.ts': 'ts',
53
+ '.tsx': 'tsx',
54
+ };
55
+
56
+ const SCANNABLE_EXTENSIONS = new Set(Object.keys(LOADER_BY_EXTENSION));
57
+
58
+ const TRANSPILER_CACHE = new Map<TranspilerLoader, Bun.Transpiler>();
59
+
60
+ const getImportScanner = (loader: TranspilerLoader): Bun.Transpiler => {
61
+ const cached = TRANSPILER_CACHE.get(loader);
62
+ if (cached !== undefined) {
63
+ return cached;
64
+ }
65
+ const scanner = new Bun.Transpiler({ loader });
66
+ TRANSPILER_CACHE.set(loader, scanner);
67
+ return scanner;
68
+ };
69
+
70
+ /**
71
+ * Mirror roots kept alive for the lifetime of the process.
72
+ *
73
+ * @remarks
74
+ * A fresh-loaded module may expose functions whose deferred relative imports
75
+ * are resolved only when those functions run (for example inside a trail's
76
+ * `blaze`). If we deleted the mirror tree immediately after the initial
77
+ * `import()` resolved, those later resolutions would hit an ENOENT. We keep
78
+ * the mirrors on disk and clean them up once, on process exit.
79
+ */
80
+ const ACTIVE_MIRROR_ROOTS = new Set<string>();
81
+ const RETAINED_MIRROR_ROOTS = new Set<string>();
82
+
83
+ const cleanupAllMirrorRoots = (): void => {
84
+ for (const root of [...ACTIVE_MIRROR_ROOTS, ...RETAINED_MIRROR_ROOTS]) {
85
+ removeLoadAppMirrorRootQuietly(root);
12
86
  }
87
+ ACTIVE_MIRROR_ROOTS.clear();
88
+ RETAINED_MIRROR_ROOTS.clear();
89
+ };
13
90
 
91
+ const mirrorCleanup = (() => {
92
+ let registered = false;
93
+ return {
94
+ ensureRegistered(): void {
95
+ if (registered) {
96
+ return;
97
+ }
98
+ registered = true;
99
+ process.once('exit', cleanupAllMirrorRoots);
100
+ },
101
+ };
102
+ })();
103
+
104
+ const ensureMirrorCleanupHook = (): void => {
105
+ mirrorCleanup.ensureRegistered();
106
+ };
107
+
108
+ /**
109
+ * Retain a fresh-import mirror for the lifetime of the process. A previously
110
+ * returned `loadApp` result may hold deferred relative `import()` calls whose
111
+ * resolution requires the mirror directory to still exist, so we cannot prune
112
+ * these by age without risking ENOENT in long-lived sessions (dev server,
113
+ * survey polling, concurrent fresh loads). Cleanup happens once on process
114
+ * exit via `cleanupAllMirrorRoots`.
115
+ */
116
+ const retainMirrorRoot = (mirrorRoot: string): void => {
117
+ if (RETAINED_MIRROR_ROOTS.has(mirrorRoot)) {
118
+ return;
119
+ }
120
+ RETAINED_MIRROR_ROOTS.add(mirrorRoot);
121
+ ensureMirrorCleanupHook();
122
+ };
123
+
124
+ const acquireMirrorLease = (mirrorRoot: string): (() => void) => {
125
+ ACTIVE_MIRROR_ROOTS.add(mirrorRoot);
126
+ ensureMirrorCleanupHook();
127
+
128
+ let released = false;
129
+
130
+ return () => {
131
+ if (released) {
132
+ return;
133
+ }
134
+
135
+ released = true;
136
+ ACTIVE_MIRROR_ROOTS.delete(mirrorRoot);
137
+ removeLoadAppMirrorRootQuietly(mirrorRoot);
138
+ };
139
+ };
140
+
141
+ const resolveUrlModulePath = (modulePath: string): string => {
142
+ const url = new URL(modulePath);
143
+ return url.protocol === 'file:' ? fileURLToPath(url) : modulePath;
144
+ };
145
+
146
+ const resolveFilesystemModulePath = (
147
+ modulePath: string,
148
+ cwd: string
149
+ ): string => {
14
150
  const absolutePath = isAbsolute(modulePath)
15
151
  ? modulePath
16
152
  : resolve(cwd, modulePath);
17
- return pathToFileURL(absolutePath).href;
153
+ if (!absolutePath.endsWith('.js') || existsSync(absolutePath)) {
154
+ return absolutePath;
155
+ }
156
+
157
+ const tsPath = absolutePath.replace(/\.js$/, '.ts');
158
+ return existsSync(tsPath) ? tsPath : absolutePath;
18
159
  };
19
160
 
20
- /** Load a Topo export from a module path relative to cwd. */
21
- export const loadApp = async (
161
+ const trustBoundaryError = (reason: string): PermissionError =>
162
+ new PermissionError(
163
+ `Refusing to load an app module outside the workspace trust boundary (${reason}). Use a workspace-relative module path, or pass trustedModulePath: true from trusted code.`
164
+ );
165
+
166
+ const asError = (error: unknown): Error =>
167
+ error instanceof Error ? error : new Error(String(error));
168
+
169
+ const toLoadAppError = (error: unknown): Error => {
170
+ if (isTrailsError(error)) {
171
+ return error;
172
+ }
173
+ const cause = asError(error);
174
+ return new ValidationError(`Failed to load app module: ${cause.message}`, {
175
+ cause,
176
+ context: { detail: cause.message },
177
+ });
178
+ };
179
+
180
+ const isPathInside = (root: string, target: string): boolean => {
181
+ const candidate = relative(root, target);
182
+ return (
183
+ candidate === '' || (!candidate.startsWith('..') && !isAbsolute(candidate))
184
+ );
185
+ };
186
+
187
+ const realpathIfPresent = (path: string): string | undefined => {
188
+ try {
189
+ return realpathSync(path);
190
+ } catch {
191
+ return undefined;
192
+ }
193
+ };
194
+
195
+ const ensureRealPathInsideCwd = (
196
+ resolvedModulePath: string,
197
+ cwd: string
198
+ ): string => {
199
+ const realRoot = realpathIfPresent(cwd);
200
+ const realModule = realpathIfPresent(resolvedModulePath);
201
+ if (
202
+ realRoot !== undefined &&
203
+ realModule !== undefined &&
204
+ !isPathInside(realRoot, realModule)
205
+ ) {
206
+ throw trustBoundaryError('symlink_escape');
207
+ }
208
+
209
+ return resolvedModulePath;
210
+ };
211
+
212
+ /** Resolve a caller-trusted module path using the legacy escape hatch policy. */
213
+ const resolveTrustedModulePath = (modulePath: string, cwd: string): string =>
214
+ URL_SCHEME.test(modulePath)
215
+ ? resolveUrlModulePath(modulePath)
216
+ : resolveFilesystemModulePath(modulePath, cwd);
217
+
218
+ /**
219
+ * Resolve the default app module path inside cwd.
220
+ *
221
+ * @remarks
222
+ * CLI and trail callers accept user-supplied module specifiers, so the default
223
+ * policy is deliberately narrower than `import()` itself: no URL schemes, no
224
+ * absolute paths, and no parent traversal. Internal callers that intentionally
225
+ * load a path outside cwd must opt into `trustedModulePath`.
226
+ */
227
+ const resolveContainedModulePath = (
22
228
  modulePath: string,
23
229
  cwd: string
24
- ): Promise<Topo> => {
25
- const mod = (await import(resolveModuleSpecifier(modulePath, cwd))) as Record<
230
+ ): string => {
231
+ if (URL_SCHEME.test(modulePath)) {
232
+ throw trustBoundaryError('url_scheme');
233
+ }
234
+ if (isAbsolute(modulePath)) {
235
+ throw trustBoundaryError('absolute_path');
236
+ }
237
+
238
+ const safePath = deriveSafePath(cwd, modulePath);
239
+ if (safePath.isErr()) {
240
+ throw trustBoundaryError('parent_escape');
241
+ }
242
+
243
+ return ensureRealPathInsideCwd(
244
+ resolveFilesystemModulePath(safePath.value, cwd),
245
+ cwd
246
+ );
247
+ };
248
+
249
+ interface LoadAppTrustOptions {
250
+ readonly trustedModulePath?: boolean | undefined;
251
+ }
252
+
253
+ const resolveLoadAppModulePath = (
254
+ modulePath: string,
255
+ cwd: string,
256
+ options: LoadAppTrustOptions = {}
257
+ ): string =>
258
+ options.trustedModulePath === true
259
+ ? resolveTrustedModulePath(modulePath, cwd)
260
+ : resolveContainedModulePath(modulePath, cwd);
261
+
262
+ const findWorkspaceRelativeAppModule = (cwd: string): string => {
263
+ const discovered = findAppModule(cwd);
264
+ return isAbsolute(discovered) ? relative(cwd, discovered) : discovered;
265
+ };
266
+
267
+ const isLocalFilesystemImport = (importPath: string): boolean =>
268
+ importPath.startsWith('.') ||
269
+ importPath.startsWith('/') ||
270
+ importPath.startsWith('file:');
271
+
272
+ interface PackageJson {
273
+ readonly exports?: unknown;
274
+ readonly imports?: unknown;
275
+ readonly main?: unknown;
276
+ readonly module?: unknown;
277
+ readonly name?: unknown;
278
+ readonly type?: unknown;
279
+ readonly workspaces?: unknown;
280
+ }
281
+
282
+ type NamedPackageJson = PackageJson & { readonly name: string };
283
+
284
+ const readPackageJson = (packagePath: string): PackageJson | undefined => {
285
+ try {
286
+ return JSON.parse(readFileSync(packagePath, 'utf8')) as PackageJson;
287
+ } catch {
288
+ return undefined;
289
+ }
290
+ };
291
+
292
+ const readPackageName = (packagePath: string): string | undefined => {
293
+ const parsed = readPackageJson(packagePath);
294
+ return typeof parsed?.name === 'string' && parsed.name.length > 0
295
+ ? parsed.name
296
+ : undefined;
297
+ };
298
+
299
+ const findNearestPackageName = (directoryPath: string): string | undefined => {
300
+ let current = directoryPath;
301
+ while (true) {
302
+ const name = readPackageName(join(current, 'package.json'));
303
+ if (name !== undefined) {
304
+ return name;
305
+ }
306
+
307
+ const parent = dirname(current);
308
+ if (parent === current) {
309
+ return undefined;
310
+ }
311
+ current = parent;
312
+ }
313
+ };
314
+
315
+ const isPackageLocalImport = (
316
+ importerPath: string,
317
+ importPath: string
318
+ ): boolean => {
319
+ if (importPath.startsWith('#')) {
320
+ return true;
321
+ }
322
+
323
+ const packageName = findNearestPackageName(dirname(importerPath));
324
+ return (
325
+ packageName !== undefined &&
326
+ (importPath === packageName || importPath.startsWith(`${packageName}/`))
327
+ );
328
+ };
329
+
330
+ const isScannableModule = (modulePath: string): boolean =>
331
+ SCANNABLE_EXTENSIONS.has(extname(modulePath));
332
+
333
+ const readDirectoryEntries = (directoryPath: string): readonly string[] => {
334
+ try {
335
+ return readdirSync(directoryPath);
336
+ } catch {
337
+ return [];
338
+ }
339
+ };
340
+
341
+ const safeStat = (
342
+ entryPath: string
343
+ ): ReturnType<typeof statSync> | undefined => {
344
+ try {
345
+ return statSync(entryPath);
346
+ } catch {
347
+ return undefined;
348
+ }
349
+ };
350
+
351
+ const resolveImportedModulePath = (
352
+ importerPath: string,
353
+ importPath: string
354
+ ): string => {
355
+ const resolved = import.meta.resolve(
356
+ importPath,
357
+ pathToFileURL(importerPath).href
358
+ );
359
+ return resolveFilesystemModulePath(
360
+ fileURLToPath(resolved),
361
+ dirname(importerPath)
362
+ );
363
+ };
364
+
365
+ interface WorkspacePackage {
366
+ readonly name: string;
367
+ readonly packageJson: PackageJson;
368
+ readonly packageRoot: string;
369
+ }
370
+
371
+ const readWorkspacePackages = (cwd: string): readonly WorkspacePackage[] =>
372
+ listWorkspacePackages<NamedPackageJson>(cwd).map((workspacePackage) => ({
373
+ name: workspacePackage.manifest.name,
374
+ packageJson: workspacePackage.manifest,
375
+ packageRoot: workspacePackage.packageRoot,
376
+ }));
377
+
378
+ const parsePackageSpecifier = (
379
+ importPath: string
380
+ ): { packageName: string; subpath: string } | null => {
381
+ if (URL_SCHEME.test(importPath)) {
382
+ return null;
383
+ }
384
+ if (importPath.startsWith('.') || importPath.startsWith('#')) {
385
+ return null;
386
+ }
387
+ const segments = importPath.split('/');
388
+ const [firstSegment, secondSegment] = segments;
389
+ if (firstSegment?.startsWith('@')) {
390
+ const scope = firstSegment;
391
+ const name = secondSegment;
392
+ if (name === undefined) {
393
+ return null;
394
+ }
395
+ const packageName = `${scope}/${name}`;
396
+ const rest = segments.slice(2).join('/');
397
+ return { packageName, subpath: rest.length > 0 ? `./${rest}` : '.' };
398
+ }
399
+ const [name] = segments;
400
+ if (name === undefined || name.length === 0) {
401
+ return null;
402
+ }
403
+ const rest = segments.slice(1).join('/');
404
+ return { packageName: name, subpath: rest.length > 0 ? `./${rest}` : '.' };
405
+ };
406
+
407
+ const resolveConditionalExportTarget = (
408
+ target: unknown
409
+ ): string | undefined => {
410
+ if (typeof target === 'string') {
411
+ return target;
412
+ }
413
+ if (typeof target !== 'object' || target === null || Array.isArray(target)) {
414
+ return undefined;
415
+ }
416
+ const record = target as Record<string, unknown>;
417
+ return (
418
+ resolveConditionalExportTarget(record['import']) ??
419
+ resolveConditionalExportTarget(record['default']) ??
420
+ resolveConditionalExportTarget(record['bun']) ??
421
+ resolveConditionalExportTarget(record['node'])
422
+ );
423
+ };
424
+
425
+ const resolveExportTarget = (
426
+ packageJson: PackageJson,
427
+ subpath: string
428
+ ): string | undefined => {
429
+ const { exports } = packageJson;
430
+ if (exports === undefined) {
431
+ if (subpath === '.') {
432
+ if (typeof packageJson.module === 'string') {
433
+ return packageJson.module;
434
+ }
435
+ if (typeof packageJson.main === 'string') {
436
+ return packageJson.main;
437
+ }
438
+ return './src/index.ts';
439
+ }
440
+ return subpath;
441
+ }
442
+ if (typeof exports === 'string' || Array.isArray(exports)) {
443
+ return subpath === '.'
444
+ ? resolveConditionalExportTarget(exports)
445
+ : undefined;
446
+ }
447
+ if (typeof exports !== 'object' || exports === null) {
448
+ return undefined;
449
+ }
450
+ return resolveConditionalExportTarget(
451
+ (exports as Record<string, unknown>)[subpath]
452
+ );
453
+ };
454
+
455
+ interface WorkspacePackageResolution {
456
+ readonly modulePath: string;
457
+ readonly packageName: string;
458
+ readonly packageRoot: string;
459
+ }
460
+
461
+ const resolveWorkspacePackageImport = (
462
+ importPath: string,
463
+ cwd: string
464
+ ): WorkspacePackageResolution | null => {
465
+ const parsed = parsePackageSpecifier(importPath);
466
+ if (parsed === null) {
467
+ return null;
468
+ }
469
+ const workspacePackage = readWorkspacePackages(cwd).find(
470
+ (candidate) => candidate.name === parsed.packageName
471
+ );
472
+ if (workspacePackage === undefined) {
473
+ return null;
474
+ }
475
+ const target = resolveExportTarget(
476
+ workspacePackage.packageJson,
477
+ parsed.subpath
478
+ );
479
+ if (target === undefined || !target.startsWith('.')) {
480
+ return null;
481
+ }
482
+ const targetPath = deriveSafePath(workspacePackage.packageRoot, target);
483
+ if (targetPath.isErr()) {
484
+ return null;
485
+ }
486
+ return {
487
+ modulePath: resolveFilesystemModulePath(
488
+ ensureRealPathInsideCwd(targetPath.value, cwd),
489
+ workspacePackage.packageRoot
490
+ ),
491
+ packageName: workspacePackage.name,
492
+ packageRoot: workspacePackage.packageRoot,
493
+ };
494
+ };
495
+
496
+ const findPackageRootForName = (
497
+ directoryPath: string,
498
+ packageName: string
499
+ ): string | null => {
500
+ let current = directoryPath;
501
+ while (true) {
502
+ const packagePath = join(current, 'package.json');
503
+ if (readPackageName(packagePath) === packageName) {
504
+ return current;
505
+ }
506
+
507
+ const parent = dirname(current);
508
+ if (parent === current) {
509
+ return null;
510
+ }
511
+ current = parent;
512
+ }
513
+ };
514
+
515
+ interface ExternalPackageResolution {
516
+ readonly packageName: string;
517
+ readonly packageRoot: string;
518
+ }
519
+
520
+ const resolveExternalPackageImport = (
521
+ importerPath: string,
522
+ importPath: string
523
+ ): ExternalPackageResolution | null => {
524
+ const parsed = parsePackageSpecifier(importPath);
525
+ if (parsed === null) {
526
+ return null;
527
+ }
528
+ let resolved: string;
529
+ try {
530
+ resolved = import.meta.resolve(
531
+ importPath,
532
+ pathToFileURL(importerPath).href
533
+ );
534
+ } catch {
535
+ return null;
536
+ }
537
+ if (!resolved.startsWith('file:')) {
538
+ return null;
539
+ }
540
+ const packageRoot = findPackageRootForName(
541
+ dirname(fileURLToPath(resolved)),
542
+ parsed.packageName
543
+ );
544
+ return packageRoot === null
545
+ ? null
546
+ : { packageName: parsed.packageName, packageRoot };
547
+ };
548
+
549
+ type MirrorImportResolution =
550
+ | {
551
+ readonly kind: 'module';
552
+ readonly modulePath: string;
553
+ }
554
+ | {
555
+ readonly kind: 'external-package';
556
+ readonly packageName: string;
557
+ readonly packageRoot: string;
558
+ }
559
+ | {
560
+ readonly kind: 'workspace-package';
561
+ readonly modulePath: string;
562
+ readonly packageName: string;
563
+ readonly packageRoot: string;
564
+ };
565
+
566
+ const resolveMirrorImport = (
567
+ importerPath: string,
568
+ importPath: string,
569
+ cwd: string
570
+ ): MirrorImportResolution | null => {
571
+ if (
572
+ isLocalFilesystemImport(importPath) ||
573
+ isPackageLocalImport(importerPath, importPath)
574
+ ) {
575
+ return {
576
+ kind: 'module',
577
+ modulePath: resolveImportedModulePath(importerPath, importPath),
578
+ };
579
+ }
580
+
581
+ const workspacePackage = resolveWorkspacePackageImport(importPath, cwd);
582
+ if (workspacePackage !== null) {
583
+ return { kind: 'workspace-package', ...workspacePackage };
584
+ }
585
+
586
+ const externalPackage = resolveExternalPackageImport(
587
+ importerPath,
588
+ importPath
589
+ );
590
+ return externalPackage === null
591
+ ? null
592
+ : { kind: 'external-package', ...externalPackage };
593
+ };
594
+
595
+ const collectImportedModuleResolutions = (
596
+ modulePath: string,
597
+ source: string,
598
+ cwd: string
599
+ ): readonly MirrorImportResolution[] => {
600
+ const extension = extname(modulePath);
601
+ const loader = LOADER_BY_EXTENSION[extension];
602
+ if (loader === undefined) {
603
+ return [];
604
+ }
605
+
606
+ return getImportScanner(loader)
607
+ .scanImports(source)
608
+ .map((entry) => entry.path)
609
+ .map((importPath) => resolveMirrorImport(modulePath, importPath, cwd))
610
+ .filter(
611
+ (resolution): resolution is MirrorImportResolution => resolution !== null
612
+ );
613
+ };
614
+
615
+ const copyFileToMirror = async (
616
+ sourcePath: string,
617
+ mirrorRoot: string,
618
+ copied: Set<string>
619
+ ): Promise<void> => {
620
+ if (copied.has(sourcePath)) {
621
+ return;
622
+ }
623
+ copied.add(sourcePath);
624
+
625
+ const written = await writeLoadAppMirrorFile(sourcePath, mirrorRoot);
626
+ if (written.isErr()) {
627
+ throw written.error;
628
+ }
629
+ };
630
+
631
+ /**
632
+ * Directory basenames that are never worth mirroring.
633
+ *
634
+ * @remarks
635
+ * These directories are excluded because they can be large and are never
636
+ * sources of resolvable imports — they hold VCS metadata, package installs,
637
+ * prior mirror artifacts, or build/tooling output that module resolution
638
+ * should not touch.
639
+ */
640
+ const MIRROR_SKIP_DIRECTORIES = new Set([
641
+ '.cache',
642
+ '.git',
643
+ '.next',
644
+ '.nuxt',
645
+ '.output',
646
+ '.svelte-kit',
647
+ '.trails-tmp',
648
+ '.turbo',
649
+ 'build',
650
+ 'coverage',
651
+ 'dist',
652
+ 'node_modules',
653
+ ]);
654
+
655
+ /**
656
+ * Recursively copy every regular file inside `directoryPath` into the
657
+ * mirror, skipping well-known heavy directories.
658
+ *
659
+ * @remarks
660
+ * `Bun.Transpiler#scanImports` only surfaces statically analyzable import
661
+ * specifiers. Computed dynamic imports such as `import(\`./${name}.ts\`)`
662
+ * never appear, so their targets would otherwise be missing from the
663
+ * mirror. Shadowing each directory touched by the static walk with its
664
+ * full subtree keeps those sibling modules resolvable under the mirror
665
+ * root at runtime without pulling in package installs or nested mirror
666
+ * artifacts.
667
+ */
668
+ /**
669
+ * Age threshold (ms) above which a mirror entry in `.trails-tmp/` is
670
+ * considered stale and safe to remove opportunistically.
671
+ *
672
+ * @remarks
673
+ * Fresh loads complete in seconds. Anything older than 10 minutes is almost
674
+ * certainly left over from a crashed or signal-killed process. We intentionally
675
+ * avoid registering SIGTERM/SIGINT handlers here because that would risk
676
+ * clobbering host-app signal handlers (and still wouldn't rescue SIGKILL).
677
+ * Opportunistic cleanup is self-healing across crashes from any cause.
678
+ */
679
+ const STALE_MIRROR_THRESHOLD_MS = 10 * 60 * 1000;
680
+
681
+ const isStaleMirrorEntry = (entryPath: string, now: number): boolean => {
682
+ if (
683
+ ACTIVE_MIRROR_ROOTS.has(entryPath) ||
684
+ RETAINED_MIRROR_ROOTS.has(entryPath)
685
+ ) {
686
+ return false;
687
+ }
688
+ const entryStat = safeStat(entryPath);
689
+ if (entryStat === undefined) {
690
+ return false;
691
+ }
692
+ const mtimeMs = Number(entryStat.mtimeMs);
693
+ return now - mtimeMs >= STALE_MIRROR_THRESHOLD_MS;
694
+ };
695
+
696
+ const removeStaleMirrorEntry = (entryPath: string): void => {
697
+ /*
698
+ * Another concurrent load may own it. Safe to ignore — the next sweep
699
+ * will retry.
700
+ */
701
+ removeLoadAppMirrorRootQuietly(entryPath);
702
+ };
703
+
704
+ /**
705
+ * Best-effort removal of stale mirror directories left by previous (crashed or
706
+ * signal-killed) processes. Called before creating a new mirror root.
707
+ */
708
+ const cleanupStaleMirrorRoots = (mirrorParent: string): void => {
709
+ const entries = readDirectoryEntries(mirrorParent);
710
+ if (entries.length === 0) {
711
+ return;
712
+ }
713
+ const now = Date.now();
714
+ for (const entry of entries) {
715
+ if (!entry.startsWith(LOAD_APP_MIRROR_ENTRY_PREFIX)) {
716
+ continue;
717
+ }
718
+ const entryPath = join(mirrorParent, entry);
719
+ if (isStaleMirrorEntry(entryPath, now)) {
720
+ removeStaleMirrorEntry(entryPath);
721
+ }
722
+ }
723
+ };
724
+
725
+ const freshMirrorRootPath = (cwd: string): string => {
726
+ const mirrorParent = join(cwd, LOAD_APP_MIRROR_PARENT_DIRNAME);
727
+ cleanupStaleMirrorRoots(mirrorParent);
728
+ return createLoadAppMirrorRootPath(cwd);
729
+ };
730
+
731
+ interface MirrorWalkContext {
732
+ readonly cwd: string;
733
+ readonly mirrorRoot: string;
734
+ readonly copied: Set<string>;
735
+ readonly visitedDirectories: Set<string>;
736
+ readonly linkedPackageNames: Set<string>;
737
+ }
738
+
739
+ type DirectoryEntryKind = 'directory' | 'file' | 'skip';
740
+
741
+ const classifyDirectoryEntry = (
742
+ entry: string,
743
+ entryPath: string
744
+ ): DirectoryEntryKind => {
745
+ const entryStat = safeStat(entryPath);
746
+ if (entryStat === undefined) {
747
+ return 'skip';
748
+ }
749
+ if (entryStat.isDirectory()) {
750
+ return MIRROR_SKIP_DIRECTORIES.has(entry) ? 'skip' : 'directory';
751
+ }
752
+ return entryStat.isFile() ? 'file' : 'skip';
753
+ };
754
+
755
+ const copyDirectoryTreeToMirror = async (
756
+ directoryPath: string,
757
+ context: MirrorWalkContext
758
+ ): Promise<void> => {
759
+ if (context.visitedDirectories.has(directoryPath)) {
760
+ return;
761
+ }
762
+ context.visitedDirectories.add(directoryPath);
763
+
764
+ for (const entry of readDirectoryEntries(directoryPath)) {
765
+ const entryPath = join(directoryPath, entry);
766
+ const kind = classifyDirectoryEntry(entry, entryPath);
767
+ if (kind === 'directory') {
768
+ await copyDirectoryTreeToMirror(entryPath, context);
769
+ } else if (kind === 'file') {
770
+ await copyFileToMirror(entryPath, context.mirrorRoot, context.copied);
771
+ }
772
+ }
773
+ };
774
+
775
+ const copyNearestPackageJsonToMirror = async (
776
+ directoryPath: string,
777
+ context: MirrorWalkContext
778
+ ): Promise<void> => {
779
+ let current = directoryPath;
780
+ while (true) {
781
+ const packagePath = join(current, 'package.json');
782
+ const packageStat = safeStat(packagePath);
783
+ if (packageStat?.isFile()) {
784
+ await copyFileToMirror(packagePath, context.mirrorRoot, context.copied);
785
+ return;
786
+ }
787
+
788
+ const parent = dirname(current);
789
+ if (parent === current) {
790
+ return;
791
+ }
792
+ current = parent;
793
+ }
794
+ };
795
+
796
+ const packageLinkSegments = (packageName: string): readonly string[] =>
797
+ packageName.split('/').filter((segment) => segment.length > 0);
798
+
799
+ const createPackageMirrorLink = (
800
+ packageName: string,
801
+ targetRoot: string,
802
+ context: MirrorWalkContext
803
+ ): void => {
804
+ if (context.linkedPackageNames.has(packageName)) {
805
+ return;
806
+ }
807
+ const mirrorWorkspaceRoot = resolveLoadAppMirrorFilePath(
808
+ context.cwd,
809
+ context.mirrorRoot
810
+ );
811
+ if (mirrorWorkspaceRoot.isErr()) {
812
+ throw mirrorWorkspaceRoot.error;
813
+ }
814
+ const linkPath = join(
815
+ mirrorWorkspaceRoot.value,
816
+ 'node_modules',
817
+ ...packageLinkSegments(packageName)
818
+ );
819
+
820
+ const ensured = ensureLoadAppMirrorDirectory(
821
+ dirname(linkPath),
822
+ context.mirrorRoot
823
+ );
824
+ if (ensured.isErr()) {
825
+ throw ensured.error;
826
+ }
827
+
828
+ try {
829
+ symlinkSync(targetRoot, linkPath, 'dir');
830
+ } catch (error) {
831
+ if (
832
+ !(error instanceof Error) ||
833
+ !('code' in error) ||
834
+ error.code !== 'EEXIST'
835
+ ) {
836
+ throw error;
837
+ }
838
+ }
839
+ context.linkedPackageNames.add(packageName);
840
+ };
841
+
842
+ const createWorkspacePackageMirrorLink = (
843
+ packageName: string,
844
+ packageRoot: string,
845
+ context: MirrorWalkContext
846
+ ): void => {
847
+ const mirrorPackageRoot = resolveLoadAppMirrorFilePath(
848
+ packageRoot,
849
+ context.mirrorRoot
850
+ );
851
+ if (mirrorPackageRoot.isErr()) {
852
+ throw mirrorPackageRoot.error;
853
+ }
854
+ createPackageMirrorLink(packageName, mirrorPackageRoot.value, context);
855
+ };
856
+
857
+ const mirrorImportedModule = async (
858
+ modulePath: string,
859
+ context: MirrorWalkContext
860
+ ): Promise<void> => {
861
+ const moduleDirectory = dirname(modulePath);
862
+ await copyNearestPackageJsonToMirror(moduleDirectory, context);
863
+ if (context.visitedDirectories.has(moduleDirectory)) {
864
+ await copyFileToMirror(modulePath, context.mirrorRoot, context.copied);
865
+ return;
866
+ }
867
+ await copyDirectoryTreeToMirror(moduleDirectory, context);
868
+ };
869
+
870
+ const scanAndVisitLocalImports = async (
871
+ modulePath: string,
872
+ context: MirrorWalkContext,
873
+ visit: (path: string) => Promise<void>
874
+ ): Promise<void> => {
875
+ if (!isScannableModule(modulePath)) {
876
+ return;
877
+ }
878
+ const source = await Bun.file(modulePath).text();
879
+ for (const imported of collectImportedModuleResolutions(
880
+ modulePath,
881
+ source,
882
+ context.cwd
883
+ )) {
884
+ if (imported.kind === 'external-package') {
885
+ createPackageMirrorLink(
886
+ imported.packageName,
887
+ imported.packageRoot,
888
+ context
889
+ );
890
+ continue;
891
+ }
892
+ if (imported.kind === 'workspace-package') {
893
+ createWorkspacePackageMirrorLink(
894
+ imported.packageName,
895
+ imported.packageRoot,
896
+ context
897
+ );
898
+ }
899
+ await visit(imported.modulePath);
900
+ }
901
+ };
902
+
903
+ const mirrorFreshImportGraph = async (
904
+ entryPath: string,
905
+ cwd: string,
906
+ mirrorRoot: string
907
+ ): Promise<string> => {
908
+ const scanned = new Set<string>();
909
+ const context: MirrorWalkContext = {
910
+ copied: new Set<string>(),
911
+ cwd,
912
+ linkedPackageNames: new Set<string>(),
913
+ mirrorRoot,
914
+ visitedDirectories: new Set<string>(),
915
+ };
916
+
917
+ const visit = async (modulePath: string): Promise<void> => {
918
+ if (scanned.has(modulePath)) {
919
+ return;
920
+ }
921
+ scanned.add(modulePath);
922
+ await scanAndVisitLocalImports(modulePath, context, visit);
923
+ await mirrorImportedModule(modulePath, context);
924
+ };
925
+
926
+ await visit(entryPath);
927
+ const freshPath = resolveLoadAppMirrorFilePath(entryPath, mirrorRoot);
928
+ if (freshPath.isErr()) {
929
+ throw freshPath.error;
930
+ }
931
+ return freshPath.value;
932
+ };
933
+
934
+ /**
935
+ * Import a module bypassing the ESM cache for the local filesystem import graph.
936
+ *
937
+ * @remarks
938
+ * External packages and built-in modules still resolve normally. Only local
939
+ * filesystem imports are mirrored into the fresh temp root. The mirror tree
940
+ * is retained for the lifetime of the process so that deferred relative
941
+ * `import()`/`require()` calls originating from the loaded module (e.g.
942
+ * inside a trail's `blaze`) can still resolve. If the graph walk itself
943
+ * fails, the partially-written mirror is removed immediately so failed
944
+ * loads do not leak disk space.
945
+ */
946
+ const importWithCacheBust = async (
947
+ absolutePath: string
948
+ ): Promise<Record<string, unknown>> => {
949
+ const url = new URL(absolutePath);
950
+ url.searchParams.set('t', Date.now().toString());
951
+ return (await import(url.href)) as Record<string, unknown>;
952
+ };
953
+
954
+ const prepareMirror = async (
955
+ absolutePath: string,
956
+ cwd: string
957
+ ): Promise<{ mirrorRoot: string; freshPath: string }> => {
958
+ const resolvedCwd = resolve(cwd);
959
+ const mirrorRoot = freshMirrorRootPath(resolvedCwd);
960
+ try {
961
+ const freshPath = await mirrorFreshImportGraph(
962
+ absolutePath,
963
+ resolvedCwd,
964
+ mirrorRoot
965
+ );
966
+ return { freshPath, mirrorRoot };
967
+ } catch (error) {
968
+ removeLoadAppMirrorRootQuietly(mirrorRoot);
969
+ throw error;
970
+ }
971
+ };
972
+
973
+ const importFreshModule = async (
974
+ resolvedModulePath: string,
975
+ cwd: string
976
+ ): Promise<Record<string, unknown>> => {
977
+ const absolutePath = resolvedModulePath;
978
+ if (URL_SCHEME.test(absolutePath) && !absolutePath.startsWith('/')) {
979
+ return await importWithCacheBust(absolutePath);
980
+ }
981
+
982
+ const { mirrorRoot, freshPath } = await prepareMirror(absolutePath, cwd);
983
+ retainMirrorRoot(mirrorRoot);
984
+ return (await import(pathToFileURL(freshPath).href)) as Record<
26
985
  string,
27
986
  unknown
28
987
  >;
29
- const app = (mod['default'] ?? mod['app']) as Topo | undefined;
988
+ };
989
+
990
+ const resolveLoadedTopo = (
991
+ effectivePath: string,
992
+ mod: Record<string, unknown>
993
+ ): Topo => {
994
+ const app = (mod['default'] ?? mod['graph'] ?? mod['app']) as
995
+ | Topo
996
+ | undefined;
30
997
  if (!app?.trails) {
31
- throw new Error(
32
- `Could not find a Topo export in "${modulePath}". ` +
33
- "Expected a default or named 'app' export created with topo()."
998
+ throw new ValidationError(
999
+ `Could not find a Topo export in "${effectivePath}". ` +
1000
+ "Expected a default, 'graph', or 'app' named export created with topo()."
34
1001
  );
35
1002
  }
36
1003
  return app;
37
1004
  };
1005
+
1006
+ type LoadedCliAliases = Readonly<
1007
+ Record<string, readonly CliCommandAliasInput[]>
1008
+ >;
1009
+
1010
+ const isStringArray = (value: unknown): value is readonly string[] =>
1011
+ Array.isArray(value) && value.every((item) => typeof item === 'string');
1012
+
1013
+ const isCliAliasInput = (value: unknown): value is CliCommandAliasInput =>
1014
+ typeof value === 'string' || isStringArray(value);
1015
+
1016
+ const resolveLoadedCliAliases = (
1017
+ effectivePath: string,
1018
+ mod: Record<string, unknown>
1019
+ ): LoadedCliAliases | undefined => {
1020
+ const value = mod['trailsCliAliases'] ?? mod['cliAliases'];
1021
+ if (value === undefined) {
1022
+ return undefined;
1023
+ }
1024
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
1025
+ throw new ValidationError(
1026
+ `CLI alias export in "${effectivePath}" must be a record from trail ID to alias list.`
1027
+ );
1028
+ }
1029
+
1030
+ const aliases = value as Record<string, unknown>;
1031
+ for (const [trailId, trailAliases] of Object.entries(aliases)) {
1032
+ if (!Array.isArray(trailAliases)) {
1033
+ throw new ValidationError(
1034
+ `CLI alias export for trail "${trailId}" in "${effectivePath}" must be an array.`
1035
+ );
1036
+ }
1037
+ for (const alias of trailAliases) {
1038
+ if (!isCliAliasInput(alias)) {
1039
+ throw new ValidationError(
1040
+ `CLI alias export for trail "${trailId}" in "${effectivePath}" must contain string aliases or string-array paths.`
1041
+ );
1042
+ }
1043
+ }
1044
+ }
1045
+
1046
+ return aliases as LoadedCliAliases;
1047
+ };
1048
+
1049
+ export interface FreshAppLease {
1050
+ readonly app: Topo;
1051
+ readonly cliAliases?: LoadedCliAliases | undefined;
1052
+ readonly mirrorRoot: string;
1053
+ readonly release: () => void;
1054
+ }
1055
+
1056
+ export type LoadAppLeaseOptions = LoadAppTrustOptions;
1057
+
1058
+ export interface LoadAppOptions extends LoadAppTrustOptions {
1059
+ readonly fresh?: boolean | undefined;
1060
+ }
1061
+
1062
+ const noopRelease = (): void => undefined;
1063
+
1064
+ const createUrlSchemeLease = async (
1065
+ absolutePath: string,
1066
+ effectivePath: string
1067
+ ): Promise<FreshAppLease> => {
1068
+ const mod = (await importWithCacheBust(absolutePath)) as Record<
1069
+ string,
1070
+ unknown
1071
+ >;
1072
+ return {
1073
+ app: resolveLoadedTopo(effectivePath, mod),
1074
+ cliAliases: resolveLoadedCliAliases(effectivePath, mod),
1075
+ mirrorRoot: absolutePath,
1076
+ release: noopRelease,
1077
+ };
1078
+ };
1079
+
1080
+ const createFilesystemLease = async (
1081
+ absolutePath: string,
1082
+ cwd: string,
1083
+ effectivePath: string
1084
+ ): Promise<FreshAppLease> => {
1085
+ const { mirrorRoot, freshPath } = await prepareMirror(absolutePath, cwd);
1086
+ const release = acquireMirrorLease(mirrorRoot);
1087
+
1088
+ try {
1089
+ const mod = (await import(pathToFileURL(freshPath).href)) as Record<
1090
+ string,
1091
+ unknown
1092
+ >;
1093
+
1094
+ return {
1095
+ app: resolveLoadedTopo(effectivePath, mod),
1096
+ cliAliases: resolveLoadedCliAliases(effectivePath, mod),
1097
+ mirrorRoot,
1098
+ release,
1099
+ };
1100
+ } catch (error) {
1101
+ release();
1102
+ throw error;
1103
+ }
1104
+ };
1105
+
1106
+ export const loadFreshAppLease = async (
1107
+ modulePath: string | undefined,
1108
+ cwd: string,
1109
+ options: LoadAppLeaseOptions = {}
1110
+ ): Promise<FreshAppLease> => {
1111
+ const effectivePath =
1112
+ modulePath === undefined ? findWorkspaceRelativeAppModule(cwd) : modulePath;
1113
+ const absolutePath = resolveLoadAppModulePath(effectivePath, cwd, options);
1114
+
1115
+ return URL_SCHEME.test(absolutePath) && !absolutePath.startsWith('/')
1116
+ ? await createUrlSchemeLease(absolutePath, effectivePath)
1117
+ : await createFilesystemLease(absolutePath, cwd, effectivePath);
1118
+ };
1119
+
1120
+ export const tryLoadFreshAppLease = async (
1121
+ modulePath: string | undefined,
1122
+ cwd: string,
1123
+ options: LoadAppLeaseOptions = {}
1124
+ ): Promise<Result<FreshAppLease, Error>> => {
1125
+ try {
1126
+ return Result.ok(await loadFreshAppLease(modulePath, cwd, options));
1127
+ } catch (error) {
1128
+ return Result.err(toLoadAppError(error));
1129
+ }
1130
+ };
1131
+
1132
+ /**
1133
+ * Load a Topo export from a module path relative to cwd.
1134
+ *
1135
+ * @remarks
1136
+ * By default, `modulePath` must be workspace-relative and stay under `cwd`.
1137
+ * URL-shaped, absolute, and parent-escape paths are rejected. Trusted internal
1138
+ * callers can pass `trustedModulePath: true` to deliberately use the broader
1139
+ * dynamic-import escape hatch.
1140
+ */
1141
+ export const loadApp = async (
1142
+ modulePath: string | undefined,
1143
+ cwd: string,
1144
+ options: LoadAppOptions = {}
1145
+ ): Promise<Topo> => {
1146
+ const effectivePath =
1147
+ modulePath === undefined ? findWorkspaceRelativeAppModule(cwd) : modulePath;
1148
+ const resolvedModulePath = resolveLoadAppModulePath(
1149
+ effectivePath,
1150
+ cwd,
1151
+ options
1152
+ );
1153
+ const mod =
1154
+ options.fresh === true
1155
+ ? await importFreshModule(resolvedModulePath, cwd)
1156
+ : ((await import(
1157
+ URL_SCHEME.test(resolvedModulePath) &&
1158
+ !resolvedModulePath.startsWith('/')
1159
+ ? new URL(resolvedModulePath).href
1160
+ : pathToFileURL(resolvedModulePath).href
1161
+ )) as Record<string, unknown>);
1162
+ return resolveLoadedTopo(effectivePath, mod);
1163
+ };