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

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.
@@ -0,0 +1,390 @@
1
+ /* oxlint-disable max-lines-per-function, max-statements -- release-note parsing keeps source extraction explicit. */
2
+ import { readdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+
5
+ const DEFAULT_REPO = 'outfitter-dev/trails';
6
+
7
+ interface PackageJson {
8
+ readonly name?: string;
9
+ readonly private?: boolean;
10
+ readonly version?: string;
11
+ }
12
+
13
+ interface ReleaseNotesWorkspace {
14
+ readonly isPrivate: boolean;
15
+ readonly name: string;
16
+ readonly path: string;
17
+ readonly version: string;
18
+ }
19
+
20
+ export interface ReleaseNotesChange {
21
+ readonly commit?: string | undefined;
22
+ readonly packages: readonly string[];
23
+ readonly summary: string;
24
+ readonly url?: string | undefined;
25
+ }
26
+
27
+ export interface ReleaseNotesPackageVersion {
28
+ readonly name: string;
29
+ readonly version: string;
30
+ }
31
+
32
+ export interface ReleaseNotesInput {
33
+ readonly changes: readonly ReleaseNotesChange[];
34
+ readonly distTag: string;
35
+ readonly mode: 'github-release' | 'release-pr';
36
+ readonly packageVersions: readonly ReleaseNotesPackageVersion[];
37
+ readonly repo?: string | undefined;
38
+ readonly version: string;
39
+ }
40
+
41
+ interface ChangelogEntryInput {
42
+ readonly changelog: string;
43
+ readonly packageName: string;
44
+ readonly version: string;
45
+ }
46
+
47
+ export interface ReleaseNotesParsedChange {
48
+ readonly commit?: string | undefined;
49
+ readonly packageName: string;
50
+ readonly summary: string;
51
+ readonly url?: string | undefined;
52
+ }
53
+
54
+ export interface ReleaseNotesCollectOptions {
55
+ readonly distTag?: string | undefined;
56
+ readonly mode: ReleaseNotesInput['mode'];
57
+ readonly repo?: string | undefined;
58
+ readonly repoRoot: string;
59
+ readonly version?: string | undefined;
60
+ }
61
+
62
+ interface CurrentParsedChange {
63
+ readonly commit?: string | undefined;
64
+ readonly packageName: string;
65
+ readonly summaryLines: string[];
66
+ readonly url?: string | undefined;
67
+ }
68
+
69
+ const readJson = async <T>(path: string): Promise<T> =>
70
+ (await Bun.file(path).json()) as T;
71
+
72
+ const discoverWorkspaceDirs = async (
73
+ repoRoot: string,
74
+ patterns: readonly string[]
75
+ ): Promise<string[]> => {
76
+ const dirs: string[] = [];
77
+
78
+ for (const pattern of patterns) {
79
+ if (pattern.endsWith('/*')) {
80
+ const parent = join(repoRoot, pattern.slice(0, -2));
81
+ const entries = await readdir(parent, { withFileTypes: true });
82
+ for (const entry of entries) {
83
+ if (!entry.isDirectory()) {
84
+ continue;
85
+ }
86
+ const dir = join(parent, entry.name);
87
+ if (await Bun.file(join(dir, 'package.json')).exists()) {
88
+ dirs.push(dir);
89
+ }
90
+ }
91
+ continue;
92
+ }
93
+
94
+ const dir = join(repoRoot, pattern);
95
+ if (await Bun.file(join(dir, 'package.json')).exists()) {
96
+ dirs.push(dir);
97
+ }
98
+ }
99
+
100
+ return dirs;
101
+ };
102
+
103
+ const discoverReleaseNotesWorkspaces = async (
104
+ repoRoot: string,
105
+ { includePrivate }: { readonly includePrivate: boolean }
106
+ ): Promise<ReleaseNotesWorkspace[]> => {
107
+ const root = await readJson<{ workspaces?: string[] }>(
108
+ join(repoRoot, 'package.json')
109
+ );
110
+ const dirs = await discoverWorkspaceDirs(repoRoot, root.workspaces ?? []);
111
+ const workspaces: ReleaseNotesWorkspace[] = [];
112
+
113
+ for (const dir of dirs) {
114
+ const pkg = await readJson<PackageJson>(join(dir, 'package.json'));
115
+ if (
116
+ typeof pkg.name !== 'string' ||
117
+ !pkg.name.startsWith('@ontrails/') ||
118
+ typeof pkg.version !== 'string' ||
119
+ (pkg.private === true && !includePrivate)
120
+ ) {
121
+ continue;
122
+ }
123
+ workspaces.push({
124
+ isPrivate: pkg.private === true,
125
+ name: pkg.name,
126
+ path: dir.slice(repoRoot.length + 1),
127
+ version: pkg.version,
128
+ });
129
+ }
130
+
131
+ return workspaces.toSorted((a, b) => a.name.localeCompare(b.name));
132
+ };
133
+
134
+ const readTrailsVersion = async (repoRoot: string): Promise<string> => {
135
+ const pkg = await readJson<PackageJson>(
136
+ join(repoRoot, 'apps', 'trails', 'package.json')
137
+ );
138
+ if (!pkg.version) {
139
+ throw new Error('Missing version in apps/trails/package.json');
140
+ }
141
+ return pkg.version;
142
+ };
143
+
144
+ const distTagForVersion = (version: string): string =>
145
+ version.includes('-')
146
+ ? (version.split('-')[1]?.split('.')[0] ?? 'beta')
147
+ : 'latest';
148
+
149
+ export const extractChangelogEntry = (
150
+ changelog: string,
151
+ version: string
152
+ ): string | undefined => {
153
+ const lines = changelog.split('\n');
154
+ const heading = `## ${version}`;
155
+ const start = lines.findIndex((line) => line.trim() === heading);
156
+ if (start === -1) {
157
+ return undefined;
158
+ }
159
+
160
+ let end = lines.length;
161
+ for (let index = start + 1; index < lines.length; index += 1) {
162
+ if (/^##\s+\S/u.test(lines[index] ?? '')) {
163
+ end = index;
164
+ break;
165
+ }
166
+ }
167
+
168
+ return lines
169
+ .slice(start + 1, end)
170
+ .join('\n')
171
+ .trim();
172
+ };
173
+
174
+ const normalizeSummary = (lines: readonly string[]): string =>
175
+ lines.join(' ').replaceAll(/\s+/gu, ' ').trim();
176
+
177
+ const parseChangeStart = (
178
+ line: string
179
+ ): Omit<ReleaseNotesParsedChange, 'packageName'> | undefined => {
180
+ if (
181
+ !line.startsWith('- ') ||
182
+ line.startsWith('- Updated dependencies') ||
183
+ line.startsWith('- @ontrails/')
184
+ ) {
185
+ return undefined;
186
+ }
187
+
188
+ const linked = line.match(/^- \[`?([0-9a-f]{7,40})`?\]\(([^)]+)\):\s*(.*)$/u);
189
+ if (linked) {
190
+ return {
191
+ commit: linked[1],
192
+ summary: linked[3] ?? '',
193
+ url: linked[2],
194
+ };
195
+ }
196
+
197
+ const raw = line.match(/^- ([0-9a-f]{7,40}):\s*(.*)$/u);
198
+ if (raw) {
199
+ return {
200
+ commit: raw[1],
201
+ summary: raw[2] ?? '',
202
+ };
203
+ }
204
+
205
+ return { summary: line.slice(2) };
206
+ };
207
+
208
+ const parseChangelogEntryChanges = ({
209
+ changelog,
210
+ packageName,
211
+ version,
212
+ }: ChangelogEntryInput): ReleaseNotesParsedChange[] => {
213
+ const entry = extractChangelogEntry(changelog, version);
214
+ if (!entry) {
215
+ return [];
216
+ }
217
+
218
+ const changes: ReleaseNotesParsedChange[] = [];
219
+ let current: CurrentParsedChange | undefined;
220
+
221
+ const flush = () => {
222
+ if (!current) {
223
+ return;
224
+ }
225
+ const summary = normalizeSummary(current.summaryLines);
226
+ if (summary) {
227
+ changes.push({
228
+ ...(current.commit === undefined ? {} : { commit: current.commit }),
229
+ packageName: current.packageName,
230
+ summary,
231
+ ...(current.url === undefined ? {} : { url: current.url }),
232
+ });
233
+ }
234
+ };
235
+
236
+ for (const line of entry.split('\n')) {
237
+ const start = parseChangeStart(line);
238
+ if (start) {
239
+ flush();
240
+ current = {
241
+ ...(start.commit === undefined ? {} : { commit: start.commit }),
242
+ packageName,
243
+ summaryLines: [start.summary],
244
+ ...(start.url === undefined ? {} : { url: start.url }),
245
+ };
246
+ continue;
247
+ }
248
+
249
+ if (current && line.startsWith(' ') && line.trim()) {
250
+ current.summaryLines.push(line.trim());
251
+ continue;
252
+ }
253
+
254
+ if (line.startsWith('- ')) {
255
+ flush();
256
+ current = undefined;
257
+ }
258
+ }
259
+
260
+ flush();
261
+ return changes;
262
+ };
263
+
264
+ export const dedupeReleaseChanges = (
265
+ changes: readonly ReleaseNotesParsedChange[]
266
+ ): ReleaseNotesChange[] => {
267
+ const byKey = new Map<string, ReleaseNotesChange>();
268
+
269
+ for (const change of changes) {
270
+ const key = `${change.commit ?? ''}\n${change.summary}`;
271
+ const existing = byKey.get(key);
272
+ if (existing) {
273
+ byKey.set(key, {
274
+ ...existing,
275
+ packages: [
276
+ ...new Set([...existing.packages, change.packageName]),
277
+ ].toSorted(),
278
+ });
279
+ continue;
280
+ }
281
+
282
+ byKey.set(key, {
283
+ ...(change.commit === undefined ? {} : { commit: change.commit }),
284
+ packages: [change.packageName],
285
+ summary: change.summary,
286
+ ...(change.url === undefined ? {} : { url: change.url }),
287
+ });
288
+ }
289
+
290
+ return [...byKey.values()];
291
+ };
292
+
293
+ const renderCommitLink = (change: ReleaseNotesChange, repo: string): string => {
294
+ if (!change.commit) {
295
+ return '';
296
+ }
297
+ const short = change.commit.slice(0, 7);
298
+ const url =
299
+ change.url ?? `https://github.com/${repo}/commit/${change.commit}`;
300
+ return `[\`${short}\`](${url}): `;
301
+ };
302
+
303
+ const renderPackages = (packages: readonly string[]): string =>
304
+ packages.map((name) => `\`${name}\``).join(', ');
305
+
306
+ export const renderReleaseNotes = ({
307
+ changes,
308
+ distTag,
309
+ mode,
310
+ packageVersions,
311
+ repo = DEFAULT_REPO,
312
+ version,
313
+ }: ReleaseNotesInput): string => {
314
+ const intro =
315
+ mode === 'release-pr'
316
+ ? `${packageVersions.length} \`@ontrails/*\` packages will be bumped to \`${version}\`.`
317
+ : `Published the publishable \`@ontrails/*\` package set at \`${version}\` on the \`${distTag}\` dist-tag.`;
318
+ const highlights = changes.slice(0, 5);
319
+ const lines = [`# Release ${version}`, '', intro, '', '## Highlights', ''];
320
+
321
+ if (highlights.length === 0) {
322
+ lines.push('- No user-facing changes were detected in package changelogs.');
323
+ } else {
324
+ lines.push(...highlights.map((change) => `- ${change.summary}`));
325
+ }
326
+
327
+ lines.push('', '## Changes', '');
328
+ if (changes.length === 0) {
329
+ lines.push('- No user-facing changes were detected in package changelogs.');
330
+ } else {
331
+ lines.push(
332
+ ...changes.map(
333
+ (change) =>
334
+ `- ${renderCommitLink(change, repo)}${change.summary} Packages: ${renderPackages(change.packages)}`
335
+ )
336
+ );
337
+ }
338
+
339
+ lines.push(
340
+ '',
341
+ '<details>',
342
+ '<summary>Package Versions</summary>',
343
+ '',
344
+ ...packageVersions.map((pkg) => `- \`${pkg.name}@${pkg.version}\``),
345
+ '',
346
+ '</details>',
347
+ ''
348
+ );
349
+
350
+ return lines.join('\n');
351
+ };
352
+
353
+ export const collectReleaseNotesInput = async ({
354
+ distTag,
355
+ mode,
356
+ repo,
357
+ repoRoot,
358
+ version,
359
+ }: ReleaseNotesCollectOptions): Promise<ReleaseNotesInput> => {
360
+ const resolvedVersion = version ?? (await readTrailsVersion(repoRoot));
361
+ const resolvedDistTag = distTag ?? distTagForVersion(resolvedVersion);
362
+ const workspaces = await discoverReleaseNotesWorkspaces(repoRoot, {
363
+ includePrivate: mode === 'release-pr',
364
+ });
365
+ const packageVersions = workspaces.map((workspace) => ({
366
+ name: workspace.name,
367
+ version: workspace.version,
368
+ }));
369
+ const parsedChangesByPackage = await Promise.all(
370
+ workspaces.map(async (workspace: ReleaseNotesWorkspace) =>
371
+ parseChangelogEntryChanges({
372
+ changelog: await Bun.file(
373
+ join(repoRoot, workspace.path, 'CHANGELOG.md')
374
+ ).text(),
375
+ packageName: workspace.name,
376
+ version: resolvedVersion,
377
+ })
378
+ )
379
+ );
380
+ const parsedChanges = parsedChangesByPackage.flat();
381
+
382
+ return {
383
+ changes: dedupeReleaseChanges(parsedChanges),
384
+ distTag: resolvedDistTag,
385
+ mode,
386
+ packageVersions,
387
+ ...(repo === undefined ? {} : { repo }),
388
+ version: resolvedVersion,
389
+ };
390
+ };
@@ -1,3 +1,5 @@
1
+ import { runLockRoundtripSmoke } from './lock-roundtrip-smoke.js';
2
+ import type { LockRoundtripSmokeResult } from './lock-roundtrip-smoke.js';
1
3
  import { runPackedArtifactsSmoke } from './packed-artifacts-smoke.js';
2
4
  import type { PackedArtifactsSmokeResult } from './packed-artifacts-smoke.js';
3
5
  import { runWayfinderDogfoodSmoke } from './wayfinder-dogfood-smoke.js';
@@ -5,12 +7,14 @@ import type { WayfinderDogfoodSmokeResult } from './wayfinder-dogfood-smoke.js';
5
7
 
6
8
  export const releaseSmokeCheckValues = [
7
9
  'all',
10
+ 'lock-roundtrip',
8
11
  'packed-artifacts',
9
12
  'wayfinder-dogfood',
10
13
  ] as const;
11
14
  export type ReleaseSmokeCheck = (typeof releaseSmokeCheckValues)[number];
12
15
 
13
16
  export type ReleaseSmokeCheckResult =
17
+ | LockRoundtripSmokeResult
14
18
  | PackedArtifactsSmokeResult
15
19
  | WayfinderDogfoodSmokeResult;
16
20
 
@@ -23,7 +27,9 @@ export interface ReleaseSmokeResult {
23
27
  const checksForInput = (
24
28
  check: ReleaseSmokeCheck
25
29
  ): readonly Exclude<ReleaseSmokeCheck, 'all'>[] =>
26
- check === 'all' ? ['packed-artifacts', 'wayfinder-dogfood'] : [check];
30
+ check === 'all'
31
+ ? ['lock-roundtrip', 'packed-artifacts', 'wayfinder-dogfood']
32
+ : [check];
27
33
 
28
34
  export const runReleaseSmoke = async (
29
35
  check: ReleaseSmokeCheck
@@ -31,6 +37,10 @@ export const runReleaseSmoke = async (
31
37
  const results: ReleaseSmokeCheckResult[] = [];
32
38
 
33
39
  for (const selectedCheck of checksForInput(check)) {
40
+ if (selectedCheck === 'lock-roundtrip') {
41
+ results.push(await runLockRoundtripSmoke());
42
+ continue;
43
+ }
34
44
  if (selectedCheck === 'packed-artifacts') {
35
45
  results.push(await runPackedArtifactsSmoke());
36
46
  continue;
@@ -138,7 +138,7 @@ const writeOperatorAppWrapper = async (tempRoot: string): Promise<void> => {
138
138
  const appModuleUrl = pathToFileURL(join(repoRoot, 'apps/trails/src/app.ts'));
139
139
  await writeFile(
140
140
  join(srcDir, 'app.ts'),
141
- `export { app, trailsCliAliases } from ${JSON.stringify(appModuleUrl.href)};\n`
141
+ `export { app, trailsOverlays } from ${JSON.stringify(appModuleUrl.href)};\n`
142
142
  );
143
143
  };
144
144
 
package/src/run-schema.ts CHANGED
@@ -14,6 +14,18 @@ const writeJson = (value: unknown): void => {
14
14
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
15
15
  };
16
16
 
17
+ const pathStartsWith = (
18
+ path: readonly string[],
19
+ prefix: readonly string[]
20
+ ): boolean =>
21
+ path.length > prefix.length &&
22
+ prefix.every((segment, index) => path[index] === segment);
23
+
24
+ const childCommandsForPath = (
25
+ schema: ReturnType<typeof deriveCliSchema>,
26
+ path: readonly string[]
27
+ ) => schema.commands.filter((entry) => pathStartsWith(entry.commandPath, path));
28
+
17
29
  export const attachSchemaCommand = (
18
30
  program: Command,
19
31
  commands: readonly CliCommand[]
@@ -30,12 +42,33 @@ export const attachSchemaCommand = (
30
42
  }
31
43
 
32
44
  const command = findCliSchemaCommand(schema, path);
45
+ const children = childCommandsForPath(schema, path);
33
46
  if (command === undefined) {
47
+ if (children.length > 0) {
48
+ writeJson({
49
+ namespace: {
50
+ commandPath: path,
51
+ commands: children,
52
+ },
53
+ });
54
+ return;
55
+ }
56
+
34
57
  process.stderr.write(`Unknown CLI command: ${path.join(' ')}\n`);
35
58
  process.exitCode = 1;
36
59
  return;
37
60
  }
38
61
 
39
- writeJson({ command });
62
+ writeJson(
63
+ children.length === 0
64
+ ? { command }
65
+ : {
66
+ command,
67
+ namespace: {
68
+ commandPath: path,
69
+ commands: children,
70
+ },
71
+ }
72
+ );
40
73
  });
41
74
  };
@@ -1,5 +1,6 @@
1
1
  import { trail } from '@ontrails/core';
2
- import type { CliCommandAliasInput, Result, Topo } from '@ontrails/core';
2
+ import type { Result, Topo } from '@ontrails/core';
3
+ import type { TopoGraphOverlayRegistration } from '@ontrails/topographer';
3
4
  import { z } from 'zod';
4
5
 
5
6
  import { withFreshOperatorApp } from './operator-context.js';
@@ -13,11 +14,9 @@ import {
13
14
  export const compileCurrentTopo = async (
14
15
  app: Topo,
15
16
  options?: {
16
- readonly cliAliases?:
17
- | Readonly<Record<string, readonly CliCommandAliasInput[]>>
18
- | undefined;
19
17
  readonly force?: boolean | undefined;
20
18
  readonly rootDir?: string;
19
+ readonly overlays?: readonly TopoGraphOverlayRegistration[] | undefined;
21
20
  }
22
21
  ): Promise<Result<TopoExportReport, Error>> => exportCurrentTopo(app, options);
23
22
 
@@ -36,8 +35,8 @@ export const compileTrail = trail('compile', {
36
35
  blaze: async (input: CompileTrailInput, ctx) =>
37
36
  withFreshOperatorApp(input, ctx, ({ lease, rootDir }) =>
38
37
  compileCurrentTopo(lease.app, {
39
- cliAliases: lease.cliAliases,
40
38
  force: input.force,
39
+ overlays: lease.overlays,
41
40
  rootDir,
42
41
  })
43
42
  ),
@@ -54,7 +54,7 @@ export const guideTrail = trail('guide', {
54
54
  withFreshOperatorApp<GuideTrailOutput>(input, ctx, ({ lease, rootDir }) => {
55
55
  if (input.trailId) {
56
56
  const detail = deriveCurrentTopoDetail(lease.app, input.trailId, {
57
- cliAliases: lease.cliAliases,
57
+ overlays: lease.overlays,
58
58
  rootDir,
59
59
  surfaceLayerNames: readSurfaceLayerNamesFromContext(ctx),
60
60
  });
@@ -24,7 +24,9 @@ import {
24
24
  Result,
25
25
  ValidationError,
26
26
  } from '@ontrails/core';
27
- import type { CliCommandAliasInput, Topo } from '@ontrails/core';
27
+ import type { Topo } from '@ontrails/core';
28
+ import { resolveTrailsOverlays } from '@ontrails/adapter-kit';
29
+ import type { TopoGraphOverlayRegistration } from '@ontrails/topographer';
28
30
  import { findAppModule } from '@ontrails/cli';
29
31
 
30
32
  import {
@@ -356,10 +358,33 @@ const resolveImportedModulePath = (
356
358
  importPath,
357
359
  pathToFileURL(importerPath).href
358
360
  );
359
- return resolveFilesystemModulePath(
361
+ const literalPath = resolveFilesystemModulePath(
360
362
  fileURLToPath(resolved),
361
363
  dirname(importerPath)
362
364
  );
365
+ if (safeStat(literalPath)?.isFile()) {
366
+ return literalPath;
367
+ }
368
+
369
+ // `import.meta.resolve` joins relative specifiers without probing the
370
+ // filesystem, so extensionless imports point at paths that do not exist even
371
+ // though Bun resolves them at runtime. Fall back to the runtime resolver so
372
+ // runtime-valid apps stay fresh-loadable.
373
+ try {
374
+ return resolveFilesystemModulePath(
375
+ Bun.resolveSync(importPath, dirname(importerPath)),
376
+ dirname(importerPath)
377
+ );
378
+ } catch (error) {
379
+ const extensionlessHint =
380
+ extname(importPath) === ''
381
+ ? ' Extensionless relative imports must match a .ts/.tsx/.js module or a directory index.'
382
+ : '';
383
+ throw new ValidationError(
384
+ `Cannot resolve import "${importPath}" from "${importerPath}" while mirroring the app for fresh loading: "${literalPath}" does not exist and runtime resolution found no match. Fix the specifier or add the missing file.${extensionlessHint}`,
385
+ { cause: asError(error), context: { importPath, importerPath } }
386
+ );
387
+ }
363
388
  };
364
389
 
365
390
  interface WorkspacePackage {
@@ -1003,54 +1028,11 @@ const resolveLoadedTopo = (
1003
1028
  return app;
1004
1029
  };
1005
1030
 
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
1031
  export interface FreshAppLease {
1050
1032
  readonly app: Topo;
1051
- readonly cliAliases?: LoadedCliAliases | undefined;
1052
1033
  readonly mirrorRoot: string;
1053
1034
  readonly release: () => void;
1035
+ readonly overlays?: readonly TopoGraphOverlayRegistration[] | undefined;
1054
1036
  }
1055
1037
 
1056
1038
  export type LoadAppLeaseOptions = LoadAppTrustOptions;
@@ -1071,8 +1053,8 @@ const createUrlSchemeLease = async (
1071
1053
  >;
1072
1054
  return {
1073
1055
  app: resolveLoadedTopo(effectivePath, mod),
1074
- cliAliases: resolveLoadedCliAliases(effectivePath, mod),
1075
1056
  mirrorRoot: absolutePath,
1057
+ overlays: resolveTrailsOverlays(mod, effectivePath),
1076
1058
  release: noopRelease,
1077
1059
  };
1078
1060
  };
@@ -1093,8 +1075,8 @@ const createFilesystemLease = async (
1093
1075
 
1094
1076
  return {
1095
1077
  app: resolveLoadedTopo(effectivePath, mod),
1096
- cliAliases: resolveLoadedCliAliases(effectivePath, mod),
1097
1078
  mirrorRoot,
1079
+ overlays: resolveTrailsOverlays(mod, effectivePath),
1098
1080
  release,
1099
1081
  };
1100
1082
  } catch (error) {