@ontrails/trails 1.0.0-beta.15 → 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 +197 -2
- package/README.md +27 -0
- package/package.json +19 -8
- package/src/app.ts +15 -5
- package/src/cli.ts +303 -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 +45 -23
- package/src/trails/add-trail.ts +27 -17
- package/src/trails/add-verify.ts +57 -17
- package/src/trails/completions-complete.ts +165 -0
- package/src/trails/completions.ts +47 -0
- package/src/trails/create-scaffold.ts +86 -33
- package/src/trails/create.ts +11 -3
- package/src/trails/dev-clean.ts +6 -1
- package/src/trails/dev-reset.ts +6 -1
- package/src/trails/dev-stats.ts +6 -1
- package/src/trails/dev-support.ts +29 -17
- package/src/trails/draft-promote.ts +289 -80
- package/src/trails/guide.ts +54 -34
- package/src/trails/load-app.ts +251 -56
- 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 +506 -200
- package/src/trails/topo-activation.ts +385 -0
- package/src/trails/topo-compile.ts +55 -0
- package/src/trails/topo-history.ts +6 -1
- package/src/trails/topo-output-schemas.ts +175 -0
- package/src/trails/topo-pin.ts +19 -6
- package/src/trails/topo-read-support.ts +171 -228
- package/src/trails/topo-reports.ts +400 -25
- package/src/trails/topo-store-support.ts +43 -19
- package/src/trails/topo-support.ts +18 -28
- package/src/trails/topo-unpin.ts +6 -1
- package/src/trails/topo-verify.ts +18 -5
- package/src/trails/topo.ts +60 -23
- package/src/trails/warden-guide.ts +121 -0
- package/src/trails/warden.ts +137 -56
- package/src/versions.ts +3 -18
- 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 -45
- 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 -14
- 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 -110
- package/dist/src/trails/add-surface.js.map +0 -1
- package/dist/src/trails/add-trail.d.ts +0 -12
- package/dist/src/trails/add-trail.d.ts.map +0 -1
- package/dist/src/trails/add-trail.js +0 -104
- 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 -68
- 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 -295
- package/dist/src/trails/create-scaffold.js.map +0 -1
- package/dist/src/trails/create.d.ts +0 -18
- package/dist/src/trails/create.d.ts.map +0 -1
- package/dist/src/trails/create.js +0 -126
- 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 -66
- 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 -39
- 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 -181
- 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 -400
- 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 -61
- package/dist/src/trails/guide.js.map +0 -1
- package/dist/src/trails/load-app.d.ts +0 -12
- package/dist/src/trails/load-app.d.ts.map +0 -1
- package/dist/src/trails/load-app.js +0 -415
- 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 -234
- 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 -19
- package/dist/src/trails/topo-export.d.ts.map +0 -1
- package/dist/src/trails/topo-export.js +0 -31
- package/dist/src/trails/topo-export.js.map +0 -1
- package/dist/src/trails/topo-history.d.ts +0 -20
- package/dist/src/trails/topo-history.d.ts.map +0 -1
- package/dist/src/trails/topo-history.js +0 -32
- package/dist/src/trails/topo-history.js.map +0 -1
- package/dist/src/trails/topo-pin.d.ts +0 -17
- package/dist/src/trails/topo-pin.d.ts.map +0 -1
- package/dist/src/trails/topo-pin.js +0 -31
- package/dist/src/trails/topo-pin.js.map +0 -1
- package/dist/src/trails/topo-read-support.d.ts +0 -58
- package/dist/src/trails/topo-read-support.d.ts.map +0 -1
- package/dist/src/trails/topo-read-support.js +0 -167
- package/dist/src/trails/topo-read-support.js.map +0 -1
- package/dist/src/trails/topo-reports.d.ts +0 -54
- package/dist/src/trails/topo-reports.d.ts.map +0 -1
- package/dist/src/trails/topo-reports.js +0 -128
- 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 -49
- 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 -76
- package/dist/src/trails/topo-support.d.ts.map +0 -1
- package/dist/src/trails/topo-support.js +0 -132
- package/dist/src/trails/topo-support.js.map +0 -1
- package/dist/src/trails/topo-unpin.d.ts +0 -20
- package/dist/src/trails/topo-unpin.d.ts.map +0 -1
- package/dist/src/trails/topo-unpin.js +0 -44
- 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 -24
- 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 -63
- package/dist/src/trails/topo.js.map +0 -1
- package/dist/src/trails/warden.d.ts +0 -20
- package/dist/src/trails/warden.d.ts.map +0 -1
- package/dist/src/trails/warden.js +0 -98
- package/dist/src/trails/warden.js.map +0 -1
- package/dist/src/versions.d.ts +0 -12
- package/dist/src/versions.d.ts.map +0 -1
- package/dist/src/versions.js +0 -23
- package/dist/src/versions.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/__tests__/add-trail.test.ts +0 -97
- package/src/__tests__/create.test.ts +0 -415
- package/src/__tests__/draft-promote.test.ts +0 -144
- package/src/__tests__/guide.test.ts +0 -96
- package/src/__tests__/load-app.test.ts +0 -419
- package/src/__tests__/survey.test.ts +0 -377
- package/src/__tests__/topo-dev.test.ts +0 -426
- package/src/__tests__/warden.test.ts +0 -74
- package/src/trails/topo-export.ts +0 -35
- package/src/trails/topo-show.ts +0 -54
- package/tsconfig.json +0 -9
- package/tsconfig.tests.json +0 -10
|
@@ -1,36 +1,43 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
countPinnedSnapshots,
|
|
6
|
-
countPrunableSnapshots,
|
|
7
|
-
countTopoSnapshots,
|
|
8
|
-
pruneUnpinnedSnapshots,
|
|
9
|
-
} from '@ontrails/core/internal/topo-snapshots';
|
|
10
4
|
import {
|
|
11
5
|
openReadTrailsDb,
|
|
12
6
|
openWriteTrailsDb,
|
|
13
7
|
deriveTrailsDbPath,
|
|
14
8
|
deriveTrailsDir,
|
|
15
|
-
} from '@ontrails/core
|
|
9
|
+
} from '@ontrails/core';
|
|
10
|
+
import {
|
|
11
|
+
countPinnedSnapshots,
|
|
12
|
+
countPrunableSnapshots,
|
|
13
|
+
countTopoSnapshots,
|
|
14
|
+
pruneUnpinnedSnapshots,
|
|
15
|
+
} from '@ontrails/topographer/backend-support';
|
|
16
16
|
import {
|
|
17
17
|
DEFAULT_MAX_AGE,
|
|
18
18
|
DEFAULT_MAX_RECORDS,
|
|
19
19
|
applyTraceCleanup,
|
|
20
20
|
countTraceRecords,
|
|
21
21
|
previewTraceCleanup,
|
|
22
|
-
} from '@ontrails/tracing
|
|
22
|
+
} from '@ontrails/tracing';
|
|
23
|
+
|
|
24
|
+
import { removeRootRelativeFileIfPresent } from '../local-state-io.js';
|
|
25
|
+
|
|
26
|
+
import { requireTrailRootDir } from './root-dir.js';
|
|
23
27
|
|
|
24
28
|
export const DEFAULT_TOPO_SNAPSHOT_RETENTION = 50;
|
|
25
29
|
|
|
26
|
-
const deriveRootDir = (cwd?: string): string => cwd
|
|
30
|
+
const deriveRootDir = (cwd?: string): string => requireTrailRootDir(cwd);
|
|
27
31
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
const removeResetFileIfPresent = (
|
|
33
|
+
rootDir: string,
|
|
34
|
+
relativePath: string
|
|
35
|
+
): boolean => {
|
|
36
|
+
const removed = removeRootRelativeFileIfPresent(rootDir, relativePath);
|
|
37
|
+
if (removed.isErr()) {
|
|
38
|
+
throw removed.error;
|
|
31
39
|
}
|
|
32
|
-
|
|
33
|
-
return true;
|
|
40
|
+
return removed.value;
|
|
34
41
|
};
|
|
35
42
|
|
|
36
43
|
export interface DevStatsReport {
|
|
@@ -132,7 +139,7 @@ const buildDbStats = (
|
|
|
132
139
|
): DevStatsReport['db'] => ({
|
|
133
140
|
exists,
|
|
134
141
|
fileSizeBytes: exists ? statSync(dbPath).size : 0,
|
|
135
|
-
path: '.trails/trails.db',
|
|
142
|
+
path: '.trails/state/trails.db',
|
|
136
143
|
});
|
|
137
144
|
|
|
138
145
|
const emptyDevStats = (
|
|
@@ -250,6 +257,11 @@ const buildCleanReport = (
|
|
|
250
257
|
};
|
|
251
258
|
|
|
252
259
|
const RESET_FILES = [
|
|
260
|
+
'.trails/state/trails.db',
|
|
261
|
+
'.trails/state/trails.db-shm',
|
|
262
|
+
'.trails/state/trails.db-wal',
|
|
263
|
+
// Legacy paths (pre-state migration) — cleaned for one cycle so upgrading
|
|
264
|
+
// workspaces do not leave stale DB sidecars at old locations.
|
|
253
265
|
'.trails/trails.db',
|
|
254
266
|
'.trails/trails.db-shm',
|
|
255
267
|
'.trails/trails.db-wal',
|
|
@@ -317,7 +329,7 @@ export const resetDevState = (options?: {
|
|
|
317
329
|
}
|
|
318
330
|
|
|
319
331
|
const removedFiles = files.filter((relativePath) =>
|
|
320
|
-
|
|
332
|
+
removeResetFileIfPresent(rootDir, relativePath)
|
|
321
333
|
);
|
|
322
334
|
|
|
323
335
|
return {
|
|
@@ -1,5 +1,12 @@
|
|
|
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,
|
|
@@ -11,15 +18,24 @@ import {
|
|
|
11
18
|
import type { Topo } from '@ontrails/core';
|
|
12
19
|
import {
|
|
13
20
|
DRAFT_FILE_PREFIX,
|
|
14
|
-
findStringLiterals,
|
|
15
21
|
isDraftMarkedFile,
|
|
16
|
-
parse,
|
|
17
22
|
stripDraftFileMarkers,
|
|
18
23
|
} from '@ontrails/warden';
|
|
24
|
+
import { findStringLiterals, parse } from '@ontrails/warden/ast';
|
|
19
25
|
import { z } from 'zod';
|
|
20
26
|
|
|
21
|
-
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';
|
|
22
37
|
import { findTopoPath } from './project.js';
|
|
38
|
+
import { resolveTrailRootDir } from './root-dir.js';
|
|
23
39
|
|
|
24
40
|
interface PromotionEdit {
|
|
25
41
|
readonly end: number;
|
|
@@ -177,6 +193,7 @@ const toProjectModulePath = (sourceImport: string): string =>
|
|
|
177
193
|
: sourceImport;
|
|
178
194
|
|
|
179
195
|
interface PromotionRewriteState {
|
|
196
|
+
readonly plannedOperations: PlannedProjectOperation[];
|
|
180
197
|
readonly renames: FileRename[];
|
|
181
198
|
readonly updatedSourceFiles: Set<string>;
|
|
182
199
|
}
|
|
@@ -240,7 +257,22 @@ const resolveValidatedPromotionRoot = (
|
|
|
240
257
|
return validation;
|
|
241
258
|
}
|
|
242
259
|
|
|
243
|
-
|
|
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
|
+
}
|
|
244
276
|
const rootValidation = validatePromotionRoot(rootDir);
|
|
245
277
|
if (rootValidation.isErr()) {
|
|
246
278
|
return rootValidation;
|
|
@@ -249,27 +281,83 @@ const resolveValidatedPromotionRoot = (
|
|
|
249
281
|
return Result.ok(rootDir);
|
|
250
282
|
};
|
|
251
283
|
|
|
252
|
-
|
|
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 = (
|
|
253
331
|
filePaths: readonly string[],
|
|
332
|
+
sources: SourceFileMap,
|
|
254
333
|
fromId: string,
|
|
255
334
|
toId: string,
|
|
256
|
-
updatedSourceFiles: Set<string
|
|
257
|
-
|
|
335
|
+
updatedSourceFiles: Set<string>,
|
|
336
|
+
operations: ProjectWriteOperation[]
|
|
337
|
+
): Result<void, Error> => {
|
|
258
338
|
for (const filePath of filePaths) {
|
|
259
|
-
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
|
+
|
|
260
346
|
const replaced = replaceIdLiterals(sourceCode, filePath, fromId, toId);
|
|
261
347
|
if (!replaced.changed) {
|
|
262
348
|
continue;
|
|
263
349
|
}
|
|
264
350
|
|
|
265
|
-
|
|
351
|
+
sources.set(filePath, replaced.nextSource);
|
|
352
|
+
pushWriteOperation(operations, {
|
|
353
|
+
content: replaced.nextSource,
|
|
354
|
+
kind: 'write',
|
|
355
|
+
path: filePath,
|
|
356
|
+
});
|
|
266
357
|
updatedSourceFiles.add(filePath);
|
|
267
358
|
}
|
|
268
|
-
};
|
|
269
359
|
|
|
270
|
-
|
|
271
|
-
const sourceCode = await Bun.file(filePath).text();
|
|
272
|
-
return hasDraftIds(sourceCode, filePath);
|
|
360
|
+
return Result.ok();
|
|
273
361
|
};
|
|
274
362
|
|
|
275
363
|
const buildPromotableFileRename = (
|
|
@@ -293,9 +381,10 @@ const buildPromotableFileRename = (
|
|
|
293
381
|
return Result.ok({ from: filePath, to: nextPath });
|
|
294
382
|
};
|
|
295
383
|
|
|
296
|
-
const collectPromotableFileRename =
|
|
297
|
-
filePath: string
|
|
298
|
-
|
|
384
|
+
const collectPromotableFileRename = (
|
|
385
|
+
filePath: string,
|
|
386
|
+
sourceCode: string
|
|
387
|
+
): Result<FileRename | null, Error> => {
|
|
299
388
|
if (!isDraftMarkedFile(filePath)) {
|
|
300
389
|
return Result.ok(null);
|
|
301
390
|
}
|
|
@@ -305,7 +394,7 @@ const collectPromotableFileRename = async (
|
|
|
305
394
|
return Result.ok(null);
|
|
306
395
|
}
|
|
307
396
|
|
|
308
|
-
if (
|
|
397
|
+
if (hasDraftIds(sourceCode, filePath)) {
|
|
309
398
|
return Result.ok(null);
|
|
310
399
|
}
|
|
311
400
|
|
|
@@ -337,12 +426,19 @@ const validateRenameTargets = (
|
|
|
337
426
|
return Result.ok();
|
|
338
427
|
};
|
|
339
428
|
|
|
340
|
-
const collectFileRenames =
|
|
341
|
-
filePaths: readonly string[]
|
|
342
|
-
|
|
429
|
+
const collectFileRenames = (
|
|
430
|
+
filePaths: readonly string[],
|
|
431
|
+
sources: SourceFileMap
|
|
432
|
+
): Result<FileRename[], Error> => {
|
|
343
433
|
const renames: FileRename[] = [];
|
|
344
434
|
for (const filePath of filePaths) {
|
|
345
|
-
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);
|
|
346
442
|
if (renameResult.isErr()) {
|
|
347
443
|
return renameResult;
|
|
348
444
|
}
|
|
@@ -353,26 +449,6 @@ const collectFileRenames = async (
|
|
|
353
449
|
return Result.ok(renames);
|
|
354
450
|
};
|
|
355
451
|
|
|
356
|
-
const collectAndApplyFileRenames = async (
|
|
357
|
-
filePaths: readonly string[]
|
|
358
|
-
): Promise<Result<FileRename[], Error>> => {
|
|
359
|
-
const collected = await collectFileRenames(filePaths);
|
|
360
|
-
if (collected.isErr()) {
|
|
361
|
-
return collected;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const renames = collected.value;
|
|
365
|
-
const valid = validateRenameTargets(renames);
|
|
366
|
-
if (valid.isErr()) {
|
|
367
|
-
return valid;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
for (const r of renames) {
|
|
371
|
-
renameSync(r.from, r.to);
|
|
372
|
-
}
|
|
373
|
-
return Result.ok(renames);
|
|
374
|
-
};
|
|
375
|
-
|
|
376
452
|
const applyRenameEffects = (
|
|
377
453
|
updatedSourceFiles: Set<string>,
|
|
378
454
|
renames: readonly FileRename[]
|
|
@@ -384,6 +460,30 @@ const applyRenameEffects = (
|
|
|
384
460
|
}
|
|
385
461
|
};
|
|
386
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
|
+
|
|
387
487
|
const applyRelativeImportRename = (
|
|
388
488
|
sourceCode: string,
|
|
389
489
|
filePath: string,
|
|
@@ -429,69 +529,128 @@ const rewriteRelativeImportsForFile = (
|
|
|
429
529
|
return { changed, sourceCode: nextSourceCode };
|
|
430
530
|
};
|
|
431
531
|
|
|
432
|
-
const
|
|
532
|
+
const planRelativeImportsForFile = (
|
|
433
533
|
filePath: string,
|
|
434
|
-
renames: readonly FileRename[]
|
|
435
|
-
|
|
436
|
-
|
|
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
|
+
|
|
437
545
|
const updated = rewriteRelativeImportsForFile(filePath, renames, sourceCode);
|
|
438
546
|
if (updated.changed) {
|
|
439
|
-
|
|
440
|
-
|
|
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);
|
|
441
554
|
}
|
|
442
555
|
|
|
443
|
-
return false;
|
|
556
|
+
return Result.ok(false);
|
|
444
557
|
};
|
|
445
558
|
|
|
446
|
-
const
|
|
559
|
+
const planRelativeImports = (
|
|
447
560
|
filePaths: readonly string[],
|
|
448
|
-
renames: readonly FileRename[]
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
561
|
+
renames: readonly FileRename[],
|
|
562
|
+
sources: SourceFileMap,
|
|
563
|
+
updatedSourceFiles: Set<string>,
|
|
564
|
+
operations: ProjectWriteOperation[]
|
|
565
|
+
): Result<void, Error> => {
|
|
452
566
|
for (const filePath of filePaths) {
|
|
453
|
-
const changed =
|
|
454
|
-
|
|
455
|
-
|
|
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);
|
|
456
578
|
}
|
|
457
579
|
}
|
|
458
580
|
|
|
459
|
-
return
|
|
581
|
+
return Result.ok();
|
|
460
582
|
};
|
|
461
583
|
|
|
462
584
|
const rewritePromotionState = async (
|
|
463
585
|
rootDir: string,
|
|
464
586
|
input: {
|
|
587
|
+
readonly dryRun?: boolean | undefined;
|
|
465
588
|
readonly fromId: string;
|
|
466
589
|
readonly renameFiles: boolean;
|
|
467
590
|
readonly toId: string;
|
|
468
591
|
}
|
|
469
592
|
): Promise<Result<PromotionRewriteState, Error>> => {
|
|
470
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[] = [];
|
|
471
600
|
const updatedSourceFiles = new Set<string>();
|
|
472
601
|
|
|
473
|
-
|
|
602
|
+
const rewritten = planPromotedSourceFiles(
|
|
474
603
|
initialFiles,
|
|
604
|
+
sources,
|
|
475
605
|
input.fromId,
|
|
476
606
|
input.toId,
|
|
477
|
-
updatedSourceFiles
|
|
607
|
+
updatedSourceFiles,
|
|
608
|
+
operations
|
|
478
609
|
);
|
|
610
|
+
if (rewritten.isErr()) {
|
|
611
|
+
return Result.err(rewritten.error);
|
|
612
|
+
}
|
|
479
613
|
|
|
480
614
|
const renamesResult = input.renameFiles
|
|
481
|
-
?
|
|
615
|
+
? collectFileRenames(initialFiles, sources)
|
|
482
616
|
: Result.ok([] as FileRename[]);
|
|
483
617
|
if (renamesResult.isErr()) {
|
|
484
618
|
return Result.err(renamesResult.error);
|
|
485
619
|
}
|
|
486
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
|
+
|
|
487
630
|
applyRenameEffects(updatedSourceFiles, renamesResult.value);
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
renamesResult.value
|
|
491
|
-
|
|
492
|
-
|
|
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);
|
|
641
|
+
}
|
|
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);
|
|
493
648
|
}
|
|
494
|
-
return Result.ok({
|
|
649
|
+
return Result.ok({
|
|
650
|
+
plannedOperations: plannedOperations.value,
|
|
651
|
+
renames: renamesResult.value,
|
|
652
|
+
updatedSourceFiles,
|
|
653
|
+
});
|
|
495
654
|
};
|
|
496
655
|
|
|
497
656
|
const resolvePromotionAppModule = async (
|
|
@@ -523,7 +682,7 @@ const resolvePromotionAppModule = async (
|
|
|
523
682
|
type LeaseAttempt =
|
|
524
683
|
| {
|
|
525
684
|
readonly ok: true;
|
|
526
|
-
readonly lease:
|
|
685
|
+
readonly lease: FreshAppLease;
|
|
527
686
|
}
|
|
528
687
|
| { readonly ok: false; readonly loadError: string };
|
|
529
688
|
|
|
@@ -531,12 +690,11 @@ const tryAcquireLease = async (
|
|
|
531
690
|
appModule: string,
|
|
532
691
|
rootDir: string
|
|
533
692
|
): Promise<LeaseAttempt> => {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const loadError = error instanceof Error ? error.message : String(error);
|
|
538
|
-
return { loadError, ok: false };
|
|
693
|
+
const loaded = await tryLoadFreshAppLease(appModule, rootDir);
|
|
694
|
+
if (loaded.isErr()) {
|
|
695
|
+
return { loadError: loaded.error.message, ok: false };
|
|
539
696
|
}
|
|
697
|
+
return { lease: loaded.value, ok: true };
|
|
540
698
|
};
|
|
541
699
|
|
|
542
700
|
const withVerifiedApp = async <T>(
|
|
@@ -572,19 +730,33 @@ const toUpdatedFiles = (rootDir: string, updatedSourceFiles: Set<string>) =>
|
|
|
572
730
|
.toSorted()
|
|
573
731
|
.map((filePath) => toRelativeOutputPath(rootDir, filePath));
|
|
574
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
|
+
|
|
575
746
|
const buildUnverifiedPromotionResult = (
|
|
576
747
|
rootDir: string,
|
|
577
748
|
loadError: string | null,
|
|
578
749
|
renames: readonly FileRename[],
|
|
579
750
|
updatedSourceFiles: Set<string>,
|
|
580
|
-
appModule: string | null
|
|
751
|
+
appModule: string | null,
|
|
752
|
+
plannedOperations: readonly PlannedProjectOperation[],
|
|
753
|
+
dryRun: boolean
|
|
581
754
|
) =>
|
|
582
755
|
Result.ok({
|
|
583
756
|
appModule,
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
: `Promotion rewrote source files, but verification failed: ${loadError}`,
|
|
757
|
+
dryRun,
|
|
758
|
+
message: buildUnverifiedPromotionMessage(loadError, dryRun),
|
|
759
|
+
plannedOperations,
|
|
588
760
|
promotedEstablished: false,
|
|
589
761
|
remainingDraftIds: [],
|
|
590
762
|
renamedFiles: toRenamedFiles(rootDir, renames),
|
|
@@ -598,6 +770,7 @@ const buildVerifiedPromotionResult = (
|
|
|
598
770
|
renames: readonly FileRename[],
|
|
599
771
|
updatedSourceFiles: Set<string>,
|
|
600
772
|
appModule: string | null,
|
|
773
|
+
plannedOperations: readonly PlannedProjectOperation[],
|
|
601
774
|
toId: string
|
|
602
775
|
) => {
|
|
603
776
|
const blockingFinding = analysis.findings.find(
|
|
@@ -606,11 +779,13 @@ const buildVerifiedPromotionResult = (
|
|
|
606
779
|
|
|
607
780
|
return Result.ok({
|
|
608
781
|
appModule,
|
|
782
|
+
dryRun: false,
|
|
609
783
|
message:
|
|
610
784
|
blockingFinding?.message ??
|
|
611
785
|
(promotedEstablished
|
|
612
786
|
? `Promoted "${toId}" is now established.`
|
|
613
787
|
: `Promoted "${toId}" could not be verified as established.`),
|
|
788
|
+
plannedOperations,
|
|
614
789
|
promotedEstablished,
|
|
615
790
|
remainingDraftIds: [...analysis.declaredDraftIds].toSorted(),
|
|
616
791
|
renamedFiles: toRenamedFiles(rootDir, renames),
|
|
@@ -624,6 +799,7 @@ const buildVerifiedPromotionResultFromApp = (
|
|
|
624
799
|
renames: readonly FileRename[],
|
|
625
800
|
updatedSourceFiles: Set<string>,
|
|
626
801
|
appModule: string | null,
|
|
802
|
+
plannedOperations: readonly PlannedProjectOperation[],
|
|
627
803
|
toId: string
|
|
628
804
|
) => {
|
|
629
805
|
const analysis = deriveDraftReport(loadedApp);
|
|
@@ -638,6 +814,7 @@ const buildVerifiedPromotionResultFromApp = (
|
|
|
638
814
|
renames,
|
|
639
815
|
updatedSourceFiles,
|
|
640
816
|
appModule,
|
|
817
|
+
plannedOperations,
|
|
641
818
|
toId
|
|
642
819
|
);
|
|
643
820
|
};
|
|
@@ -645,6 +822,7 @@ const buildVerifiedPromotionResultFromApp = (
|
|
|
645
822
|
const promoteDraftState = async (
|
|
646
823
|
input: {
|
|
647
824
|
readonly appModule?: string | undefined;
|
|
825
|
+
readonly dryRun?: boolean | undefined;
|
|
648
826
|
readonly fromId: string;
|
|
649
827
|
readonly renameFiles: boolean;
|
|
650
828
|
readonly rootDir?: string | undefined;
|
|
@@ -664,6 +842,18 @@ const promoteDraftState = async (
|
|
|
664
842
|
|
|
665
843
|
const { renames, updatedSourceFiles } = rewriteResult.value;
|
|
666
844
|
const appModule = await resolvePromotionAppModule(input, rootDirResult.value);
|
|
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
|
+
}
|
|
856
|
+
|
|
667
857
|
const { load, value } = await withVerifiedApp(
|
|
668
858
|
appModule,
|
|
669
859
|
rootDirResult.value,
|
|
@@ -674,6 +864,7 @@ const promoteDraftState = async (
|
|
|
674
864
|
renames,
|
|
675
865
|
updatedSourceFiles,
|
|
676
866
|
appModule,
|
|
867
|
+
rewriteResult.value.plannedOperations,
|
|
677
868
|
input.toId
|
|
678
869
|
)
|
|
679
870
|
);
|
|
@@ -685,7 +876,9 @@ const promoteDraftState = async (
|
|
|
685
876
|
load.loadError,
|
|
686
877
|
renames,
|
|
687
878
|
updatedSourceFiles,
|
|
688
|
-
appModule
|
|
879
|
+
appModule,
|
|
880
|
+
rewriteResult.value.plannedOperations,
|
|
881
|
+
false
|
|
689
882
|
)
|
|
690
883
|
);
|
|
691
884
|
};
|
|
@@ -712,6 +905,10 @@ export const draftPromoteTrail = trail('draft.promote', {
|
|
|
712
905
|
.string()
|
|
713
906
|
.optional()
|
|
714
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'),
|
|
715
912
|
fromId: z.string().describe('Draft id to promote'),
|
|
716
913
|
renameFiles: z
|
|
717
914
|
.boolean()
|
|
@@ -725,7 +922,19 @@ export const draftPromoteTrail = trail('draft.promote', {
|
|
|
725
922
|
intent: 'write',
|
|
726
923
|
output: z.object({
|
|
727
924
|
appModule: z.string().nullable(),
|
|
925
|
+
dryRun: z.boolean(),
|
|
728
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
|
+
),
|
|
729
938
|
promotedEstablished: z.boolean(),
|
|
730
939
|
remainingDraftIds: z.array(z.string()),
|
|
731
940
|
renamedFiles: z.array(
|