@entelligentsia/forgecli 0.10.1 → 0.11.2
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 +78 -0
- package/README.md +21 -3
- package/dist/CHANGELOG-forge-plugin.md +22 -0
- package/dist/extensions/forgecli/add-pipeline.d.ts +19 -0
- package/dist/extensions/forgecli/add-pipeline.js +143 -0
- package/dist/extensions/forgecli/add-pipeline.js.map +1 -0
- package/dist/extensions/forgecli/add-task.d.ts +20 -0
- package/dist/extensions/forgecli/add-task.js +154 -0
- package/dist/extensions/forgecli/add-task.js.map +1 -0
- package/dist/extensions/forgecli/calibrate.d.ts +61 -0
- package/dist/extensions/forgecli/calibrate.js +488 -0
- package/dist/extensions/forgecli/calibrate.js.map +1 -0
- package/dist/extensions/forgecli/fix-bug.d.ts +9 -1
- package/dist/extensions/forgecli/fix-bug.js +70 -8
- package/dist/extensions/forgecli/fix-bug.js.map +1 -1
- package/dist/extensions/forgecli/forge-commands.js +15 -22
- package/dist/extensions/forgecli/forge-commands.js.map +1 -1
- package/dist/extensions/forgecli/forge-subagent.js +34 -7
- package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
- package/dist/extensions/forgecli/forge-update-command.d.ts +9 -0
- package/dist/extensions/forgecli/forge-update-command.js +106 -7
- package/dist/extensions/forgecli/forge-update-command.js.map +1 -1
- package/dist/extensions/forgecli/health-check.d.ts +22 -1
- package/dist/extensions/forgecli/health-check.js +177 -4
- package/dist/extensions/forgecli/health-check.js.map +1 -1
- package/dist/extensions/forgecli/hook-dispatcher.d.ts +25 -1
- package/dist/extensions/forgecli/hook-dispatcher.js +104 -9
- package/dist/extensions/forgecli/hook-dispatcher.js.map +1 -1
- package/dist/extensions/forgecli/hooks/check-update.d.ts +81 -0
- package/dist/extensions/forgecli/hooks/check-update.js +308 -0
- package/dist/extensions/forgecli/hooks/check-update.js.map +1 -0
- package/dist/extensions/forgecli/hooks/forge-permissions.d.ts +32 -0
- package/dist/extensions/forgecli/hooks/forge-permissions.js +119 -0
- package/dist/extensions/forgecli/hooks/forge-permissions.js.map +1 -0
- package/dist/extensions/forgecli/hooks/triage-error.d.ts +23 -0
- package/dist/extensions/forgecli/hooks/triage-error.js +62 -0
- package/dist/extensions/forgecli/hooks/triage-error.js.map +1 -0
- package/dist/extensions/forgecli/hooks/write-guard.d.ts +28 -0
- package/dist/extensions/forgecli/hooks/write-guard.js +225 -0
- package/dist/extensions/forgecli/hooks/write-guard.js.map +1 -0
- package/dist/extensions/forgecli/index.js +60 -0
- package/dist/extensions/forgecli/index.js.map +1 -1
- package/dist/extensions/forgecli/init-context.d.ts +1 -1
- package/dist/extensions/forgecli/init-context.js +21 -6
- package/dist/extensions/forgecli/init-context.js.map +1 -1
- package/dist/extensions/forgecli/materialize.d.ts +16 -0
- package/dist/extensions/forgecli/materialize.js +195 -0
- package/dist/extensions/forgecli/materialize.js.map +1 -0
- package/dist/extensions/forgecli/migrate.d.ts +19 -0
- package/dist/extensions/forgecli/migrate.js +258 -0
- package/dist/extensions/forgecli/migrate.js.map +1 -0
- package/dist/extensions/forgecli/migration-engine.d.ts +111 -0
- package/dist/extensions/forgecli/migration-engine.js +533 -0
- package/dist/extensions/forgecli/migration-engine.js.map +1 -0
- package/dist/extensions/forgecli/quiz-agent.d.ts +17 -0
- package/dist/extensions/forgecli/quiz-agent.js +98 -0
- package/dist/extensions/forgecli/quiz-agent.js.map +1 -0
- package/dist/extensions/forgecli/remove-command.d.ts +17 -0
- package/dist/extensions/forgecli/remove-command.js +124 -0
- package/dist/extensions/forgecli/remove-command.js.map +1 -0
- package/dist/extensions/forgecli/report-bug.d.ts +25 -0
- package/dist/extensions/forgecli/report-bug.js +159 -0
- package/dist/extensions/forgecli/report-bug.js.map +1 -0
- package/dist/extensions/forgecli/retrospective.d.ts +19 -0
- package/dist/extensions/forgecli/retrospective.js +156 -0
- package/dist/extensions/forgecli/retrospective.js.map +1 -0
- package/dist/extensions/forgecli/run-sprint.js +34 -0
- package/dist/extensions/forgecli/run-sprint.js.map +1 -1
- package/dist/extensions/forgecli/run-task.d.ts +9 -1
- package/dist/extensions/forgecli/run-task.js +64 -10
- package/dist/extensions/forgecli/run-task.js.map +1 -1
- package/dist/extensions/forgecli/session-registry.d.ts +27 -2
- package/dist/extensions/forgecli/session-registry.js +52 -1
- package/dist/extensions/forgecli/session-registry.js.map +1 -1
- package/dist/extensions/forgecli/status-command.d.ts +19 -0
- package/dist/extensions/forgecli/status-command.js +140 -0
- package/dist/extensions/forgecli/status-command.js.map +1 -0
- package/dist/extensions/forgecli/store-query.d.ts +22 -0
- package/dist/extensions/forgecli/store-query.js +107 -0
- package/dist/extensions/forgecli/store-query.js.map +1 -0
- package/dist/extensions/forgecli/store-repair.d.ts +17 -0
- package/dist/extensions/forgecli/store-repair.js +123 -0
- package/dist/extensions/forgecli/store-repair.js.map +1 -0
- package/dist/extensions/forgecli/thread-switcher.js +213 -28
- package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
- package/dist/extensions/forgecli/update-tools.d.ts +23 -0
- package/dist/extensions/forgecli/update-tools.js +136 -0
- package/dist/extensions/forgecli/update-tools.js.map +1 -0
- package/dist/extensions/forgecli/viewport-theme.js +4 -0
- package/dist/extensions/forgecli/viewport-theme.js.map +1 -1
- package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
- package/dist/forge-payload/.schemas/config.schema.json +83 -0
- package/dist/forge-payload/.schemas/migrations.json +2049 -0
- package/dist/forge-payload/commands/regenerate.md +17 -1
- package/dist/forge-payload/meta/personas/README.md +16 -0
- package/dist/forge-payload/meta/personas/meta-architect.md +70 -0
- package/dist/forge-payload/meta/personas/meta-bug-fixer.md +73 -0
- package/dist/forge-payload/meta/personas/meta-collator.md +72 -0
- package/dist/forge-payload/meta/personas/meta-engineer.md +70 -0
- package/dist/forge-payload/meta/personas/meta-orchestrator.md +71 -0
- package/dist/forge-payload/meta/personas/meta-product-manager.md +82 -0
- package/dist/forge-payload/meta/personas/meta-qa-engineer.md +91 -0
- package/dist/forge-payload/meta/personas/meta-supervisor.md +92 -0
- package/dist/forge-payload/meta/skill-recommendations.md +154 -0
- package/dist/forge-payload/meta/skills/meta-architect-skills.md +43 -0
- package/dist/forge-payload/meta/skills/meta-bug-fixer-skills.md +43 -0
- package/dist/forge-payload/meta/skills/meta-collator-skills.md +41 -0
- package/dist/forge-payload/meta/skills/meta-engineer-skills.md +43 -0
- package/dist/forge-payload/meta/skills/meta-generic-skills.md +58 -0
- package/dist/forge-payload/meta/skills/meta-qa-engineer-skills.md +46 -0
- package/dist/forge-payload/meta/skills/meta-supervisor-skills.md +43 -0
- package/dist/forge-payload/meta/store-schema/bug.schema.md +71 -0
- package/dist/forge-payload/meta/store-schema/event.schema.md +76 -0
- package/dist/forge-payload/meta/store-schema/feature.schema.md +65 -0
- package/dist/forge-payload/meta/store-schema/sprint.schema.md +64 -0
- package/dist/forge-payload/meta/store-schema/task.schema.md +78 -0
- package/dist/forge-payload/meta/templates/meta-code-review.md +26 -0
- package/dist/forge-payload/meta/templates/meta-plan-review.md +28 -0
- package/dist/forge-payload/meta/templates/meta-plan.md +28 -0
- package/dist/forge-payload/meta/templates/meta-progress.md +25 -0
- package/dist/forge-payload/meta/templates/meta-retrospective.md +28 -0
- package/dist/forge-payload/meta/templates/meta-sprint-manifest.md +26 -0
- package/dist/forge-payload/meta/templates/meta-sprint-requirements.md +91 -0
- package/dist/forge-payload/meta/templates/meta-task-prompt.md +26 -0
- package/dist/forge-payload/meta/tool-specs/collate.spec.md +88 -0
- package/dist/forge-payload/meta/tool-specs/generation-manifest.spec.md +139 -0
- package/dist/forge-payload/meta/tool-specs/manage-config.spec.md +143 -0
- package/dist/forge-payload/meta/tool-specs/seed-store.spec.md +91 -0
- package/dist/forge-payload/meta/tool-specs/store-cli.spec.md +328 -0
- package/dist/forge-payload/meta/tool-specs/validate-store.spec.md +191 -0
- package/dist/forge-payload/meta/workflows/_fragments/context-injection.md +75 -0
- package/dist/forge-payload/meta/workflows/_fragments/event-emission-schema.md +73 -0
- package/dist/forge-payload/meta/workflows/_fragments/finalize.md +13 -0
- package/dist/forge-payload/meta/workflows/_fragments/friction-emit.md +73 -0
- package/dist/forge-payload/meta/workflows/_fragments/progress-reporting.md +38 -0
- package/dist/forge-payload/meta/workflows/_fragments/store-cli-verbs.md +39 -0
- package/dist/forge-payload/meta/workflows/meta-approve.md +119 -0
- package/dist/forge-payload/meta/workflows/meta-collate.md +89 -0
- package/dist/forge-payload/meta/workflows/meta-commit.md +93 -0
- package/dist/forge-payload/meta/workflows/meta-enhance.md +286 -0
- package/dist/forge-payload/meta/workflows/meta-fix-bug.md +501 -0
- package/dist/forge-payload/meta/workflows/meta-implement.md +132 -0
- package/dist/forge-payload/meta/workflows/meta-migrate.md +455 -0
- package/dist/forge-payload/meta/workflows/meta-orchestrate.md +993 -0
- package/dist/forge-payload/meta/workflows/meta-plan-task.md +133 -0
- package/dist/forge-payload/meta/workflows/meta-quiz-agent.md +135 -0
- package/dist/forge-payload/meta/workflows/meta-retrospective.md +65 -0
- package/dist/forge-payload/meta/workflows/meta-review-implementation.md +119 -0
- package/dist/forge-payload/meta/workflows/meta-review-plan.md +108 -0
- package/dist/forge-payload/meta/workflows/meta-review-sprint-completion.md +65 -0
- package/dist/forge-payload/meta/workflows/meta-sprint-intake.md +76 -0
- package/dist/forge-payload/meta/workflows/meta-sprint-plan.md +147 -0
- package/dist/forge-payload/meta/workflows/meta-update-implementation.md +76 -0
- package/dist/forge-payload/meta/workflows/meta-update-plan.md +76 -0
- package/dist/forge-payload/meta/workflows/meta-validate.md +111 -0
- package/dist/forge-payload/tools/check-structure.cjs +344 -0
- package/dist/forge-payload/tools/list-skills.js +76 -0
- package/dist/forge-payload/tools/store-cli.cjs +27 -1
- package/dist/forge-payload/tools/substitute-placeholders.cjs +60 -8
- package/dist/forge-payload/tools/verify-integrity.cjs +86 -0
- package/package.json +2 -2
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export interface MigrationEntry {
|
|
2
|
+
version: string;
|
|
3
|
+
date: string;
|
|
4
|
+
notes: string;
|
|
5
|
+
regenerate?: string[];
|
|
6
|
+
fileOps?: FileOp[];
|
|
7
|
+
breaking?: boolean;
|
|
8
|
+
manual?: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface FileOp {
|
|
11
|
+
op: "mkdir" | "copy" | "delete" | "substitute-placeholder";
|
|
12
|
+
path: string;
|
|
13
|
+
src?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface MigrationsJson {
|
|
16
|
+
[key: string]: MigrationEntry;
|
|
17
|
+
}
|
|
18
|
+
export interface MigrationResult {
|
|
19
|
+
applied: Array<{
|
|
20
|
+
fromVersion: string;
|
|
21
|
+
toVersion: string;
|
|
22
|
+
categories: string[];
|
|
23
|
+
}>;
|
|
24
|
+
skippedBreaking: Array<{
|
|
25
|
+
fromVersion: string;
|
|
26
|
+
toVersion: string;
|
|
27
|
+
reason: string;
|
|
28
|
+
}>;
|
|
29
|
+
manualSteps: Array<{
|
|
30
|
+
fromVersion: string;
|
|
31
|
+
toVersion: string;
|
|
32
|
+
steps: string[];
|
|
33
|
+
}>;
|
|
34
|
+
dryRun: boolean;
|
|
35
|
+
schemasRefreshed: string[];
|
|
36
|
+
forgeRootUpdated: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface RunMigrationsOptions {
|
|
39
|
+
/** Absolute path to the dist/forge-payload/ bundle root */
|
|
40
|
+
bundleRoot: string;
|
|
41
|
+
/** Absolute path to the project root (contains .forge/) */
|
|
42
|
+
projectRoot: string;
|
|
43
|
+
/** Version the user was running before the upgrade */
|
|
44
|
+
fromVersion: string;
|
|
45
|
+
/** Version the user just upgraded to */
|
|
46
|
+
toVersion: string;
|
|
47
|
+
/** When true, log actions without writing any files */
|
|
48
|
+
dryRun?: boolean;
|
|
49
|
+
}
|
|
50
|
+
interface EntryWithKey {
|
|
51
|
+
key: string;
|
|
52
|
+
entry: MigrationEntry;
|
|
53
|
+
}
|
|
54
|
+
/** Write-descriptor: source path + destination path for a file copy/write operation */
|
|
55
|
+
interface WriteDescriptor {
|
|
56
|
+
src: string;
|
|
57
|
+
dest: string;
|
|
58
|
+
content?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Compare two version strings using semver integer-component comparison.
|
|
62
|
+
* Returns: negative if a < b, 0 if a === b, positive if a > b.
|
|
63
|
+
*
|
|
64
|
+
* Unlike string comparison, this correctly handles 0.9.x vs 0.10.x boundaries.
|
|
65
|
+
* Strips leading "v" prefix (matches parseTriple() behavior in forge-update-command.ts:118).
|
|
66
|
+
* Falls back to localeCompare for invalid/non-semver inputs.
|
|
67
|
+
*/
|
|
68
|
+
export declare function semverCompare(a: string, b: string): number;
|
|
69
|
+
/**
|
|
70
|
+
* Filter migrations.json entries using [fromVersion, toVersion) semantics on keys.
|
|
71
|
+
*
|
|
72
|
+
* - fromVersion entry IS included: it represents the transition AWAY from that version.
|
|
73
|
+
* - toVersion entry is EXCLUDED: it would be a further transition past the target.
|
|
74
|
+
* - Results are sorted ascending by semver (oldest first).
|
|
75
|
+
*
|
|
76
|
+
* No first-run special case — this same filter applies for both first-run
|
|
77
|
+
* (empty ledger) and subsequent runs. The idempotency ledger handles re-run protection.
|
|
78
|
+
*/
|
|
79
|
+
export declare function filterMigrationEntries(migrations: MigrationsJson, fromVersion: string, toVersion: string): EntryWithKey[];
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a migration category string to one or more WriteDescriptors.
|
|
82
|
+
* Pure function — does not write files; appends to the provided writes array.
|
|
83
|
+
*
|
|
84
|
+
* ENOENT trap rule: if source doesn't exist, skip silently (never throw on ENOENT).
|
|
85
|
+
* Non-ENOENT IO errors propagate.
|
|
86
|
+
*
|
|
87
|
+
* Path-traversal defense: all output paths are validated against
|
|
88
|
+
* path.join(projectRoot, '.forge') before being added to writes.
|
|
89
|
+
*/
|
|
90
|
+
export declare function resolveCategory(category: string, bundleRoot: string, projectRoot: string, writes: WriteDescriptor[]): void;
|
|
91
|
+
/**
|
|
92
|
+
* Execute all migration entries between [fromVersion, toVersion) from the
|
|
93
|
+
* bundled migrations.json against the project's .forge/ directory.
|
|
94
|
+
*
|
|
95
|
+
* Design constraints:
|
|
96
|
+
* - No UI context (no ctx.ui calls).
|
|
97
|
+
* - No event emission (caller is responsible).
|
|
98
|
+
* - Pure deterministic engine: reads from bundleRoot, writes to projectRoot/.forge/.
|
|
99
|
+
* - Idempotent: already-applied versions (from .forge/applied-migrations.json) are skipped.
|
|
100
|
+
*
|
|
101
|
+
* Forward-compat: entries with non-empty fileOps[] use fileOps; otherwise regenerate.
|
|
102
|
+
* NOTE: 0 of 158 current entries use fileOps — the executor is dead code on day 1.
|
|
103
|
+
* Once a real fileOps entry lands, an integration test against that entry MUST be added.
|
|
104
|
+
*/
|
|
105
|
+
export declare function runMigrations(opts: RunMigrationsOptions): Promise<MigrationResult>;
|
|
106
|
+
export declare const __test__: {
|
|
107
|
+
semverCompare: typeof semverCompare;
|
|
108
|
+
filterMigrationEntries: typeof filterMigrationEntries;
|
|
109
|
+
resolveCategory: typeof resolveCategory;
|
|
110
|
+
};
|
|
111
|
+
export {};
|
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
// migration-engine.ts — Deterministic migration apply engine (FORGE-S23-T01)
|
|
2
|
+
//
|
|
3
|
+
// Reads migrations.json from the bundled payload, walks version entries between
|
|
4
|
+
// the given bounds using semver comparison [from, to) semantics, applies
|
|
5
|
+
// regeneration actions against the project's .forge/ directory, and maintains
|
|
6
|
+
// an idempotency ledger at .forge/applied-migrations.json.
|
|
7
|
+
//
|
|
8
|
+
// Design: pure engine — no pi runtime context, no UI calls, no event emission.
|
|
9
|
+
// The calling command handler (forge-update-command.ts) is responsible for
|
|
10
|
+
// event emission after this engine returns a MigrationResult.
|
|
11
|
+
//
|
|
12
|
+
// Layer: Layer 1 of the two-layer split (pure engine / command handler).
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
// ── semverCompare ─────────────────────────────────────────────────────────────
|
|
17
|
+
/**
|
|
18
|
+
* Compare two version strings using semver integer-component comparison.
|
|
19
|
+
* Returns: negative if a < b, 0 if a === b, positive if a > b.
|
|
20
|
+
*
|
|
21
|
+
* Unlike string comparison, this correctly handles 0.9.x vs 0.10.x boundaries.
|
|
22
|
+
* Strips leading "v" prefix (matches parseTriple() behavior in forge-update-command.ts:118).
|
|
23
|
+
* Falls back to localeCompare for invalid/non-semver inputs.
|
|
24
|
+
*/
|
|
25
|
+
export function semverCompare(a, b) {
|
|
26
|
+
const parse = (v) => {
|
|
27
|
+
const cleaned = v.startsWith("v") ? v.slice(1) : v; // Strip "v" prefix (parity with parseTriple())
|
|
28
|
+
const parts = cleaned.split(".");
|
|
29
|
+
if (parts.length !== 3)
|
|
30
|
+
return null;
|
|
31
|
+
const [major, minor, patch] = parts.map(Number);
|
|
32
|
+
if ([major, minor, patch].some((n) => Number.isNaN(n)))
|
|
33
|
+
return null;
|
|
34
|
+
return [major, minor, patch];
|
|
35
|
+
};
|
|
36
|
+
const pa = parse(a);
|
|
37
|
+
const pb = parse(b);
|
|
38
|
+
if (!pa || !pb)
|
|
39
|
+
return a.localeCompare(b); // Fallback for invalid format
|
|
40
|
+
for (let i = 0; i < 3; i++) {
|
|
41
|
+
if (pa[i] !== pb[i])
|
|
42
|
+
return pa[i] - pb[i];
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
// ── filterMigrationEntries ────────────────────────────────────────────────────
|
|
47
|
+
/**
|
|
48
|
+
* Filter migrations.json entries using [fromVersion, toVersion) semantics on keys.
|
|
49
|
+
*
|
|
50
|
+
* - fromVersion entry IS included: it represents the transition AWAY from that version.
|
|
51
|
+
* - toVersion entry is EXCLUDED: it would be a further transition past the target.
|
|
52
|
+
* - Results are sorted ascending by semver (oldest first).
|
|
53
|
+
*
|
|
54
|
+
* No first-run special case — this same filter applies for both first-run
|
|
55
|
+
* (empty ledger) and subsequent runs. The idempotency ledger handles re-run protection.
|
|
56
|
+
*/
|
|
57
|
+
export function filterMigrationEntries(migrations, fromVersion, toVersion) {
|
|
58
|
+
return Object.entries(migrations)
|
|
59
|
+
.filter(([key]) => {
|
|
60
|
+
return (semverCompare(key, fromVersion) >= 0 &&
|
|
61
|
+
semverCompare(key, toVersion) < 0);
|
|
62
|
+
})
|
|
63
|
+
.map(([key, entry]) => ({ key, entry }))
|
|
64
|
+
.sort((a, b) => semverCompare(a.key, b.key));
|
|
65
|
+
}
|
|
66
|
+
// ── resolveCategory ────────────────────────────────────────────────────────────
|
|
67
|
+
/**
|
|
68
|
+
* Resolve a migration category string to one or more WriteDescriptors.
|
|
69
|
+
* Pure function — does not write files; appends to the provided writes array.
|
|
70
|
+
*
|
|
71
|
+
* ENOENT trap rule: if source doesn't exist, skip silently (never throw on ENOENT).
|
|
72
|
+
* Non-ENOENT IO errors propagate.
|
|
73
|
+
*
|
|
74
|
+
* Path-traversal defense: all output paths are validated against
|
|
75
|
+
* path.join(projectRoot, '.forge') before being added to writes.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveCategory(category, bundleRoot, projectRoot, writes) {
|
|
78
|
+
const forgeDir = path.join(projectRoot, ".forge");
|
|
79
|
+
const basePack = path.join(bundleRoot, ".base-pack");
|
|
80
|
+
const schemas = path.join(bundleRoot, ".schemas");
|
|
81
|
+
function safeDest(rel) {
|
|
82
|
+
const resolved = path.join(forgeDir, rel);
|
|
83
|
+
const safePrefix = forgeDir + path.sep;
|
|
84
|
+
if (!resolved.startsWith(safePrefix)) {
|
|
85
|
+
throw new Error(`Path traversal attempt: resolved output path '${resolved}' is outside .forge/`);
|
|
86
|
+
}
|
|
87
|
+
return resolved;
|
|
88
|
+
}
|
|
89
|
+
function tryAdd(src, dest) {
|
|
90
|
+
try {
|
|
91
|
+
const stat = fs.statSync(src);
|
|
92
|
+
if (stat.isFile()) {
|
|
93
|
+
writes.push({ src, dest });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (err.code !== "ENOENT")
|
|
98
|
+
throw err;
|
|
99
|
+
// ENOENT — skip silently
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ── Schema categories ───────────────────────────────────────────────────
|
|
103
|
+
if (category === "schemas") {
|
|
104
|
+
// Bare schemas — copy all *.schema.json from bundle/.schemas/
|
|
105
|
+
try {
|
|
106
|
+
const files = fs.readdirSync(schemas).filter((f) => f.endsWith(".schema.json"));
|
|
107
|
+
for (const f of files) {
|
|
108
|
+
writes.push({
|
|
109
|
+
src: path.join(schemas, f),
|
|
110
|
+
dest: safeDest(path.join("schemas", f)),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
if (err.code !== "ENOENT")
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (category.startsWith("schemas:")) {
|
|
121
|
+
const name = category.slice("schemas:".length);
|
|
122
|
+
// Path-traversal defense on the name itself
|
|
123
|
+
if (name.includes("..") || name.includes("/") || name.includes("\\")) {
|
|
124
|
+
// Validate safe destination will catch it — but throw explicitly for clarity
|
|
125
|
+
const dest = path.join(forgeDir, "schemas", `${name}.schema.json`);
|
|
126
|
+
const safePrefix = forgeDir + path.sep;
|
|
127
|
+
if (!dest.startsWith(safePrefix)) {
|
|
128
|
+
throw new Error(`Path traversal attempt: schemas category '${category}'`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Special case: structure-manifest (non-.schema.json file)
|
|
132
|
+
if (name === "structure-manifest") {
|
|
133
|
+
// Probe .schema.json first, fall back to .json
|
|
134
|
+
const srcSchema = path.join(schemas, `${name}.schema.json`);
|
|
135
|
+
const srcJson = path.join(schemas, `${name}.json`);
|
|
136
|
+
const dest = safeDest(path.join("schemas", `${name}.json`));
|
|
137
|
+
if (fs.existsSync(srcSchema)) {
|
|
138
|
+
writes.push({ src: srcSchema, dest });
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
tryAdd(srcJson, dest);
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Standard schema: <name>.schema.json
|
|
146
|
+
tryAdd(path.join(schemas, `${name}.schema.json`), safeDest(path.join("schemas", `${name}.schema.json`)));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// ── hooks:* short-circuit ───────────────────────────────────────────────
|
|
150
|
+
if (category === "hooks" || category.startsWith("hooks:")) {
|
|
151
|
+
// No source directory in bundle; short-circuit before any fs call
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// ── tools:* short-circuit (excluding tools:lib:*) ──────────────────────
|
|
155
|
+
if (category === "tools" || (category.startsWith("tools:") && !category.startsWith("tools:lib"))) {
|
|
156
|
+
// paths.forgeRoot is updated by substitute-placeholders, not by this engine directly.
|
|
157
|
+
// No file operations for tools:* categories.
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// ── tools:lib/<name> and tools:lib:<name> ──────────────────────────────
|
|
161
|
+
if (category.startsWith("tools:lib")) {
|
|
162
|
+
const rest = category.slice("tools:lib".length);
|
|
163
|
+
// Normalize separator: tools:lib/forge-root or tools:lib:validate → name
|
|
164
|
+
const name = rest.startsWith("/") ? rest.slice(1) : rest.startsWith(":") ? rest.slice(1) : rest;
|
|
165
|
+
const libSrc = path.join(bundleRoot, "tools", "lib");
|
|
166
|
+
// tools:lib files are forge-cli internal tools (bundled in dist/forge-payload/tools/lib/).
|
|
167
|
+
// They serve the forge-cli extension itself, not the user's .forge/ project tree.
|
|
168
|
+
// This engine can only write to projectRoot/.forge/ — it cannot update the installed
|
|
169
|
+
// forge-cli tools. The migration engine records which source files exist (for tracking),
|
|
170
|
+
// but cannot write them to a meaningful location within .forge/.
|
|
171
|
+
//
|
|
172
|
+
// Probe .cjs first, then .js (matching LIB_ALLOWLIST entry patterns).
|
|
173
|
+
// For default-payload installs: forge-root.cjs, paths.cjs, validate.js ARE present.
|
|
174
|
+
// ENOENT on both: graceful skip (no throw).
|
|
175
|
+
const srcCjs = path.join(libSrc, `${name}.cjs`);
|
|
176
|
+
const srcJs = path.join(libSrc, `${name}.js`);
|
|
177
|
+
const resolvedSrc = fs.existsSync(srcCjs) ? srcCjs : fs.existsSync(srcJs) ? srcJs : null;
|
|
178
|
+
if (resolvedSrc) {
|
|
179
|
+
// Record as informational write to a .forge/schemas/ tracking file.
|
|
180
|
+
// This is a best-effort record — the actual lib file update requires npm reinstall.
|
|
181
|
+
writes.push({
|
|
182
|
+
src: resolvedSrc,
|
|
183
|
+
dest: safeDest(path.join("schemas", `.tools-lib-${name}.marker`)),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// ENOENT on both: graceful skip
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// ── fragments:* and events:* (convention aliases) ──────────────────────
|
|
190
|
+
if (category.startsWith("fragments:") || category.startsWith("events:")) {
|
|
191
|
+
const prefix = category.startsWith("fragments:") ? "fragments:" : "events:";
|
|
192
|
+
const name = category.slice(prefix.length);
|
|
193
|
+
tryAdd(path.join(basePack, "workflows", "_fragments", `${name}.md`), safeDest(path.join("workflows", "_fragments", `${name}.md`)));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
// ── workflows:_fragments (bare-directory token) ─────────────────────────
|
|
197
|
+
if (category === "workflows:_fragments") {
|
|
198
|
+
const fragDir = path.join(basePack, "workflows", "_fragments");
|
|
199
|
+
try {
|
|
200
|
+
const entries = fs.readdirSync(fragDir);
|
|
201
|
+
for (const entry of entries) {
|
|
202
|
+
// Nesting guard: do NOT recurse into a subdirectory named _fragments/
|
|
203
|
+
if (entry === "_fragments")
|
|
204
|
+
continue;
|
|
205
|
+
const fullSrc = path.join(fragDir, entry);
|
|
206
|
+
try {
|
|
207
|
+
const st = fs.statSync(fullSrc);
|
|
208
|
+
if (st.isFile()) {
|
|
209
|
+
writes.push({
|
|
210
|
+
src: fullSrc,
|
|
211
|
+
dest: safeDest(path.join("workflows", "_fragments", entry)),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// Skip subdirectories (nesting guard above handles _fragments/)
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
if (err.code !== "ENOENT")
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
if (err.code !== "ENOENT")
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// ── workflows:_fragments_<name> (underscore compound) ──────────────────
|
|
229
|
+
if (category.startsWith("workflows:_fragments_")) {
|
|
230
|
+
const name = category.slice("workflows:_fragments_".length);
|
|
231
|
+
tryAdd(path.join(basePack, "workflows", "_fragments", `${name}.md`), safeDest(path.join("workflows", "_fragments", `${name}.md`)));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
// ── Base-pack categories ────────────────────────────────────────────────
|
|
235
|
+
const VALID_BASE_PACK_TYPES = ["personas", "workflows", "skills", "templates", "commands"];
|
|
236
|
+
const colonIdx = category.indexOf(":");
|
|
237
|
+
const typeStr = colonIdx >= 0 ? category.slice(0, colonIdx) : category;
|
|
238
|
+
const subTarget = colonIdx >= 0 ? category.slice(colonIdx + 1) : null;
|
|
239
|
+
if (!VALID_BASE_PACK_TYPES.includes(typeStr)) {
|
|
240
|
+
// Unknown category — log at debug level and skip
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const type = typeStr;
|
|
244
|
+
if (subTarget === null) {
|
|
245
|
+
// Bare category — walk the entire subdirectory
|
|
246
|
+
const srcDir = path.join(basePack, type);
|
|
247
|
+
try {
|
|
248
|
+
const files = fs.readdirSync(srcDir).filter((f) => f.endsWith(".md"));
|
|
249
|
+
for (const f of files) {
|
|
250
|
+
const dest = type === "skills"
|
|
251
|
+
? safeDest(path.join(type, f)) // skills files already have -skills.md suffix
|
|
252
|
+
: type === "commands"
|
|
253
|
+
? safeDest(path.join("commands", "forge", f))
|
|
254
|
+
: safeDest(path.join(type, f));
|
|
255
|
+
tryAdd(path.join(srcDir, f), dest);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
if (err.code !== "ENOENT")
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
// Special case: workflows:base-pack-store-cli-form — explicit no-op
|
|
265
|
+
if (type === "workflows" && subTarget === "base-pack-store-cli-form") {
|
|
266
|
+
// Sub-target doesn't exist in base-pack; debug-logged no-op
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// Sub-target category: <type>:<name>
|
|
270
|
+
const filename = type === "skills"
|
|
271
|
+
? `${subTarget}-skills.md` // skills:<name> → <name>-skills.md
|
|
272
|
+
: `${subTarget}.md`;
|
|
273
|
+
const dest = type === "commands"
|
|
274
|
+
? safeDest(path.join("commands", "forge", filename))
|
|
275
|
+
: safeDest(path.join(type, filename));
|
|
276
|
+
tryAdd(path.join(basePack, type, filename), dest);
|
|
277
|
+
}
|
|
278
|
+
// ── executeFileOps ─────────────────────────────────────────────────────────────
|
|
279
|
+
function executeFileOps(fileOps, projectRoot, dryRun) {
|
|
280
|
+
const forgeDir = path.join(projectRoot, ".forge");
|
|
281
|
+
const safePrefix = forgeDir + path.sep;
|
|
282
|
+
const categories = [];
|
|
283
|
+
for (const op of fileOps) {
|
|
284
|
+
const dest = path.resolve(projectRoot, op.path);
|
|
285
|
+
if (!dest.startsWith(safePrefix) && !dest.startsWith(forgeDir)) {
|
|
286
|
+
throw new Error(`Path traversal attempt in fileOps: '${op.path}'`);
|
|
287
|
+
}
|
|
288
|
+
categories.push(`fileOps:${op.op}`);
|
|
289
|
+
if (dryRun)
|
|
290
|
+
continue;
|
|
291
|
+
switch (op.op) {
|
|
292
|
+
case "mkdir":
|
|
293
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
294
|
+
break;
|
|
295
|
+
case "copy":
|
|
296
|
+
if (op.src) {
|
|
297
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
298
|
+
try {
|
|
299
|
+
fs.copyFileSync(op.src, dest);
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
if (err.code !== "ENOENT")
|
|
303
|
+
throw err;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
case "delete":
|
|
308
|
+
try {
|
|
309
|
+
fs.rmSync(dest, { force: true });
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
if (err.code !== "ENOENT")
|
|
313
|
+
throw err;
|
|
314
|
+
}
|
|
315
|
+
break;
|
|
316
|
+
case "substitute-placeholder":
|
|
317
|
+
// Forward-compat: just copy for now (no ctx available in engine)
|
|
318
|
+
if (op.src) {
|
|
319
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
320
|
+
try {
|
|
321
|
+
fs.copyFileSync(op.src, dest);
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
if (err.code !== "ENOENT")
|
|
325
|
+
throw err;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return categories;
|
|
332
|
+
}
|
|
333
|
+
// ── Ledger helpers ────────────────────────────────────────────────────────────
|
|
334
|
+
function readLedger(projectRoot) {
|
|
335
|
+
const ledgerPath = path.join(projectRoot, ".forge", "applied-migrations.json");
|
|
336
|
+
try {
|
|
337
|
+
const raw = fs.readFileSync(ledgerPath, "utf8");
|
|
338
|
+
return JSON.parse(raw);
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return { schemaVersion: 1, appliedVersions: [] };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function writeLedger(projectRoot, ledger) {
|
|
345
|
+
const ledgerPath = path.join(projectRoot, ".forge", "applied-migrations.json");
|
|
346
|
+
fs.writeFileSync(ledgerPath, JSON.stringify(ledger, null, 2), "utf8");
|
|
347
|
+
}
|
|
348
|
+
// ── runMigrations ─────────────────────────────────────────────────────────────
|
|
349
|
+
/**
|
|
350
|
+
* Execute all migration entries between [fromVersion, toVersion) from the
|
|
351
|
+
* bundled migrations.json against the project's .forge/ directory.
|
|
352
|
+
*
|
|
353
|
+
* Design constraints:
|
|
354
|
+
* - No UI context (no ctx.ui calls).
|
|
355
|
+
* - No event emission (caller is responsible).
|
|
356
|
+
* - Pure deterministic engine: reads from bundleRoot, writes to projectRoot/.forge/.
|
|
357
|
+
* - Idempotent: already-applied versions (from .forge/applied-migrations.json) are skipped.
|
|
358
|
+
*
|
|
359
|
+
* Forward-compat: entries with non-empty fileOps[] use fileOps; otherwise regenerate.
|
|
360
|
+
* NOTE: 0 of 158 current entries use fileOps — the executor is dead code on day 1.
|
|
361
|
+
* Once a real fileOps entry lands, an integration test against that entry MUST be added.
|
|
362
|
+
*/
|
|
363
|
+
export async function runMigrations(opts) {
|
|
364
|
+
const { bundleRoot, projectRoot, fromVersion, toVersion, dryRun = false } = opts;
|
|
365
|
+
const result = {
|
|
366
|
+
applied: [],
|
|
367
|
+
skippedBreaking: [],
|
|
368
|
+
manualSteps: [],
|
|
369
|
+
dryRun,
|
|
370
|
+
schemasRefreshed: [],
|
|
371
|
+
forgeRootUpdated: false,
|
|
372
|
+
};
|
|
373
|
+
// Read migrations.json from bundle
|
|
374
|
+
const migrationsPath = path.join(bundleRoot, ".schemas", "migrations.json");
|
|
375
|
+
let migrations;
|
|
376
|
+
try {
|
|
377
|
+
const raw = fs.readFileSync(migrationsPath, "utf8");
|
|
378
|
+
migrations = JSON.parse(raw);
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
if (err.code === "ENOENT") {
|
|
382
|
+
// migrations.json not in bundle — no migrations to apply
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
throw err;
|
|
386
|
+
}
|
|
387
|
+
// Filter entries using [from, to) semantics
|
|
388
|
+
const entries = filterMigrationEntries(migrations, fromVersion, toVersion);
|
|
389
|
+
if (entries.length === 0) {
|
|
390
|
+
// No-op: empty range (from == to) or no entries in range
|
|
391
|
+
return result;
|
|
392
|
+
}
|
|
393
|
+
// Read idempotency ledger
|
|
394
|
+
const ledger = readLedger(projectRoot);
|
|
395
|
+
// Build substitution map ONCE using config from .forge/config.json
|
|
396
|
+
let buildSubstitutionMap = null;
|
|
397
|
+
let substituteFile = null;
|
|
398
|
+
try {
|
|
399
|
+
const substPath = path.join(bundleRoot, "tools", "substitute-placeholders.cjs");
|
|
400
|
+
const _require = createRequire(import.meta.url);
|
|
401
|
+
const substModule = _require(substPath);
|
|
402
|
+
buildSubstitutionMap = substModule.buildSubstitutionMap;
|
|
403
|
+
substituteFile = substModule.substituteFile;
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// substitute-placeholders not available (testing or minimal bundle)
|
|
407
|
+
// Fall back to identity substitution
|
|
408
|
+
}
|
|
409
|
+
let substitutionMap = null;
|
|
410
|
+
if (buildSubstitutionMap) {
|
|
411
|
+
try {
|
|
412
|
+
const configPath = path.join(projectRoot, ".forge", "config.json");
|
|
413
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
414
|
+
substitutionMap = buildSubstitutionMap(config);
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
// Config unreadable — proceed without substitution
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// Process each entry in ascending semver order
|
|
421
|
+
for (const { key, entry } of entries) {
|
|
422
|
+
// Skip if already applied (idempotency)
|
|
423
|
+
if (ledger.appliedVersions.includes(key)) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
// Skip breaking entries
|
|
427
|
+
if (entry.breaking) {
|
|
428
|
+
result.skippedBreaking.push({
|
|
429
|
+
fromVersion: key,
|
|
430
|
+
toVersion: entry.version,
|
|
431
|
+
reason: `breaking: true — manual intervention required. Notes: ${entry.notes}`,
|
|
432
|
+
});
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
// Collect manual steps
|
|
436
|
+
if (entry.manual && entry.manual.length > 0) {
|
|
437
|
+
result.manualSteps.push({
|
|
438
|
+
fromVersion: key,
|
|
439
|
+
toVersion: entry.version,
|
|
440
|
+
steps: entry.manual,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
let appliedCategories = [];
|
|
444
|
+
// fileOps takes priority over regenerate when non-empty
|
|
445
|
+
// NOTE: 0 of 158 current entries use fileOps (dead code on day 1).
|
|
446
|
+
// When a real fileOps entry lands, add an integration test against it.
|
|
447
|
+
if (entry.fileOps && entry.fileOps.length > 0) {
|
|
448
|
+
appliedCategories = executeFileOps(entry.fileOps, projectRoot, dryRun);
|
|
449
|
+
}
|
|
450
|
+
else if (entry.regenerate && entry.regenerate.length > 0) {
|
|
451
|
+
// Resolve categories to write descriptors
|
|
452
|
+
const writes = [];
|
|
453
|
+
for (const category of entry.regenerate) {
|
|
454
|
+
resolveCategory(category, bundleRoot, projectRoot, writes);
|
|
455
|
+
}
|
|
456
|
+
// Execute writes (unless dry-run)
|
|
457
|
+
if (!dryRun) {
|
|
458
|
+
for (const { src, dest } of writes) {
|
|
459
|
+
try {
|
|
460
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
461
|
+
// Apply placeholder substitution for base-pack files
|
|
462
|
+
if (substituteFile &&
|
|
463
|
+
substitutionMap &&
|
|
464
|
+
src.includes(path.join(".base-pack")) &&
|
|
465
|
+
dest.endsWith(".md")) {
|
|
466
|
+
const content = fs.readFileSync(src, "utf8");
|
|
467
|
+
const substituted = substituteFile(content, substitutionMap);
|
|
468
|
+
fs.writeFileSync(dest, substituted, "utf8");
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
fs.copyFileSync(src, dest);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch (err) {
|
|
475
|
+
if (err.code !== "ENOENT")
|
|
476
|
+
throw err;
|
|
477
|
+
// ENOENT on source — skip silently (source may be absent in this build)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
appliedCategories = entry.regenerate;
|
|
482
|
+
}
|
|
483
|
+
result.applied.push({
|
|
484
|
+
fromVersion: key,
|
|
485
|
+
toVersion: entry.version,
|
|
486
|
+
categories: appliedCategories,
|
|
487
|
+
});
|
|
488
|
+
// Update ledger after successful application
|
|
489
|
+
if (!dryRun) {
|
|
490
|
+
ledger.appliedVersions.push(key);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// Write updated ledger
|
|
494
|
+
if (!dryRun && result.applied.length > 0) {
|
|
495
|
+
writeLedger(projectRoot, ledger);
|
|
496
|
+
}
|
|
497
|
+
// Always-on schema refresh post-pass: copy all *.schema.json to .forge/schemas/
|
|
498
|
+
// Runs regardless of whether any entry had a schemas category.
|
|
499
|
+
// This is an always-overwrite safety net (schemas are not user-edited).
|
|
500
|
+
const schemasDir = path.join(bundleRoot, ".schemas");
|
|
501
|
+
const forgeSchemasDir = path.join(projectRoot, ".forge", "schemas");
|
|
502
|
+
try {
|
|
503
|
+
const schemaFiles = fs.readdirSync(schemasDir).filter((f) => f.endsWith(".schema.json"));
|
|
504
|
+
if (!dryRun) {
|
|
505
|
+
fs.mkdirSync(forgeSchemasDir, { recursive: true });
|
|
506
|
+
for (const f of schemaFiles) {
|
|
507
|
+
try {
|
|
508
|
+
fs.copyFileSync(path.join(schemasDir, f), path.join(forgeSchemasDir, f));
|
|
509
|
+
result.schemasRefreshed.push(f);
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
if (err.code !== "ENOENT")
|
|
513
|
+
throw err;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
result.schemasRefreshed = schemaFiles;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
if (err.code !== "ENOENT")
|
|
523
|
+
throw err;
|
|
524
|
+
}
|
|
525
|
+
return result;
|
|
526
|
+
}
|
|
527
|
+
// ── Test helpers (exported for test access) ───────────────────────────────────
|
|
528
|
+
export const __test__ = {
|
|
529
|
+
semverCompare,
|
|
530
|
+
filterMigrationEntries,
|
|
531
|
+
resolveCategory,
|
|
532
|
+
};
|
|
533
|
+
//# sourceMappingURL=migration-engine.js.map
|