@shrkcrft/migrate 0.1.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ import { type IMigration } from '../schema/migration.js';
2
+ /**
3
+ * Type-safe builder for migration definitions. Validates basic shape
4
+ * (no empty id / title / steps); deeper validation happens at plan /
5
+ * apply time when the steps are evaluated.
6
+ */
7
+ export declare function defineMigration(input: Omit<IMigration, 'schema'>): IMigration;
8
+ //# sourceMappingURL=define-migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-migration.d.ts","sourceRoot":"","sources":["../../src/engine/define-migration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAE3E;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,GAAG,UAAU,CAO7E"}
@@ -0,0 +1,16 @@
1
+ import { MIGRATION_SCHEMA } from "../schema/migration.js";
2
+ /**
3
+ * Type-safe builder for migration definitions. Validates basic shape
4
+ * (no empty id / title / steps); deeper validation happens at plan /
5
+ * apply time when the steps are evaluated.
6
+ */
7
+ export function defineMigration(input) {
8
+ if (!input.id)
9
+ throw new Error('defineMigration: id is required');
10
+ if (!input.title)
11
+ throw new Error('defineMigration: title is required');
12
+ if (!input.steps || input.steps.length === 0) {
13
+ throw new Error('defineMigration: steps must be non-empty');
14
+ }
15
+ return { schema: MIGRATION_SCHEMA, ...input };
16
+ }
@@ -0,0 +1,32 @@
1
+ import { type IRewritePlan } from '@shrkcrft/structural-search';
2
+ import type { IMigration, MigrationStep } from '../schema/migration.js';
3
+ export interface IPlannedStep {
4
+ index: number;
5
+ id: string;
6
+ description?: string;
7
+ step: MigrationStep;
8
+ /** For structural-rewrite steps, the computed plan (dry). */
9
+ rewritePlan?: IRewritePlan;
10
+ }
11
+ export interface IMigrationPlan {
12
+ schema: 'sharkcraft.migration-plan/v1';
13
+ migration: {
14
+ id: string;
15
+ title: string;
16
+ };
17
+ plannedSteps: readonly IPlannedStep[];
18
+ /** Sum of (rewritePlan.totalEdits) across all rewrite steps. */
19
+ totalEdits: number;
20
+ /** Sum of (rewritePlan.files.length) across all rewrite steps. */
21
+ totalFiles: number;
22
+ }
23
+ /**
24
+ * Compute the migration's plan without writing anything.
25
+ *
26
+ * `structural-rewrite` steps are pre-computed via `planRewrite` so the
27
+ * caller can preview file-level edits before deciding to apply.
28
+ * `shell` / `check` steps are listed as-is — they're executed only
29
+ * during `applyMigration`.
30
+ */
31
+ export declare function planMigration(migration: IMigration, projectRoot: string): IMigrationPlan;
32
+ //# sourceMappingURL=plan-migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-migration.d.ts","sourceRoot":"","sources":["../../src/engine/plan-migration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC7E,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAExE,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,aAAa,CAAC;IACpB,6DAA6D;IAC7D,WAAW,CAAC,EAAE,YAAY,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,8BAA8B,CAAC;IACvC,SAAS,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,YAAY,EAAE,SAAS,YAAY,EAAE,CAAC;IACtC,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAG,cAAc,CAsCxF"}
@@ -0,0 +1,49 @@
1
+ import { planRewrite } from '@shrkcrft/structural-search';
2
+ /**
3
+ * Compute the migration's plan without writing anything.
4
+ *
5
+ * `structural-rewrite` steps are pre-computed via `planRewrite` so the
6
+ * caller can preview file-level edits before deciding to apply.
7
+ * `shell` / `check` steps are listed as-is — they're executed only
8
+ * during `applyMigration`.
9
+ */
10
+ export function planMigration(migration, projectRoot) {
11
+ const plannedSteps = [];
12
+ let totalEdits = 0;
13
+ let totalFiles = 0;
14
+ for (let i = 0; i < migration.steps.length; i += 1) {
15
+ const step = migration.steps[i];
16
+ const id = step.id ?? `step-${i + 1}`;
17
+ if (step.kind === 'structural-rewrite') {
18
+ const rewritePlan = planRewrite({
19
+ projectRoot,
20
+ pattern: step.pattern,
21
+ recipe: step.recipe,
22
+ });
23
+ totalEdits += rewritePlan.totalEdits;
24
+ totalFiles += rewritePlan.files.length;
25
+ plannedSteps.push({
26
+ index: i,
27
+ id,
28
+ ...(step.description ? { description: step.description } : {}),
29
+ step,
30
+ rewritePlan,
31
+ });
32
+ }
33
+ else {
34
+ plannedSteps.push({
35
+ index: i,
36
+ id,
37
+ ...(step.description ? { description: step.description } : {}),
38
+ step,
39
+ });
40
+ }
41
+ }
42
+ return {
43
+ schema: 'sharkcraft.migration-plan/v1',
44
+ migration: { id: migration.id, title: migration.title },
45
+ plannedSteps,
46
+ totalEdits,
47
+ totalFiles,
48
+ };
49
+ }
@@ -0,0 +1,8 @@
1
+ export * from './schema/migration.js';
2
+ export * from './engine/define-migration.js';
3
+ export * from './engine/plan-migration.js';
4
+ export * from './runner/apply-migration.js';
5
+ export * from './runner/state-store.js';
6
+ export * from './runner/resume-migration.js';
7
+ export * from './runner/prune-migrations.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,yBAAyB,CAAC;AACxC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,8BAA8B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export * from "./schema/migration.js";
2
+ export * from "./engine/define-migration.js";
3
+ export * from "./engine/plan-migration.js";
4
+ export * from "./runner/apply-migration.js";
5
+ export * from "./runner/state-store.js";
6
+ export * from "./runner/resume-migration.js";
7
+ export * from "./runner/prune-migrations.js";
@@ -0,0 +1,37 @@
1
+ import { type IMigration, type IMigrationRunReport, type IStepRunResult } from '../schema/migration.js';
2
+ export interface IApplyMigrationOptions {
3
+ projectRoot: string;
4
+ /** When true, structural rewrites compute but don't write. Shell and
5
+ * check steps are still skipped (not executed). Default false. */
6
+ dryRun?: boolean;
7
+ /** When true, the runner stops at the first failed step. Default true. */
8
+ stopOnFailure?: boolean;
9
+ /** Per-shell timeout in ms. Default 5 * 60 * 1000. */
10
+ shellTimeoutMs?: number;
11
+ /**
12
+ * When true, persist a checkpoint after each step (and the final
13
+ * report) to `.sharkcraft/migrations/<id>.state.json` so
14
+ * `resumeMigration` can pick up where `apply` left off. Default true
15
+ * for non-dry-run, false for dry-run.
16
+ */
17
+ persistCheckpoints?: boolean;
18
+ /**
19
+ * Index of the step to start at. Steps before this index are
20
+ * carried over from `priorSteps` with status `applied`. Used by
21
+ * `resumeMigration`; callers don't typically set this directly.
22
+ */
23
+ resumeFromIndex?: number;
24
+ /** Step results carried over from a prior run (when resuming). */
25
+ priorSteps?: readonly IStepRunResult[];
26
+ }
27
+ /**
28
+ * Execute a migration end-to-end. Returns a structured run report;
29
+ * never throws (errors are captured in per-step `status: 'failed'`).
30
+ *
31
+ * `structural-rewrite` steps go through `planRewrite` + `applyRewritePlan`
32
+ * (or stop at plan when `dryRun: true`). `shell` / `check` steps run
33
+ * via `spawnSync(bash -c, ...)`. A `check` step that exits non-zero
34
+ * marks the step as `failed` and (by default) halts the run.
35
+ */
36
+ export declare function applyMigration(migration: IMigration, options: IApplyMigrationOptions): IMigrationRunReport;
37
+ //# sourceMappingURL=apply-migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-migration.d.ts","sourceRoot":"","sources":["../../src/runner/apply-migration.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACpB,MAAM,wBAAwB,CAAC;AAGhC,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB;sEACkE;IAClE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,sDAAsD;IACtD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,UAAU,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;CACxC;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,UAAU,EACrB,OAAO,EAAE,sBAAsB,GAC9B,mBAAmB,CAqIrB"}
@@ -0,0 +1,169 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import * as nodePath from 'node:path';
3
+ import { applyRewritePlan, planRewrite } from '@shrkcrft/structural-search';
4
+ import { MIGRATION_RUN_SCHEMA, } from "../schema/migration.js";
5
+ import { MigrationStateStore } from "./state-store.js";
6
+ /**
7
+ * Execute a migration end-to-end. Returns a structured run report;
8
+ * never throws (errors are captured in per-step `status: 'failed'`).
9
+ *
10
+ * `structural-rewrite` steps go through `planRewrite` + `applyRewritePlan`
11
+ * (or stop at plan when `dryRun: true`). `shell` / `check` steps run
12
+ * via `spawnSync(bash -c, ...)`. A `check` step that exits non-zero
13
+ * marks the step as `failed` and (by default) halts the run.
14
+ */
15
+ export function applyMigration(migration, options) {
16
+ const startedAt = new Date().toISOString();
17
+ const start = Date.now();
18
+ const dryRun = options.dryRun ?? false;
19
+ const stopOnFailure = options.stopOnFailure ?? true;
20
+ const shellTimeoutMs = options.shellTimeoutMs ?? 5 * 60 * 1000;
21
+ const persist = options.persistCheckpoints ?? !dryRun;
22
+ const resumeFromIndex = options.resumeFromIndex ?? 0;
23
+ const priorSteps = options.priorSteps ?? [];
24
+ const store = persist ? new MigrationStateStore(options.projectRoot) : undefined;
25
+ const results = [];
26
+ let halted = false;
27
+ // Carry forward prior successful steps when resuming.
28
+ if (resumeFromIndex > 0 && priorSteps.length > 0) {
29
+ for (const p of priorSteps) {
30
+ if (p.index >= resumeFromIndex)
31
+ break;
32
+ // Re-stamp status as `applied` (in case the prior run halted
33
+ // BEFORE this step's status was updated — shouldn't happen with
34
+ // checkpoints but be defensive).
35
+ results.push(p.status === 'applied' || p.status === 'planned' || p.status === 'skipped'
36
+ ? p
37
+ : { ...p, status: 'applied' });
38
+ }
39
+ }
40
+ for (let i = resumeFromIndex; i < migration.steps.length; i += 1) {
41
+ const step = migration.steps[i];
42
+ const id = step.id ?? `step-${i + 1}`;
43
+ if (halted) {
44
+ results.push({
45
+ index: i,
46
+ id,
47
+ kind: step.kind,
48
+ status: 'skipped',
49
+ message: 'skipped after previous failure',
50
+ durationMs: 0,
51
+ diagnostics: [],
52
+ });
53
+ continue;
54
+ }
55
+ const stepStart = Date.now();
56
+ if (step.kind === 'structural-rewrite') {
57
+ const plan = planRewrite({
58
+ projectRoot: options.projectRoot,
59
+ pattern: step.pattern,
60
+ recipe: step.recipe,
61
+ });
62
+ const apply = applyRewritePlan(plan, { projectRoot: options.projectRoot, dryRun });
63
+ const failed = apply.conflicts.length > 0;
64
+ results.push({
65
+ index: i,
66
+ id,
67
+ kind: step.kind,
68
+ status: failed ? 'failed' : (dryRun ? 'planned' : 'applied'),
69
+ message: failed
70
+ ? `${apply.conflicts.length} conflict(s) — file content drifted`
71
+ : `${apply.filesChanged} file(s) ${dryRun ? 'would be ' : ''}changed, ${plan.totalEdits} edit(s)`,
72
+ durationMs: Date.now() - stepStart,
73
+ rewriteStats: {
74
+ filesScanned: plan.filesScanned,
75
+ filesAttempted: apply.filesAttempted,
76
+ filesChanged: apply.filesChanged,
77
+ totalEdits: plan.totalEdits,
78
+ conflicts: apply.conflicts,
79
+ },
80
+ diagnostics: [...plan.diagnostics, ...apply.diagnostics],
81
+ });
82
+ if (failed && stopOnFailure)
83
+ halted = true;
84
+ }
85
+ else if (step.kind === 'shell' || step.kind === 'check') {
86
+ if (dryRun) {
87
+ results.push({
88
+ index: i,
89
+ id,
90
+ kind: step.kind,
91
+ status: 'planned',
92
+ message: `${step.kind} (dry-run): ${step.command}`,
93
+ durationMs: Date.now() - stepStart,
94
+ diagnostics: [],
95
+ });
96
+ continue;
97
+ }
98
+ const cwd = step.cwd
99
+ ? nodePath.resolve(options.projectRoot, step.cwd)
100
+ : options.projectRoot;
101
+ const res = spawnSync('bash', ['-c', step.command], {
102
+ cwd,
103
+ encoding: 'utf8',
104
+ timeout: shellTimeoutMs,
105
+ });
106
+ const exitCode = res.status ?? -1;
107
+ const failed = exitCode !== 0;
108
+ results.push({
109
+ index: i,
110
+ id,
111
+ kind: step.kind,
112
+ status: failed ? 'failed' : 'applied',
113
+ message: failed
114
+ ? `exit ${exitCode}: ${oneLine(res.stderr ?? '') || step.command}`
115
+ : `exit 0: ${step.command}`,
116
+ durationMs: Date.now() - stepStart,
117
+ shellOutput: {
118
+ exitCode,
119
+ stdout: (res.stdout ?? '').slice(0, 8000),
120
+ stderr: (res.stderr ?? '').slice(0, 8000),
121
+ },
122
+ diagnostics: [],
123
+ });
124
+ if (step.kind === 'check' && failed && stopOnFailure)
125
+ halted = true;
126
+ }
127
+ if (store) {
128
+ // Per-step checkpoint so a crashing runner / killed process can
129
+ // still be resumed from the right point.
130
+ store.write(migration.id, buildPartialReport(migration, dryRun, startedAt, start, results));
131
+ }
132
+ }
133
+ const overall = results.some((r) => r.status === 'failed')
134
+ ? 'fail'
135
+ : results.some((r) => r.status === 'applied' || r.status === 'planned')
136
+ ? 'pass'
137
+ : 'skipped';
138
+ const report = {
139
+ schema: MIGRATION_RUN_SCHEMA,
140
+ migration: { id: migration.id, title: migration.title },
141
+ dryRun,
142
+ startedAt,
143
+ totalDurationMs: Date.now() - start,
144
+ overall,
145
+ steps: results,
146
+ };
147
+ if (store)
148
+ store.write(migration.id, report);
149
+ return report;
150
+ }
151
+ function oneLine(s) {
152
+ return s.replace(/\s+/g, ' ').trim().slice(0, 160);
153
+ }
154
+ function buildPartialReport(migration, dryRun, startedAt, start, results) {
155
+ const overall = results.some((r) => r.status === 'failed')
156
+ ? 'fail'
157
+ : results.some((r) => r.status === 'applied' || r.status === 'planned')
158
+ ? 'pass'
159
+ : 'skipped';
160
+ return {
161
+ schema: MIGRATION_RUN_SCHEMA,
162
+ migration: { id: migration.id, title: migration.title },
163
+ dryRun,
164
+ startedAt,
165
+ totalDurationMs: Date.now() - start,
166
+ overall,
167
+ steps: results,
168
+ };
169
+ }
@@ -0,0 +1,43 @@
1
+ export interface IPruneOptions {
2
+ projectRoot: string;
3
+ /** Minimum age in days for a state file to be eligible. Default 30. */
4
+ olderThanDays?: number;
5
+ /**
6
+ * When true, also prune state files where the last recorded run is
7
+ * a `fail`. Default false — failed migrations are typically what the
8
+ * user wants to keep so they can `resume`.
9
+ */
10
+ includeFailed?: boolean;
11
+ /** When true, list what would be deleted but don't touch disk. */
12
+ dryRun?: boolean;
13
+ }
14
+ export interface IPrunedEntry {
15
+ id: string;
16
+ startedAt: string;
17
+ overall: 'pass' | 'fail' | 'skipped';
18
+ ageDays: number;
19
+ reason: 'older-than' | 'failed-included';
20
+ }
21
+ export interface IPruneResult {
22
+ schema: 'sharkcraft.migration-prune/v1';
23
+ /** Total state files scanned. */
24
+ scanned: number;
25
+ /** Files matching the eligibility criteria. */
26
+ eligible: number;
27
+ /** Files actually removed (0 in dry-run). */
28
+ removed: number;
29
+ dryRun: boolean;
30
+ entries: readonly IPrunedEntry[];
31
+ diagnostics: readonly string[];
32
+ }
33
+ /**
34
+ * Prune `.sharkcraft/migrations/*.state.json` files older than
35
+ * `olderThanDays`. Used to keep the dashboard's Migrations panel
36
+ * focused on recent activity.
37
+ *
38
+ * By default skips `overall: 'fail'` entries — those are kept so the
39
+ * user can `shrk migrate resume`. Pass `includeFailed: true` to clear
40
+ * those too (typical after a project-wide cleanup).
41
+ */
42
+ export declare function pruneMigrations(options: IPruneOptions): IPruneResult;
43
+ //# sourceMappingURL=prune-migrations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prune-migrations.d.ts","sourceRoot":"","sources":["../../src/runner/prune-migrations.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,kEAAkE;IAClE,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,YAAY,GAAG,iBAAiB,CAAC;CAC1C;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,+BAA+B,CAAC;IACxC,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IACjC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,aAAa,GAAG,YAAY,CA4FpE"}
@@ -0,0 +1,115 @@
1
+ import { existsSync, readdirSync, rmSync, statSync } from 'node:fs';
2
+ import * as nodePath from 'node:path';
3
+ import { MigrationStateStore } from "./state-store.js";
4
+ /**
5
+ * Prune `.sharkcraft/migrations/*.state.json` files older than
6
+ * `olderThanDays`. Used to keep the dashboard's Migrations panel
7
+ * focused on recent activity.
8
+ *
9
+ * By default skips `overall: 'fail'` entries — those are kept so the
10
+ * user can `shrk migrate resume`. Pass `includeFailed: true` to clear
11
+ * those too (typical after a project-wide cleanup).
12
+ */
13
+ export function pruneMigrations(options) {
14
+ const olderThanDays = options.olderThanDays ?? 30;
15
+ const includeFailed = options.includeFailed ?? false;
16
+ const dryRun = options.dryRun ?? false;
17
+ const diagnostics = [];
18
+ const dir = nodePath.join(options.projectRoot, '.sharkcraft', 'migrations');
19
+ if (!existsSync(dir)) {
20
+ return {
21
+ schema: 'sharkcraft.migration-prune/v1',
22
+ scanned: 0,
23
+ eligible: 0,
24
+ removed: 0,
25
+ dryRun,
26
+ entries: [],
27
+ diagnostics: ['no .sharkcraft/migrations/ directory'],
28
+ };
29
+ }
30
+ const store = new MigrationStateStore(options.projectRoot);
31
+ const now = Date.now();
32
+ let scanned = 0;
33
+ const eligibleEntries = [];
34
+ let entries;
35
+ try {
36
+ entries = readdirSync(dir);
37
+ }
38
+ catch (e) {
39
+ diagnostics.push(`readdir failed: ${e.message}`);
40
+ entries = [];
41
+ }
42
+ for (const entry of entries) {
43
+ if (!entry.endsWith('.state.json'))
44
+ continue;
45
+ scanned += 1;
46
+ const abs = nodePath.join(dir, entry);
47
+ let report;
48
+ try {
49
+ report = store.read(entry.replace(/\.state\.json$/, ''));
50
+ }
51
+ catch {
52
+ report = undefined;
53
+ }
54
+ let startedAt;
55
+ let ageMs;
56
+ if (report) {
57
+ startedAt = report.startedAt;
58
+ ageMs = now - Date.parse(report.startedAt);
59
+ }
60
+ else {
61
+ // Corrupted state file — fall back to mtime.
62
+ try {
63
+ const st = statSync(abs);
64
+ startedAt = new Date(st.mtimeMs).toISOString();
65
+ ageMs = now - st.mtimeMs;
66
+ }
67
+ catch {
68
+ diagnostics.push(`could not stat ${abs}`);
69
+ continue;
70
+ }
71
+ }
72
+ if (Number.isNaN(ageMs))
73
+ continue;
74
+ const ageDays = ageMs / (1000 * 60 * 60 * 24);
75
+ const isFailed = report?.overall === 'fail';
76
+ let reason;
77
+ if (ageDays >= olderThanDays) {
78
+ if (isFailed && !includeFailed)
79
+ continue;
80
+ reason = ageDays >= olderThanDays ? 'older-than' : undefined;
81
+ if (isFailed && includeFailed)
82
+ reason = 'failed-included';
83
+ }
84
+ if (!reason)
85
+ continue;
86
+ eligibleEntries.push({
87
+ id: report?.migration.id ?? entry.replace(/\.state\.json$/, ''),
88
+ startedAt,
89
+ overall: report?.overall ?? 'skipped',
90
+ ageDays: Math.floor(ageDays * 10) / 10,
91
+ reason,
92
+ });
93
+ }
94
+ let removed = 0;
95
+ if (!dryRun) {
96
+ for (const e of eligibleEntries) {
97
+ try {
98
+ rmSync(nodePath.join(dir, `${e.id}.state.json`));
99
+ removed += 1;
100
+ }
101
+ catch (err) {
102
+ diagnostics.push(`rm failed for ${e.id}: ${err.message}`);
103
+ }
104
+ }
105
+ }
106
+ return {
107
+ schema: 'sharkcraft.migration-prune/v1',
108
+ scanned,
109
+ eligible: eligibleEntries.length,
110
+ removed,
111
+ dryRun,
112
+ entries: eligibleEntries,
113
+ diagnostics,
114
+ };
115
+ }
@@ -0,0 +1,28 @@
1
+ import type { IMigration, IMigrationRunReport } from '../schema/migration.js';
2
+ export interface IResumeMigrationOptions {
3
+ projectRoot: string;
4
+ /** Forwarded to applyMigration. */
5
+ dryRun?: boolean;
6
+ stopOnFailure?: boolean;
7
+ shellTimeoutMs?: number;
8
+ }
9
+ export interface IResumeMigrationResult {
10
+ /** Final run report after the resume. */
11
+ report: IMigrationRunReport;
12
+ /** Step index the resume started at. */
13
+ resumedFromIndex: number;
14
+ /** Free-form diagnostics about the resume decision. */
15
+ diagnostics: readonly string[];
16
+ }
17
+ /**
18
+ * Pick up a previously-failed migration from the step that failed and
19
+ * continue forward. Reads the saved state at
20
+ * `.sharkcraft/migrations/<id>.state.json` (written by `applyMigration`
21
+ * after each step) and dispatches a fresh `applyMigration` call with
22
+ * `resumeFromIndex` set.
23
+ *
24
+ * Returns a diagnostic and skips re-running when no resume point is
25
+ * found (everything already applied, or no saved state at all).
26
+ */
27
+ export declare function resumeMigration(migration: IMigration, options: IResumeMigrationOptions): IResumeMigrationResult;
28
+ //# sourceMappingURL=resume-migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resume-migration.d.ts","sourceRoot":"","sources":["../../src/runner/resume-migration.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE9E,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,yCAAyC;IACzC,MAAM,EAAE,mBAAmB,CAAC;IAC5B,wCAAwC;IACxC,gBAAgB,EAAE,MAAM,CAAC;IACzB,uDAAuD;IACvD,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,UAAU,EACrB,OAAO,EAAE,uBAAuB,GAC/B,sBAAsB,CAqBxB"}
@@ -0,0 +1,42 @@
1
+ import { applyMigration } from "./apply-migration.js";
2
+ import { findResumePoint, MigrationStateStore } from "./state-store.js";
3
+ /**
4
+ * Pick up a previously-failed migration from the step that failed and
5
+ * continue forward. Reads the saved state at
6
+ * `.sharkcraft/migrations/<id>.state.json` (written by `applyMigration`
7
+ * after each step) and dispatches a fresh `applyMigration` call with
8
+ * `resumeFromIndex` set.
9
+ *
10
+ * Returns a diagnostic and skips re-running when no resume point is
11
+ * found (everything already applied, or no saved state at all).
12
+ */
13
+ export function resumeMigration(migration, options) {
14
+ const diagnostics = [];
15
+ const store = new MigrationStateStore(options.projectRoot);
16
+ const prior = store.read(migration.id);
17
+ if (!prior) {
18
+ diagnostics.push(`no saved state for migration "${migration.id}" — running from the beginning.`);
19
+ const fresh = applyMigration(migration, applyOpts(options));
20
+ return { report: fresh, resumedFromIndex: 0, diagnostics };
21
+ }
22
+ const resumePoint = findResumePoint(prior);
23
+ if (resumePoint === undefined) {
24
+ diagnostics.push(`migration "${migration.id}" already complete; nothing to resume.`);
25
+ return { report: prior, resumedFromIndex: prior.steps.length, diagnostics };
26
+ }
27
+ diagnostics.push(`resuming from step ${resumePoint + 1} (${migration.steps[resumePoint]?.id ?? 'unknown'}).`);
28
+ const report = applyMigration(migration, {
29
+ ...applyOpts(options),
30
+ resumeFromIndex: resumePoint,
31
+ priorSteps: prior.steps.slice(0, resumePoint),
32
+ });
33
+ return { report, resumedFromIndex: resumePoint, diagnostics };
34
+ }
35
+ function applyOpts(options) {
36
+ return {
37
+ projectRoot: options.projectRoot,
38
+ ...(options.dryRun !== undefined ? { dryRun: options.dryRun } : {}),
39
+ ...(options.stopOnFailure !== undefined ? { stopOnFailure: options.stopOnFailure } : {}),
40
+ ...(options.shellTimeoutMs !== undefined ? { shellTimeoutMs: options.shellTimeoutMs } : {}),
41
+ };
42
+ }
@@ -0,0 +1,26 @@
1
+ import type { IMigrationRunReport } from '../schema/migration.js';
2
+ /**
3
+ * Persist per-migration run state under `.sharkcraft/migrations/`. The
4
+ * checkpoint is the full `IMigrationRunReport` so far — `resumeMigration`
5
+ * reads it back, finds the last failed step, and continues from there.
6
+ */
7
+ export declare class MigrationStateStore {
8
+ private readonly projectRoot;
9
+ readonly dir: string;
10
+ constructor(projectRoot: string);
11
+ pathFor(migrationId: string): string;
12
+ exists(migrationId: string): boolean;
13
+ read(migrationId: string): IMigrationRunReport | undefined;
14
+ write(migrationId: string, report: IMigrationRunReport): void;
15
+ clear(migrationId: string): void;
16
+ }
17
+ /**
18
+ * Find the index of the first step the resume runner should re-execute.
19
+ *
20
+ * - First failed step → resume from there.
21
+ * - No failures + steps remaining → resume from the first skipped /
22
+ * pending step.
23
+ * - All steps applied → undefined (nothing to resume).
24
+ */
25
+ export declare function findResumePoint(report: IMigrationRunReport): number | undefined;
26
+ //# sourceMappingURL=state-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-store.d.ts","sourceRoot":"","sources":["../../src/runner/state-store.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAIlE;;;;GAIG;AACH,qBAAa,mBAAmB;IAGlB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAFxC,SAAgB,GAAG,EAAE,MAAM,CAAC;gBAEC,WAAW,EAAE,MAAM;IAIhD,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM;IAIpC,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAIpC,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAS1D,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,GAAG,IAAI;IAK7D,KAAK,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;CAGjC;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,mBAAmB,GAAG,MAAM,GAAG,SAAS,CAQ/E"}
@@ -0,0 +1,59 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import * as nodePath from 'node:path';
3
+ const DIR = '.sharkcraft/migrations';
4
+ /**
5
+ * Persist per-migration run state under `.sharkcraft/migrations/`. The
6
+ * checkpoint is the full `IMigrationRunReport` so far — `resumeMigration`
7
+ * reads it back, finds the last failed step, and continues from there.
8
+ */
9
+ export class MigrationStateStore {
10
+ projectRoot;
11
+ dir;
12
+ constructor(projectRoot) {
13
+ this.projectRoot = projectRoot;
14
+ this.dir = nodePath.join(projectRoot, DIR);
15
+ }
16
+ pathFor(migrationId) {
17
+ return nodePath.join(this.dir, `${migrationId}.state.json`);
18
+ }
19
+ exists(migrationId) {
20
+ return existsSync(this.pathFor(migrationId));
21
+ }
22
+ read(migrationId) {
23
+ if (!this.exists(migrationId))
24
+ return undefined;
25
+ try {
26
+ return JSON.parse(readFileSync(this.pathFor(migrationId), 'utf8'));
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ }
32
+ write(migrationId, report) {
33
+ mkdirSync(this.dir, { recursive: true });
34
+ writeFileSync(this.pathFor(migrationId), JSON.stringify(report, null, 2), 'utf8');
35
+ }
36
+ clear(migrationId) {
37
+ if (this.exists(migrationId))
38
+ rmSync(this.pathFor(migrationId));
39
+ }
40
+ }
41
+ /**
42
+ * Find the index of the first step the resume runner should re-execute.
43
+ *
44
+ * - First failed step → resume from there.
45
+ * - No failures + steps remaining → resume from the first skipped /
46
+ * pending step.
47
+ * - All steps applied → undefined (nothing to resume).
48
+ */
49
+ export function findResumePoint(report) {
50
+ for (const s of report.steps) {
51
+ if (s.status === 'failed')
52
+ return s.index;
53
+ }
54
+ for (const s of report.steps) {
55
+ if (s.status === 'skipped' || s.status === 'pending')
56
+ return s.index;
57
+ }
58
+ return undefined;
59
+ }
@@ -0,0 +1,90 @@
1
+ import type { RewriteRecipe, StructuralPattern } from '@shrkcrft/structural-search';
2
+ export declare const MIGRATION_SCHEMA: "sharkcraft.migration/v1";
3
+ export declare const MIGRATION_RUN_SCHEMA: "sharkcraft.migration-run/v1";
4
+ /**
5
+ * Ordered list of steps. Each step is one of:
6
+ *
7
+ * - `structural-rewrite`: pair of (pattern, recipe) consumed by
8
+ * `@shrkcrft/structural-search`. The most common step kind.
9
+ * - `shell`: a literal shell command. Useful for `bun install`,
10
+ * `bun run build`, version bumps, etc.
11
+ * - `check`: a CLI command whose exit code gates the migration.
12
+ * Same as `shell` but the runner records a pass/fail per step.
13
+ */
14
+ export type MigrationStep = {
15
+ kind: 'structural-rewrite';
16
+ id?: string;
17
+ description?: string;
18
+ pattern: StructuralPattern;
19
+ recipe: RewriteRecipe;
20
+ } | {
21
+ kind: 'shell';
22
+ id?: string;
23
+ description?: string;
24
+ /** Shell command line. Run via bash -c. */
25
+ command: string;
26
+ /** Working directory relative to project root. */
27
+ cwd?: string;
28
+ } | {
29
+ kind: 'check';
30
+ id?: string;
31
+ description?: string;
32
+ command: string;
33
+ cwd?: string;
34
+ };
35
+ export interface IMigration {
36
+ schema: typeof MIGRATION_SCHEMA;
37
+ /** Stable migration id (used to look it up on disk). */
38
+ id: string;
39
+ /** Display title. */
40
+ title: string;
41
+ /** Optional human description. */
42
+ description?: string;
43
+ /** Steps run in this order. */
44
+ steps: readonly MigrationStep[];
45
+ }
46
+ export type StepStatus = 'pending' | 'planned' | 'applied' | 'failed' | 'skipped';
47
+ export interface IStepRunResult {
48
+ /** Index in the migration's steps array. */
49
+ index: number;
50
+ /** Step id (defaults to `step-<index>`). */
51
+ id: string;
52
+ kind: MigrationStep['kind'];
53
+ status: StepStatus;
54
+ /** Human-readable headline. */
55
+ message: string;
56
+ /** Wall-clock duration of the step, in ms. */
57
+ durationMs: number;
58
+ /** For structural-rewrite steps: counts of files / edits. */
59
+ rewriteStats?: {
60
+ filesScanned: number;
61
+ filesAttempted: number;
62
+ filesChanged: number;
63
+ totalEdits: number;
64
+ conflicts: readonly string[];
65
+ };
66
+ /** For shell / check steps: captured stdout + exit code. */
67
+ shellOutput?: {
68
+ exitCode: number;
69
+ stdout: string;
70
+ stderr: string;
71
+ };
72
+ /** Free-form diagnostics from the step. */
73
+ diagnostics: readonly string[];
74
+ }
75
+ export interface IMigrationRunReport {
76
+ schema: typeof MIGRATION_RUN_SCHEMA;
77
+ migration: {
78
+ id: string;
79
+ title: string;
80
+ };
81
+ /** True for the `plan` flow (no fs writes); false for `apply`. */
82
+ dryRun: boolean;
83
+ startedAt: string;
84
+ totalDurationMs: number;
85
+ /** Overall status: `pass` if every step is `applied` (or `skipped`);
86
+ * `fail` if any step failed. */
87
+ overall: 'pass' | 'fail' | 'skipped';
88
+ steps: readonly IStepRunResult[];
89
+ }
90
+ //# sourceMappingURL=migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration.d.ts","sourceRoot":"","sources":["../../src/schema/migration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAEpF,eAAO,MAAM,gBAAgB,EAAG,yBAAkC,CAAC;AACnE,eAAO,MAAM,oBAAoB,EAAG,6BAAsC,CAAC;AAE3E;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,GACrB;IACE,IAAI,EAAE,oBAAoB,CAAC;IAC3B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,iBAAiB,CAAC;IAC3B,MAAM,EAAE,aAAa,CAAC;CACvB,GACD;IACE,IAAI,EAAE,OAAO,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GACD;IACE,IAAI,EAAE,OAAO,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEN,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,OAAO,gBAAgB,CAAC;IAChC,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAC;IACX,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,KAAK,EAAE,SAAS,aAAa,EAAE,CAAC;CACjC;AAED,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAElF,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC5B,MAAM,EAAE,UAAU,CAAC;IACnB,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,YAAY,CAAC,EAAE;QACb,YAAY,EAAE,MAAM,CAAC;QACrB,cAAc,EAAE,MAAM,CAAC;QACvB,YAAY,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;KAC9B,CAAC;IACF,4DAA4D;IAC5D,WAAW,CAAC,EAAE;QACZ,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,2CAA2C;IAC3C,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,OAAO,oBAAoB,CAAC;IACpC,SAAS,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,kEAAkE;IAClE,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB;oCACgC;IAChC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACrC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;CAClC"}
@@ -0,0 +1,2 @@
1
+ export const MIGRATION_SCHEMA = 'sharkcraft.migration/v1';
2
+ export const MIGRATION_RUN_SCHEMA = 'sharkcraft.migration-run/v1';
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@shrkcrft/migrate",
3
+ "version": "0.1.0-alpha.10",
4
+ "description": "SharkCraft migrations: orchestrate multi-step refactors (structural rewrites + shell + checkpoints) as one replayable migration. The safe path for big cross-cutting refactors.",
5
+ "license": "MIT",
6
+ "author": "SharkCraft contributors",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/sharkcraft/sharkcraft.git",
25
+ "directory": "packages/migrate"
26
+ },
27
+ "homepage": "https://github.com/sharkcraft/sharkcraft",
28
+ "bugs": {
29
+ "url": "https://github.com/sharkcraft/sharkcraft/issues"
30
+ },
31
+ "keywords": [
32
+ "sharkcraft",
33
+ "migration",
34
+ "codemod",
35
+ "orchestration"
36
+ ],
37
+ "engines": {
38
+ "bun": ">=1.1.0",
39
+ "node": ">=18"
40
+ },
41
+ "scripts": {
42
+ "typecheck": "tsc --noEmit -p tsconfig.json"
43
+ },
44
+ "dependencies": {
45
+ "@shrkcrft/core": "^0.1.0-alpha.10",
46
+ "@shrkcrft/structural-search": "^0.1.0-alpha.10"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }