@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.
- package/CHANGELOG.md +208 -0
- package/README.md +27 -0
- package/package.json +19 -8
- package/src/app.ts +17 -7
- package/src/clack.ts +1 -1
- package/src/cli.ts +304 -10
- package/src/completions.ts +240 -0
- package/src/load-app-mirror.ts +160 -0
- package/src/local-state-io.ts +153 -0
- package/src/project-writes.ts +320 -0
- package/src/run-collision.ts +125 -0
- package/src/run-completions-install.ts +179 -0
- package/src/run-example.ts +149 -0
- package/src/run-examples.ts +148 -0
- package/src/run-quiet.ts +75 -0
- package/src/run-trace.ts +273 -0
- package/src/run-warden.ts +39 -0
- package/src/run-watch.ts +432 -0
- package/src/scaffold-versions.generated.ts +12 -0
- package/src/trails/add-surface.ts +172 -0
- package/src/trails/add-trail.ts +73 -27
- package/src/trails/add-verify.ts +68 -23
- package/src/trails/completions-complete.ts +165 -0
- package/src/trails/completions.ts +47 -0
- package/src/trails/create-scaffold.ts +101 -35
- package/src/trails/create.ts +87 -74
- package/src/trails/dev-clean.ts +31 -22
- package/src/trails/dev-reset.ts +9 -3
- package/src/trails/dev-stats.ts +28 -20
- package/src/trails/dev-support.ts +109 -95
- package/src/trails/draft-promote.ts +351 -107
- package/src/trails/guide.ts +55 -38
- package/src/trails/load-app.ts +712 -38
- package/src/trails/root-dir.ts +21 -0
- package/src/trails/run-example.ts +482 -0
- package/src/trails/run-examples.ts +141 -0
- package/src/trails/run.ts +403 -0
- package/src/trails/survey.ts +517 -186
- package/src/trails/topo-activation.ts +385 -0
- package/src/trails/topo-compile.ts +55 -0
- package/src/trails/topo-history.ts +14 -11
- package/src/trails/topo-output-schemas.ts +175 -0
- package/src/trails/topo-pin.ts +25 -16
- package/src/trails/topo-read-support.ts +178 -238
- package/src/trails/topo-reports.ts +445 -63
- package/src/trails/topo-store-support.ts +67 -35
- package/src/trails/topo-support.ts +93 -147
- package/src/trails/topo-unpin.ts +17 -7
- package/src/trails/topo-verify.ts +19 -10
- package/src/trails/topo.ts +64 -31
- package/src/trails/warden-guide.ts +121 -0
- package/src/trails/warden.ts +137 -47
- package/src/versions.ts +28 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/__tests__/examples.test.ts +0 -20
- package/dist/bin/trails.d.ts +0 -3
- package/dist/bin/trails.d.ts.map +0 -1
- package/dist/bin/trails.js +0 -4
- package/dist/bin/trails.js.map +0 -1
- package/dist/src/app.d.ts +0 -2
- package/dist/src/app.d.ts.map +0 -1
- package/dist/src/app.js +0 -22
- package/dist/src/app.js.map +0 -1
- package/dist/src/clack.d.ts +0 -9
- package/dist/src/clack.d.ts.map +0 -1
- package/dist/src/clack.js +0 -84
- package/dist/src/clack.js.map +0 -1
- package/dist/src/cli.d.ts +0 -2
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js +0 -13
- package/dist/src/cli.js.map +0 -1
- package/dist/src/trails/add-surface.d.ts +0 -13
- package/dist/src/trails/add-surface.d.ts.map +0 -1
- package/dist/src/trails/add-surface.js +0 -88
- package/dist/src/trails/add-surface.js.map +0 -1
- package/dist/src/trails/add-trail.d.ts +0 -10
- package/dist/src/trails/add-trail.d.ts.map +0 -1
- package/dist/src/trails/add-trail.js +0 -77
- package/dist/src/trails/add-trail.js.map +0 -1
- package/dist/src/trails/add-trailhead.d.ts +0 -13
- package/dist/src/trails/add-trailhead.d.ts.map +0 -1
- package/dist/src/trails/add-trailhead.js +0 -88
- package/dist/src/trails/add-trailhead.js.map +0 -1
- package/dist/src/trails/add-verify.d.ts +0 -10
- package/dist/src/trails/add-verify.d.ts.map +0 -1
- package/dist/src/trails/add-verify.js +0 -67
- package/dist/src/trails/add-verify.js.map +0 -1
- package/dist/src/trails/create-scaffold.d.ts +0 -15
- package/dist/src/trails/create-scaffold.d.ts.map +0 -1
- package/dist/src/trails/create-scaffold.js +0 -288
- package/dist/src/trails/create-scaffold.js.map +0 -1
- package/dist/src/trails/create.d.ts +0 -22
- package/dist/src/trails/create.d.ts.map +0 -1
- package/dist/src/trails/create.js +0 -121
- package/dist/src/trails/create.js.map +0 -1
- package/dist/src/trails/dev-clean.d.ts +0 -9
- package/dist/src/trails/dev-clean.d.ts.map +0 -1
- package/dist/src/trails/dev-clean.js +0 -65
- package/dist/src/trails/dev-clean.js.map +0 -1
- package/dist/src/trails/dev-reset.d.ts +0 -6
- package/dist/src/trails/dev-reset.d.ts.map +0 -1
- package/dist/src/trails/dev-reset.js +0 -38
- package/dist/src/trails/dev-reset.js.map +0 -1
- package/dist/src/trails/dev-stats.d.ts +0 -7
- package/dist/src/trails/dev-stats.d.ts.map +0 -1
- package/dist/src/trails/dev-stats.js +0 -61
- package/dist/src/trails/dev-stats.js.map +0 -1
- package/dist/src/trails/dev-support.d.ts +0 -64
- package/dist/src/trails/dev-support.d.ts.map +0 -1
- package/dist/src/trails/dev-support.js +0 -178
- package/dist/src/trails/dev-support.js.map +0 -1
- package/dist/src/trails/draft-promote.d.ts +0 -18
- package/dist/src/trails/draft-promote.d.ts.map +0 -1
- package/dist/src/trails/draft-promote.js +0 -386
- package/dist/src/trails/draft-promote.js.map +0 -1
- package/dist/src/trails/guide.d.ts +0 -21
- package/dist/src/trails/guide.d.ts.map +0 -1
- package/dist/src/trails/guide.js +0 -64
- package/dist/src/trails/guide.js.map +0 -1
- package/dist/src/trails/load-app.d.ts +0 -6
- package/dist/src/trails/load-app.d.ts.map +0 -1
- package/dist/src/trails/load-app.js +0 -67
- package/dist/src/trails/load-app.js.map +0 -1
- package/dist/src/trails/project.d.ts +0 -8
- package/dist/src/trails/project.d.ts.map +0 -1
- package/dist/src/trails/project.js +0 -54
- package/dist/src/trails/project.js.map +0 -1
- package/dist/src/trails/survey.d.ts +0 -18
- package/dist/src/trails/survey.d.ts.map +0 -1
- package/dist/src/trails/survey.js +0 -212
- package/dist/src/trails/survey.js.map +0 -1
- package/dist/src/trails/topo-constants.d.ts +0 -3
- package/dist/src/trails/topo-constants.d.ts.map +0 -1
- package/dist/src/trails/topo-constants.js +0 -3
- package/dist/src/trails/topo-constants.js.map +0 -1
- package/dist/src/trails/topo-export.d.ts +0 -18
- package/dist/src/trails/topo-export.d.ts.map +0 -1
- package/dist/src/trails/topo-export.js +0 -34
- package/dist/src/trails/topo-export.js.map +0 -1
- package/dist/src/trails/topo-history.d.ts +0 -24
- package/dist/src/trails/topo-history.d.ts.map +0 -1
- package/dist/src/trails/topo-history.js +0 -33
- package/dist/src/trails/topo-history.js.map +0 -1
- package/dist/src/trails/topo-pin.d.ts +0 -21
- package/dist/src/trails/topo-pin.d.ts.map +0 -1
- package/dist/src/trails/topo-pin.js +0 -35
- package/dist/src/trails/topo-pin.js.map +0 -1
- package/dist/src/trails/topo-read-support.d.ts +0 -54
- package/dist/src/trails/topo-read-support.d.ts.map +0 -1
- package/dist/src/trails/topo-read-support.js +0 -178
- package/dist/src/trails/topo-read-support.js.map +0 -1
- package/dist/src/trails/topo-reports.d.ts +0 -50
- package/dist/src/trails/topo-reports.d.ts.map +0 -1
- package/dist/src/trails/topo-reports.js +0 -122
- package/dist/src/trails/topo-reports.js.map +0 -1
- package/dist/src/trails/topo-show.d.ts +0 -23
- package/dist/src/trails/topo-show.d.ts.map +0 -1
- package/dist/src/trails/topo-show.js +0 -53
- package/dist/src/trails/topo-show.js.map +0 -1
- package/dist/src/trails/topo-store-support.d.ts +0 -13
- package/dist/src/trails/topo-store-support.d.ts.map +0 -1
- package/dist/src/trails/topo-store-support.js +0 -55
- package/dist/src/trails/topo-store-support.js.map +0 -1
- package/dist/src/trails/topo-support.d.ts +0 -87
- package/dist/src/trails/topo-support.d.ts.map +0 -1
- package/dist/src/trails/topo-support.js +0 -165
- package/dist/src/trails/topo-support.js.map +0 -1
- package/dist/src/trails/topo-unpin.d.ts +0 -15
- package/dist/src/trails/topo-unpin.d.ts.map +0 -1
- package/dist/src/trails/topo-unpin.js +0 -39
- package/dist/src/trails/topo-unpin.js.map +0 -1
- package/dist/src/trails/topo-verify.d.ts +0 -5
- package/dist/src/trails/topo-verify.d.ts.map +0 -1
- package/dist/src/trails/topo-verify.js +0 -28
- package/dist/src/trails/topo-verify.js.map +0 -1
- package/dist/src/trails/topo.d.ts +0 -5
- package/dist/src/trails/topo.d.ts.map +0 -1
- package/dist/src/trails/topo.js +0 -67
- package/dist/src/trails/topo.js.map +0 -1
- package/dist/src/trails/warden.d.ts +0 -19
- package/dist/src/trails/warden.d.ts.map +0 -1
- package/dist/src/trails/warden.js +0 -89
- package/dist/src/trails/warden.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/__tests__/create.test.ts +0 -351
- package/src/__tests__/draft-promote.test.ts +0 -144
- package/src/__tests__/guide.test.ts +0 -91
- package/src/__tests__/load-app.test.ts +0 -58
- package/src/__tests__/survey.test.ts +0 -301
- package/src/__tests__/topo-dev.test.ts +0 -424
- package/src/__tests__/warden.test.ts +0 -74
- package/src/trails/add-trailhead.ts +0 -121
- package/src/trails/topo-export.ts +0 -39
- package/src/trails/topo-show.ts +0 -58
- package/tsconfig.json +0 -9
|
@@ -1,24 +1,41 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import {
|
|
3
|
+
basename,
|
|
4
|
+
dirname,
|
|
5
|
+
isAbsolute,
|
|
6
|
+
join,
|
|
7
|
+
relative,
|
|
8
|
+
resolve,
|
|
9
|
+
} from 'node:path';
|
|
3
10
|
|
|
4
11
|
import {
|
|
5
12
|
Result,
|
|
6
13
|
ValidationError,
|
|
7
|
-
|
|
14
|
+
deriveDraftReport,
|
|
8
15
|
isDraftId,
|
|
9
16
|
trail,
|
|
10
17
|
} from '@ontrails/core';
|
|
18
|
+
import type { Topo } from '@ontrails/core';
|
|
11
19
|
import {
|
|
12
20
|
DRAFT_FILE_PREFIX,
|
|
13
|
-
findStringLiterals,
|
|
14
21
|
isDraftMarkedFile,
|
|
15
|
-
parse,
|
|
16
22
|
stripDraftFileMarkers,
|
|
17
23
|
} from '@ontrails/warden';
|
|
24
|
+
import { findStringLiterals, parse } from '@ontrails/warden/ast';
|
|
18
25
|
import { z } from 'zod';
|
|
19
26
|
|
|
20
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
applyProjectOperations,
|
|
29
|
+
planProjectOperations,
|
|
30
|
+
} from '../project-writes.js';
|
|
31
|
+
import type {
|
|
32
|
+
PlannedProjectOperation,
|
|
33
|
+
ProjectWriteOperation,
|
|
34
|
+
} from '../project-writes.js';
|
|
35
|
+
import type { FreshAppLease } from './load-app.js';
|
|
36
|
+
import { tryLoadFreshAppLease } from './load-app.js';
|
|
21
37
|
import { findTopoPath } from './project.js';
|
|
38
|
+
import { resolveTrailRootDir } from './root-dir.js';
|
|
22
39
|
|
|
23
40
|
interface PromotionEdit {
|
|
24
41
|
readonly end: number;
|
|
@@ -164,10 +181,8 @@ const replaceLiteralValue = (
|
|
|
164
181
|
};
|
|
165
182
|
};
|
|
166
183
|
|
|
167
|
-
const collectOutputId = (
|
|
168
|
-
app
|
|
169
|
-
id: string
|
|
170
|
-
) => app.get(id) ?? app.signals.get(id) ?? app.getProvision(id);
|
|
184
|
+
const collectOutputId = (app: Topo, id: string) =>
|
|
185
|
+
app.get(id) ?? app.signals.get(id) ?? app.getResource(id);
|
|
171
186
|
|
|
172
187
|
const toRelativeOutputPath = (rootDir: string, filePath: string): string =>
|
|
173
188
|
relative(rootDir, filePath).replaceAll('\\', '/');
|
|
@@ -178,6 +193,7 @@ const toProjectModulePath = (sourceImport: string): string =>
|
|
|
178
193
|
: sourceImport;
|
|
179
194
|
|
|
180
195
|
interface PromotionRewriteState {
|
|
196
|
+
readonly plannedOperations: PlannedProjectOperation[];
|
|
181
197
|
readonly renames: FileRename[];
|
|
182
198
|
readonly updatedSourceFiles: Set<string>;
|
|
183
199
|
}
|
|
@@ -185,7 +201,6 @@ interface PromotionRewriteState {
|
|
|
185
201
|
interface PromotionLoadState {
|
|
186
202
|
readonly appModule: string | null;
|
|
187
203
|
readonly loadError: string | null;
|
|
188
|
-
readonly loadedApp: Awaited<ReturnType<typeof loadApp>> | undefined;
|
|
189
204
|
}
|
|
190
205
|
|
|
191
206
|
const validatePromotionInput = (input: {
|
|
@@ -242,7 +257,22 @@ const resolveValidatedPromotionRoot = (
|
|
|
242
257
|
return validation;
|
|
243
258
|
}
|
|
244
259
|
|
|
245
|
-
|
|
260
|
+
let rootDir: string;
|
|
261
|
+
if (input.rootDir === undefined) {
|
|
262
|
+
const cwdResult = resolveTrailRootDir(undefined, ctx.cwd);
|
|
263
|
+
if (cwdResult.isErr()) {
|
|
264
|
+
return cwdResult;
|
|
265
|
+
}
|
|
266
|
+
rootDir = resolve(cwdResult.value);
|
|
267
|
+
} else if (isAbsolute(input.rootDir)) {
|
|
268
|
+
rootDir = resolve(input.rootDir);
|
|
269
|
+
} else {
|
|
270
|
+
const cwdResult = resolveTrailRootDir(undefined, ctx.cwd);
|
|
271
|
+
if (cwdResult.isErr()) {
|
|
272
|
+
return cwdResult;
|
|
273
|
+
}
|
|
274
|
+
rootDir = resolve(cwdResult.value, input.rootDir);
|
|
275
|
+
}
|
|
246
276
|
const rootValidation = validatePromotionRoot(rootDir);
|
|
247
277
|
if (rootValidation.isErr()) {
|
|
248
278
|
return rootValidation;
|
|
@@ -251,27 +281,83 @@ const resolveValidatedPromotionRoot = (
|
|
|
251
281
|
return Result.ok(rootDir);
|
|
252
282
|
};
|
|
253
283
|
|
|
254
|
-
|
|
284
|
+
type SourceFileMap = Map<string, string>;
|
|
285
|
+
type WriteProjectOperation = Extract<
|
|
286
|
+
ProjectWriteOperation,
|
|
287
|
+
{ readonly kind: 'write' }
|
|
288
|
+
>;
|
|
289
|
+
|
|
290
|
+
const pushWriteOperation = (
|
|
291
|
+
operations: ProjectWriteOperation[],
|
|
292
|
+
operation: WriteProjectOperation
|
|
293
|
+
): void => {
|
|
294
|
+
for (let index = operations.length - 1; index >= 0; index -= 1) {
|
|
295
|
+
const existing = operations[index];
|
|
296
|
+
if (
|
|
297
|
+
existing?.kind === 'rename' &&
|
|
298
|
+
(existing.from === operation.path || existing.to === operation.path)
|
|
299
|
+
) {
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
if (existing?.kind === 'write' && existing.path === operation.path) {
|
|
303
|
+
operations[index] = operation;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
operations.push(operation);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const readSourceFiles = async (
|
|
312
|
+
filePaths: readonly string[]
|
|
313
|
+
): Promise<Result<SourceFileMap, Error>> => {
|
|
314
|
+
const sources = new Map<string, string>();
|
|
315
|
+
for (const filePath of filePaths) {
|
|
316
|
+
try {
|
|
317
|
+
sources.set(filePath, await Bun.file(filePath).text());
|
|
318
|
+
} catch (error) {
|
|
319
|
+
return Result.err(
|
|
320
|
+
new ValidationError(
|
|
321
|
+
`Cannot read source file "${filePath}"`,
|
|
322
|
+
error instanceof Error ? { cause: error } : undefined
|
|
323
|
+
)
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return Result.ok(sources);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const planPromotedSourceFiles = (
|
|
255
331
|
filePaths: readonly string[],
|
|
332
|
+
sources: SourceFileMap,
|
|
256
333
|
fromId: string,
|
|
257
334
|
toId: string,
|
|
258
|
-
updatedSourceFiles: Set<string
|
|
259
|
-
|
|
335
|
+
updatedSourceFiles: Set<string>,
|
|
336
|
+
operations: ProjectWriteOperation[]
|
|
337
|
+
): Result<void, Error> => {
|
|
260
338
|
for (const filePath of filePaths) {
|
|
261
|
-
const sourceCode =
|
|
339
|
+
const sourceCode = sources.get(filePath);
|
|
340
|
+
if (sourceCode === undefined) {
|
|
341
|
+
return Result.err(
|
|
342
|
+
new ValidationError(`Cannot read source file "${filePath}"`)
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
262
346
|
const replaced = replaceIdLiterals(sourceCode, filePath, fromId, toId);
|
|
263
347
|
if (!replaced.changed) {
|
|
264
348
|
continue;
|
|
265
349
|
}
|
|
266
350
|
|
|
267
|
-
|
|
351
|
+
sources.set(filePath, replaced.nextSource);
|
|
352
|
+
pushWriteOperation(operations, {
|
|
353
|
+
content: replaced.nextSource,
|
|
354
|
+
kind: 'write',
|
|
355
|
+
path: filePath,
|
|
356
|
+
});
|
|
268
357
|
updatedSourceFiles.add(filePath);
|
|
269
358
|
}
|
|
270
|
-
};
|
|
271
359
|
|
|
272
|
-
|
|
273
|
-
const sourceCode = await Bun.file(filePath).text();
|
|
274
|
-
return hasDraftIds(sourceCode, filePath);
|
|
360
|
+
return Result.ok();
|
|
275
361
|
};
|
|
276
362
|
|
|
277
363
|
const buildPromotableFileRename = (
|
|
@@ -295,9 +381,10 @@ const buildPromotableFileRename = (
|
|
|
295
381
|
return Result.ok({ from: filePath, to: nextPath });
|
|
296
382
|
};
|
|
297
383
|
|
|
298
|
-
const collectPromotableFileRename =
|
|
299
|
-
filePath: string
|
|
300
|
-
|
|
384
|
+
const collectPromotableFileRename = (
|
|
385
|
+
filePath: string,
|
|
386
|
+
sourceCode: string
|
|
387
|
+
): Result<FileRename | null, Error> => {
|
|
301
388
|
if (!isDraftMarkedFile(filePath)) {
|
|
302
389
|
return Result.ok(null);
|
|
303
390
|
}
|
|
@@ -307,7 +394,7 @@ const collectPromotableFileRename = async (
|
|
|
307
394
|
return Result.ok(null);
|
|
308
395
|
}
|
|
309
396
|
|
|
310
|
-
if (
|
|
397
|
+
if (hasDraftIds(sourceCode, filePath)) {
|
|
311
398
|
return Result.ok(null);
|
|
312
399
|
}
|
|
313
400
|
|
|
@@ -339,12 +426,19 @@ const validateRenameTargets = (
|
|
|
339
426
|
return Result.ok();
|
|
340
427
|
};
|
|
341
428
|
|
|
342
|
-
const collectFileRenames =
|
|
343
|
-
filePaths: readonly string[]
|
|
344
|
-
|
|
429
|
+
const collectFileRenames = (
|
|
430
|
+
filePaths: readonly string[],
|
|
431
|
+
sources: SourceFileMap
|
|
432
|
+
): Result<FileRename[], Error> => {
|
|
345
433
|
const renames: FileRename[] = [];
|
|
346
434
|
for (const filePath of filePaths) {
|
|
347
|
-
const
|
|
435
|
+
const sourceCode = sources.get(filePath);
|
|
436
|
+
if (sourceCode === undefined) {
|
|
437
|
+
return Result.err(
|
|
438
|
+
new ValidationError(`Cannot read source file "${filePath}"`)
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
const renameResult = collectPromotableFileRename(filePath, sourceCode);
|
|
348
442
|
if (renameResult.isErr()) {
|
|
349
443
|
return renameResult;
|
|
350
444
|
}
|
|
@@ -355,26 +449,6 @@ const collectFileRenames = async (
|
|
|
355
449
|
return Result.ok(renames);
|
|
356
450
|
};
|
|
357
451
|
|
|
358
|
-
const collectAndApplyFileRenames = async (
|
|
359
|
-
filePaths: readonly string[]
|
|
360
|
-
): Promise<Result<FileRename[], Error>> => {
|
|
361
|
-
const collected = await collectFileRenames(filePaths);
|
|
362
|
-
if (collected.isErr()) {
|
|
363
|
-
return collected;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const renames = collected.value;
|
|
367
|
-
const valid = validateRenameTargets(renames);
|
|
368
|
-
if (valid.isErr()) {
|
|
369
|
-
return valid;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
for (const r of renames) {
|
|
373
|
-
renameSync(r.from, r.to);
|
|
374
|
-
}
|
|
375
|
-
return Result.ok(renames);
|
|
376
|
-
};
|
|
377
|
-
|
|
378
452
|
const applyRenameEffects = (
|
|
379
453
|
updatedSourceFiles: Set<string>,
|
|
380
454
|
renames: readonly FileRename[]
|
|
@@ -386,6 +460,30 @@ const applyRenameEffects = (
|
|
|
386
460
|
}
|
|
387
461
|
};
|
|
388
462
|
|
|
463
|
+
const applySourceRenameEffects = (
|
|
464
|
+
sources: SourceFileMap,
|
|
465
|
+
renames: readonly FileRename[]
|
|
466
|
+
): void => {
|
|
467
|
+
for (const rename of renames) {
|
|
468
|
+
const sourceCode = sources.get(rename.from);
|
|
469
|
+
if (sourceCode === undefined) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
sources.delete(rename.from);
|
|
473
|
+
sources.set(rename.to, sourceCode);
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const applyFilePathRenames = (
|
|
478
|
+
filePaths: readonly string[],
|
|
479
|
+
renames: readonly FileRename[]
|
|
480
|
+
): string[] => {
|
|
481
|
+
const renamedBySource = new Map(
|
|
482
|
+
renames.map((rename) => [rename.from, rename.to])
|
|
483
|
+
);
|
|
484
|
+
return filePaths.map((filePath) => renamedBySource.get(filePath) ?? filePath);
|
|
485
|
+
};
|
|
486
|
+
|
|
389
487
|
const applyRelativeImportRename = (
|
|
390
488
|
sourceCode: string,
|
|
391
489
|
filePath: string,
|
|
@@ -431,69 +529,128 @@ const rewriteRelativeImportsForFile = (
|
|
|
431
529
|
return { changed, sourceCode: nextSourceCode };
|
|
432
530
|
};
|
|
433
531
|
|
|
434
|
-
const
|
|
532
|
+
const planRelativeImportsForFile = (
|
|
435
533
|
filePath: string,
|
|
436
|
-
renames: readonly FileRename[]
|
|
437
|
-
|
|
438
|
-
|
|
534
|
+
renames: readonly FileRename[],
|
|
535
|
+
sources: SourceFileMap,
|
|
536
|
+
operations: ProjectWriteOperation[]
|
|
537
|
+
): Result<boolean, Error> => {
|
|
538
|
+
const sourceCode = sources.get(filePath);
|
|
539
|
+
if (sourceCode === undefined) {
|
|
540
|
+
return Result.err(
|
|
541
|
+
new ValidationError(`Cannot read source file "${filePath}"`)
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
439
545
|
const updated = rewriteRelativeImportsForFile(filePath, renames, sourceCode);
|
|
440
546
|
if (updated.changed) {
|
|
441
|
-
|
|
442
|
-
|
|
547
|
+
sources.set(filePath, updated.sourceCode);
|
|
548
|
+
pushWriteOperation(operations, {
|
|
549
|
+
content: updated.sourceCode,
|
|
550
|
+
kind: 'write',
|
|
551
|
+
path: filePath,
|
|
552
|
+
});
|
|
553
|
+
return Result.ok(true);
|
|
443
554
|
}
|
|
444
555
|
|
|
445
|
-
return false;
|
|
556
|
+
return Result.ok(false);
|
|
446
557
|
};
|
|
447
558
|
|
|
448
|
-
const
|
|
559
|
+
const planRelativeImports = (
|
|
449
560
|
filePaths: readonly string[],
|
|
450
|
-
renames: readonly FileRename[]
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
561
|
+
renames: readonly FileRename[],
|
|
562
|
+
sources: SourceFileMap,
|
|
563
|
+
updatedSourceFiles: Set<string>,
|
|
564
|
+
operations: ProjectWriteOperation[]
|
|
565
|
+
): Result<void, Error> => {
|
|
454
566
|
for (const filePath of filePaths) {
|
|
455
|
-
const changed =
|
|
456
|
-
|
|
457
|
-
|
|
567
|
+
const changed = planRelativeImportsForFile(
|
|
568
|
+
filePath,
|
|
569
|
+
renames,
|
|
570
|
+
sources,
|
|
571
|
+
operations
|
|
572
|
+
);
|
|
573
|
+
if (changed.isErr()) {
|
|
574
|
+
return Result.err(changed.error);
|
|
575
|
+
}
|
|
576
|
+
if (changed.value) {
|
|
577
|
+
updatedSourceFiles.add(filePath);
|
|
458
578
|
}
|
|
459
579
|
}
|
|
460
580
|
|
|
461
|
-
return
|
|
581
|
+
return Result.ok();
|
|
462
582
|
};
|
|
463
583
|
|
|
464
584
|
const rewritePromotionState = async (
|
|
465
585
|
rootDir: string,
|
|
466
586
|
input: {
|
|
587
|
+
readonly dryRun?: boolean | undefined;
|
|
467
588
|
readonly fromId: string;
|
|
468
589
|
readonly renameFiles: boolean;
|
|
469
590
|
readonly toId: string;
|
|
470
591
|
}
|
|
471
592
|
): Promise<Result<PromotionRewriteState, Error>> => {
|
|
472
593
|
const initialFiles = collectTsFiles(rootDir);
|
|
594
|
+
const sourcesResult = await readSourceFiles(initialFiles);
|
|
595
|
+
if (sourcesResult.isErr()) {
|
|
596
|
+
return Result.err(sourcesResult.error);
|
|
597
|
+
}
|
|
598
|
+
const sources = sourcesResult.value;
|
|
599
|
+
const operations: ProjectWriteOperation[] = [];
|
|
473
600
|
const updatedSourceFiles = new Set<string>();
|
|
474
601
|
|
|
475
|
-
|
|
602
|
+
const rewritten = planPromotedSourceFiles(
|
|
476
603
|
initialFiles,
|
|
604
|
+
sources,
|
|
477
605
|
input.fromId,
|
|
478
606
|
input.toId,
|
|
479
|
-
updatedSourceFiles
|
|
607
|
+
updatedSourceFiles,
|
|
608
|
+
operations
|
|
480
609
|
);
|
|
610
|
+
if (rewritten.isErr()) {
|
|
611
|
+
return Result.err(rewritten.error);
|
|
612
|
+
}
|
|
481
613
|
|
|
482
614
|
const renamesResult = input.renameFiles
|
|
483
|
-
?
|
|
615
|
+
? collectFileRenames(initialFiles, sources)
|
|
484
616
|
: Result.ok([] as FileRename[]);
|
|
485
617
|
if (renamesResult.isErr()) {
|
|
486
618
|
return Result.err(renamesResult.error);
|
|
487
619
|
}
|
|
488
620
|
|
|
621
|
+
const valid = validateRenameTargets(renamesResult.value);
|
|
622
|
+
if (valid.isErr()) {
|
|
623
|
+
return valid;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
for (const rename of renamesResult.value) {
|
|
627
|
+
operations.push({ from: rename.from, kind: 'rename', to: rename.to });
|
|
628
|
+
}
|
|
629
|
+
|
|
489
630
|
applyRenameEffects(updatedSourceFiles, renamesResult.value);
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
renamesResult.value
|
|
493
|
-
|
|
494
|
-
|
|
631
|
+
applySourceRenameEffects(sources, renamesResult.value);
|
|
632
|
+
const importUpdates = planRelativeImports(
|
|
633
|
+
applyFilePathRenames(initialFiles, renamesResult.value),
|
|
634
|
+
renamesResult.value,
|
|
635
|
+
sources,
|
|
636
|
+
updatedSourceFiles,
|
|
637
|
+
operations
|
|
638
|
+
);
|
|
639
|
+
if (importUpdates.isErr()) {
|
|
640
|
+
return Result.err(importUpdates.error);
|
|
495
641
|
}
|
|
496
|
-
|
|
642
|
+
|
|
643
|
+
const plannedOperations = input.dryRun
|
|
644
|
+
? planProjectOperations(rootDir, operations)
|
|
645
|
+
: await applyProjectOperations(rootDir, operations);
|
|
646
|
+
if (plannedOperations.isErr()) {
|
|
647
|
+
return Result.err(plannedOperations.error);
|
|
648
|
+
}
|
|
649
|
+
return Result.ok({
|
|
650
|
+
plannedOperations: plannedOperations.value,
|
|
651
|
+
renames: renamesResult.value,
|
|
652
|
+
updatedSourceFiles,
|
|
653
|
+
});
|
|
497
654
|
};
|
|
498
655
|
|
|
499
656
|
const resolvePromotionAppModule = async (
|
|
@@ -511,23 +668,55 @@ const resolvePromotionAppModule = async (
|
|
|
511
668
|
);
|
|
512
669
|
};
|
|
513
670
|
|
|
514
|
-
|
|
515
|
-
|
|
671
|
+
/**
|
|
672
|
+
* Run `consume` while holding a fresh-load lease on `appModule`.
|
|
673
|
+
*
|
|
674
|
+
* @remarks
|
|
675
|
+
* The lease deletes its on-disk mirror on release. Any Topo consumption that
|
|
676
|
+
* may trigger deferred filesystem imports (for example inside a trail's
|
|
677
|
+
* `blaze` or a lazy relative `import()`) must run before the lease is
|
|
678
|
+
* released, otherwise those resolutions race the mirror teardown. Collapsing
|
|
679
|
+
* consumption into the leased critical section keeps that contract
|
|
680
|
+
* structural rather than relying on the caller to discover it.
|
|
681
|
+
*/
|
|
682
|
+
type LeaseAttempt =
|
|
683
|
+
| {
|
|
684
|
+
readonly ok: true;
|
|
685
|
+
readonly lease: FreshAppLease;
|
|
686
|
+
}
|
|
687
|
+
| { readonly ok: false; readonly loadError: string };
|
|
688
|
+
|
|
689
|
+
const tryAcquireLease = async (
|
|
690
|
+
appModule: string,
|
|
516
691
|
rootDir: string
|
|
517
|
-
): Promise<
|
|
692
|
+
): Promise<LeaseAttempt> => {
|
|
693
|
+
const loaded = await tryLoadFreshAppLease(appModule, rootDir);
|
|
694
|
+
if (loaded.isErr()) {
|
|
695
|
+
return { loadError: loaded.error.message, ok: false };
|
|
696
|
+
}
|
|
697
|
+
return { lease: loaded.value, ok: true };
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const withVerifiedApp = async <T>(
|
|
701
|
+
appModule: string | null,
|
|
702
|
+
rootDir: string,
|
|
703
|
+
consume: (app: Topo) => T | Promise<T>
|
|
704
|
+
): Promise<{ readonly load: PromotionLoadState; readonly value: T | null }> => {
|
|
518
705
|
if (appModule === null) {
|
|
519
|
-
return { appModule, loadError: null,
|
|
706
|
+
return { load: { appModule, loadError: null }, value: null };
|
|
520
707
|
}
|
|
521
708
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
return undefined;
|
|
527
|
-
}
|
|
528
|
-
);
|
|
709
|
+
const attempt = await tryAcquireLease(appModule, rootDir);
|
|
710
|
+
if (!attempt.ok) {
|
|
711
|
+
return { load: { appModule, loadError: attempt.loadError }, value: null };
|
|
712
|
+
}
|
|
529
713
|
|
|
530
|
-
|
|
714
|
+
try {
|
|
715
|
+
const value = await consume(attempt.lease.app);
|
|
716
|
+
return { load: { appModule, loadError: null }, value };
|
|
717
|
+
} finally {
|
|
718
|
+
attempt.lease.release();
|
|
719
|
+
}
|
|
531
720
|
};
|
|
532
721
|
|
|
533
722
|
const toRenamedFiles = (rootDir: string, renames: readonly FileRename[]) =>
|
|
@@ -541,19 +730,33 @@ const toUpdatedFiles = (rootDir: string, updatedSourceFiles: Set<string>) =>
|
|
|
541
730
|
.toSorted()
|
|
542
731
|
.map((filePath) => toRelativeOutputPath(rootDir, filePath));
|
|
543
732
|
|
|
733
|
+
const buildUnverifiedPromotionMessage = (
|
|
734
|
+
loadError: string | null,
|
|
735
|
+
dryRun: boolean
|
|
736
|
+
): string => {
|
|
737
|
+
if (dryRun) {
|
|
738
|
+
return 'Promotion plan is valid. Re-run without dryRun to apply it.';
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return loadError === null
|
|
742
|
+
? 'Promotion rewrote source files, but no topo entrypoint could be loaded for verification.'
|
|
743
|
+
: `Promotion rewrote source files, but verification failed: ${loadError}`;
|
|
744
|
+
};
|
|
745
|
+
|
|
544
746
|
const buildUnverifiedPromotionResult = (
|
|
545
747
|
rootDir: string,
|
|
546
748
|
loadError: string | null,
|
|
547
749
|
renames: readonly FileRename[],
|
|
548
750
|
updatedSourceFiles: Set<string>,
|
|
549
|
-
appModule: string | null
|
|
751
|
+
appModule: string | null,
|
|
752
|
+
plannedOperations: readonly PlannedProjectOperation[],
|
|
753
|
+
dryRun: boolean
|
|
550
754
|
) =>
|
|
551
755
|
Result.ok({
|
|
552
756
|
appModule,
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
: `Promotion rewrote source files, but verification failed: ${loadError}`,
|
|
757
|
+
dryRun,
|
|
758
|
+
message: buildUnverifiedPromotionMessage(loadError, dryRun),
|
|
759
|
+
plannedOperations,
|
|
557
760
|
promotedEstablished: false,
|
|
558
761
|
remainingDraftIds: [],
|
|
559
762
|
renamedFiles: toRenamedFiles(rootDir, renames),
|
|
@@ -562,11 +765,12 @@ const buildUnverifiedPromotionResult = (
|
|
|
562
765
|
|
|
563
766
|
const buildVerifiedPromotionResult = (
|
|
564
767
|
rootDir: string,
|
|
565
|
-
analysis: ReturnType<typeof
|
|
768
|
+
analysis: ReturnType<typeof deriveDraftReport>,
|
|
566
769
|
promotedEstablished: boolean,
|
|
567
770
|
renames: readonly FileRename[],
|
|
568
771
|
updatedSourceFiles: Set<string>,
|
|
569
772
|
appModule: string | null,
|
|
773
|
+
plannedOperations: readonly PlannedProjectOperation[],
|
|
570
774
|
toId: string
|
|
571
775
|
) => {
|
|
572
776
|
const blockingFinding = analysis.findings.find(
|
|
@@ -575,11 +779,13 @@ const buildVerifiedPromotionResult = (
|
|
|
575
779
|
|
|
576
780
|
return Result.ok({
|
|
577
781
|
appModule,
|
|
782
|
+
dryRun: false,
|
|
578
783
|
message:
|
|
579
784
|
blockingFinding?.message ??
|
|
580
785
|
(promotedEstablished
|
|
581
786
|
? `Promoted "${toId}" is now established.`
|
|
582
787
|
: `Promoted "${toId}" could not be verified as established.`),
|
|
788
|
+
plannedOperations,
|
|
583
789
|
promotedEstablished,
|
|
584
790
|
remainingDraftIds: [...analysis.declaredDraftIds].toSorted(),
|
|
585
791
|
renamedFiles: toRenamedFiles(rootDir, renames),
|
|
@@ -589,13 +795,14 @@ const buildVerifiedPromotionResult = (
|
|
|
589
795
|
|
|
590
796
|
const buildVerifiedPromotionResultFromApp = (
|
|
591
797
|
rootDir: string,
|
|
592
|
-
loadedApp:
|
|
798
|
+
loadedApp: Topo,
|
|
593
799
|
renames: readonly FileRename[],
|
|
594
800
|
updatedSourceFiles: Set<string>,
|
|
595
801
|
appModule: string | null,
|
|
802
|
+
plannedOperations: readonly PlannedProjectOperation[],
|
|
596
803
|
toId: string
|
|
597
804
|
) => {
|
|
598
|
-
const analysis =
|
|
805
|
+
const analysis = deriveDraftReport(loadedApp);
|
|
599
806
|
const promotedNode = collectOutputId(loadedApp, toId);
|
|
600
807
|
const promotedEstablished =
|
|
601
808
|
promotedNode !== undefined && !analysis.contaminatedIds.has(toId);
|
|
@@ -607,6 +814,7 @@ const buildVerifiedPromotionResultFromApp = (
|
|
|
607
814
|
renames,
|
|
608
815
|
updatedSourceFiles,
|
|
609
816
|
appModule,
|
|
817
|
+
plannedOperations,
|
|
610
818
|
toId
|
|
611
819
|
);
|
|
612
820
|
};
|
|
@@ -614,6 +822,7 @@ const buildVerifiedPromotionResultFromApp = (
|
|
|
614
822
|
const promoteDraftState = async (
|
|
615
823
|
input: {
|
|
616
824
|
readonly appModule?: string | undefined;
|
|
825
|
+
readonly dryRun?: boolean | undefined;
|
|
617
826
|
readonly fromId: string;
|
|
618
827
|
readonly renameFiles: boolean;
|
|
619
828
|
readonly rootDir?: string | undefined;
|
|
@@ -633,27 +842,45 @@ const promoteDraftState = async (
|
|
|
633
842
|
|
|
634
843
|
const { renames, updatedSourceFiles } = rewriteResult.value;
|
|
635
844
|
const appModule = await resolvePromotionAppModule(input, rootDirResult.value);
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
845
|
+
if (input.dryRun === true) {
|
|
846
|
+
return buildUnverifiedPromotionResult(
|
|
847
|
+
rootDirResult.value,
|
|
848
|
+
null,
|
|
849
|
+
renames,
|
|
850
|
+
updatedSourceFiles,
|
|
851
|
+
appModule,
|
|
852
|
+
rewriteResult.value.plannedOperations,
|
|
853
|
+
true
|
|
854
|
+
);
|
|
855
|
+
}
|
|
640
856
|
|
|
641
|
-
|
|
642
|
-
|
|
857
|
+
const { load, value } = await withVerifiedApp(
|
|
858
|
+
appModule,
|
|
859
|
+
rootDirResult.value,
|
|
860
|
+
(loadedApp) =>
|
|
861
|
+
buildVerifiedPromotionResultFromApp(
|
|
643
862
|
rootDirResult.value,
|
|
644
863
|
loadedApp,
|
|
645
864
|
renames,
|
|
646
865
|
updatedSourceFiles,
|
|
647
866
|
appModule,
|
|
867
|
+
rewriteResult.value.plannedOperations,
|
|
648
868
|
input.toId
|
|
649
869
|
)
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
return (
|
|
873
|
+
value ??
|
|
874
|
+
buildUnverifiedPromotionResult(
|
|
875
|
+
rootDirResult.value,
|
|
876
|
+
load.loadError,
|
|
877
|
+
renames,
|
|
878
|
+
updatedSourceFiles,
|
|
879
|
+
appModule,
|
|
880
|
+
rewriteResult.value.plannedOperations,
|
|
881
|
+
false
|
|
882
|
+
)
|
|
883
|
+
);
|
|
657
884
|
};
|
|
658
885
|
|
|
659
886
|
export const draftPromoteTrail = trail('draft.promote', {
|
|
@@ -664,6 +891,7 @@ export const draftPromoteTrail = trail('draft.promote', {
|
|
|
664
891
|
{
|
|
665
892
|
error: 'ValidationError',
|
|
666
893
|
input: {
|
|
894
|
+
// warden-ignore-next-line
|
|
667
895
|
fromId: '_draft.entity.prepare',
|
|
668
896
|
renameFiles: true,
|
|
669
897
|
rootDir: './__does_not_exist__/draft-promote-example',
|
|
@@ -677,6 +905,10 @@ export const draftPromoteTrail = trail('draft.promote', {
|
|
|
677
905
|
.string()
|
|
678
906
|
.optional()
|
|
679
907
|
.describe('Optional app module to verify after promotion'),
|
|
908
|
+
dryRun: z
|
|
909
|
+
.boolean()
|
|
910
|
+
.default(false)
|
|
911
|
+
.describe('Plan promotion rewrites without touching source files'),
|
|
680
912
|
fromId: z.string().describe('Draft id to promote'),
|
|
681
913
|
renameFiles: z
|
|
682
914
|
.boolean()
|
|
@@ -690,7 +922,19 @@ export const draftPromoteTrail = trail('draft.promote', {
|
|
|
690
922
|
intent: 'write',
|
|
691
923
|
output: z.object({
|
|
692
924
|
appModule: z.string().nullable(),
|
|
925
|
+
dryRun: z.boolean(),
|
|
693
926
|
message: z.string(),
|
|
927
|
+
plannedOperations: z.array(
|
|
928
|
+
z.discriminatedUnion('kind', [
|
|
929
|
+
z.object({ kind: z.literal('mkdir'), path: z.string() }),
|
|
930
|
+
z.object({
|
|
931
|
+
from: z.string(),
|
|
932
|
+
kind: z.literal('rename'),
|
|
933
|
+
to: z.string(),
|
|
934
|
+
}),
|
|
935
|
+
z.object({ kind: z.literal('write'), path: z.string() }),
|
|
936
|
+
])
|
|
937
|
+
),
|
|
694
938
|
promotedEstablished: z.boolean(),
|
|
695
939
|
remainingDraftIds: z.array(z.string()),
|
|
696
940
|
renamedFiles: z.array(
|