@savestate/cli 0.5.0 → 0.7.0
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/dist/cli.js +8 -0
- package/dist/cli.js.map +1 -1
- package/dist/mcp/server.d.ts +26 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +371 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/migrate/__tests__/claude-loader.test.d.ts +7 -0
- package/dist/migrate/__tests__/claude-loader.test.d.ts.map +1 -0
- package/dist/migrate/__tests__/claude-loader.test.js +544 -0
- package/dist/migrate/__tests__/claude-loader.test.js.map +1 -0
- package/dist/migrate/__tests__/orchestrator.test.d.ts +8 -0
- package/dist/migrate/__tests__/orchestrator.test.d.ts.map +1 -0
- package/dist/migrate/__tests__/orchestrator.test.js +355 -0
- package/dist/migrate/__tests__/orchestrator.test.js.map +1 -0
- package/dist/migrate/capabilities.d.ts +23 -0
- package/dist/migrate/capabilities.d.ts.map +1 -0
- package/dist/migrate/capabilities.js +80 -0
- package/dist/migrate/capabilities.js.map +1 -0
- package/dist/migrate/extractors/__tests__/chatgpt.test.d.ts +12 -0
- package/dist/migrate/extractors/__tests__/chatgpt.test.d.ts.map +1 -0
- package/dist/migrate/extractors/__tests__/chatgpt.test.js +522 -0
- package/dist/migrate/extractors/__tests__/chatgpt.test.js.map +1 -0
- package/dist/migrate/extractors/chatgpt.d.ts +70 -0
- package/dist/migrate/extractors/chatgpt.d.ts.map +1 -0
- package/dist/migrate/extractors/chatgpt.js +791 -0
- package/dist/migrate/extractors/chatgpt.js.map +1 -0
- package/dist/migrate/extractors/registry.d.ts +24 -0
- package/dist/migrate/extractors/registry.d.ts.map +1 -0
- package/dist/migrate/extractors/registry.js +39 -0
- package/dist/migrate/extractors/registry.js.map +1 -0
- package/dist/migrate/index.d.ts +18 -0
- package/dist/migrate/index.d.ts.map +1 -0
- package/dist/migrate/index.js +26 -0
- package/dist/migrate/index.js.map +1 -0
- package/dist/migrate/loaders/claude.d.ts +61 -0
- package/dist/migrate/loaders/claude.d.ts.map +1 -0
- package/dist/migrate/loaders/claude.js +433 -0
- package/dist/migrate/loaders/claude.js.map +1 -0
- package/dist/migrate/loaders/registry.d.ts +24 -0
- package/dist/migrate/loaders/registry.d.ts.map +1 -0
- package/dist/migrate/loaders/registry.js +39 -0
- package/dist/migrate/loaders/registry.js.map +1 -0
- package/dist/migrate/orchestrator.d.ts +151 -0
- package/dist/migrate/orchestrator.d.ts.map +1 -0
- package/dist/migrate/orchestrator.js +518 -0
- package/dist/migrate/orchestrator.js.map +1 -0
- package/dist/migrate/testing/index.d.ts +28 -0
- package/dist/migrate/testing/index.d.ts.map +1 -0
- package/dist/migrate/testing/index.js +55 -0
- package/dist/migrate/testing/index.js.map +1 -0
- package/dist/migrate/testing/mock-extractor.d.ts +30 -0
- package/dist/migrate/testing/mock-extractor.d.ts.map +1 -0
- package/dist/migrate/testing/mock-extractor.js +137 -0
- package/dist/migrate/testing/mock-extractor.js.map +1 -0
- package/dist/migrate/testing/mock-loader.d.ts +36 -0
- package/dist/migrate/testing/mock-loader.d.ts.map +1 -0
- package/dist/migrate/testing/mock-loader.js +81 -0
- package/dist/migrate/testing/mock-loader.js.map +1 -0
- package/dist/migrate/testing/mock-transformer.d.ts +26 -0
- package/dist/migrate/testing/mock-transformer.d.ts.map +1 -0
- package/dist/migrate/testing/mock-transformer.js +185 -0
- package/dist/migrate/testing/mock-transformer.js.map +1 -0
- package/dist/migrate/transformers/__tests__/chatgpt-to-claude.test.d.ts +5 -0
- package/dist/migrate/transformers/__tests__/chatgpt-to-claude.test.d.ts.map +1 -0
- package/dist/migrate/transformers/__tests__/chatgpt-to-claude.test.js +333 -0
- package/dist/migrate/transformers/__tests__/chatgpt-to-claude.test.js.map +1 -0
- package/dist/migrate/transformers/__tests__/claude-to-chatgpt.test.d.ts +5 -0
- package/dist/migrate/transformers/__tests__/claude-to-chatgpt.test.d.ts.map +1 -0
- package/dist/migrate/transformers/__tests__/claude-to-chatgpt.test.js +333 -0
- package/dist/migrate/transformers/__tests__/claude-to-chatgpt.test.js.map +1 -0
- package/dist/migrate/transformers/__tests__/rules.test.d.ts +5 -0
- package/dist/migrate/transformers/__tests__/rules.test.d.ts.map +1 -0
- package/dist/migrate/transformers/__tests__/rules.test.js +375 -0
- package/dist/migrate/transformers/__tests__/rules.test.js.map +1 -0
- package/dist/migrate/transformers/chatgpt-to-claude.d.ts +40 -0
- package/dist/migrate/transformers/chatgpt-to-claude.d.ts.map +1 -0
- package/dist/migrate/transformers/chatgpt-to-claude.js +443 -0
- package/dist/migrate/transformers/chatgpt-to-claude.js.map +1 -0
- package/dist/migrate/transformers/claude-to-chatgpt.d.ts +41 -0
- package/dist/migrate/transformers/claude-to-chatgpt.d.ts.map +1 -0
- package/dist/migrate/transformers/claude-to-chatgpt.js +532 -0
- package/dist/migrate/transformers/claude-to-chatgpt.js.map +1 -0
- package/dist/migrate/transformers/registry.d.ts +27 -0
- package/dist/migrate/transformers/registry.d.ts.map +1 -0
- package/dist/migrate/transformers/registry.js +49 -0
- package/dist/migrate/transformers/registry.js.map +1 -0
- package/dist/migrate/transformers/rules.d.ts +168 -0
- package/dist/migrate/transformers/rules.d.ts.map +1 -0
- package/dist/migrate/transformers/rules.js +487 -0
- package/dist/migrate/transformers/rules.js.map +1 -0
- package/dist/migrate/types.d.ts +350 -0
- package/dist/migrate/types.d.ts.map +1 -0
- package/dist/migrate/types.js +8 -0
- package/dist/migrate/types.js.map +1 -0
- package/package.json +9 -3
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Coordinates the three-phase migration process:
|
|
5
|
+
* Extract → Transform → Load
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Phase checkpoint/resume capability
|
|
9
|
+
* - Progress tracking
|
|
10
|
+
* - Error recovery
|
|
11
|
+
* - Rollback support (undo loaded changes)
|
|
12
|
+
*/
|
|
13
|
+
import type { Platform, MigrationBundle, MigrationState, MigrationPhase, MigrationOptions, CompatibilityReport, LoadResult } from './types.js';
|
|
14
|
+
export interface RollbackAction {
|
|
15
|
+
/** Type of rollback action */
|
|
16
|
+
type: 'delete-project' | 'delete-memories' | 'delete-files' | 'restore-instructions';
|
|
17
|
+
/** Description of what will be undone */
|
|
18
|
+
description: string;
|
|
19
|
+
/** Platform where the action will be performed */
|
|
20
|
+
platform: Platform;
|
|
21
|
+
/** Resource IDs to rollback */
|
|
22
|
+
resourceIds: string[];
|
|
23
|
+
/** Original data (for restore operations) */
|
|
24
|
+
originalData?: unknown;
|
|
25
|
+
}
|
|
26
|
+
export interface RollbackPlan {
|
|
27
|
+
/** Migration ID this plan belongs to */
|
|
28
|
+
migrationId: string;
|
|
29
|
+
/** Actions to perform (in reverse order) */
|
|
30
|
+
actions: RollbackAction[];
|
|
31
|
+
/** When the plan was created */
|
|
32
|
+
createdAt: string;
|
|
33
|
+
/** Whether rollback has been executed */
|
|
34
|
+
executed: boolean;
|
|
35
|
+
/** Execution timestamp */
|
|
36
|
+
executedAt?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface RollbackResult {
|
|
39
|
+
/** Whether rollback was successful */
|
|
40
|
+
success: boolean;
|
|
41
|
+
/** Actions that succeeded */
|
|
42
|
+
succeeded: RollbackAction[];
|
|
43
|
+
/** Actions that failed */
|
|
44
|
+
failed: Array<{
|
|
45
|
+
action: RollbackAction;
|
|
46
|
+
error: string;
|
|
47
|
+
}>;
|
|
48
|
+
/** Warnings during rollback */
|
|
49
|
+
warnings: string[];
|
|
50
|
+
}
|
|
51
|
+
export type MigrationEventType = 'phase:start' | 'phase:complete' | 'phase:error' | 'progress' | 'checkpoint' | 'complete' | 'error';
|
|
52
|
+
export interface MigrationEvent {
|
|
53
|
+
type: MigrationEventType;
|
|
54
|
+
phase?: MigrationPhase;
|
|
55
|
+
progress?: number;
|
|
56
|
+
message?: string;
|
|
57
|
+
error?: Error;
|
|
58
|
+
data?: unknown;
|
|
59
|
+
}
|
|
60
|
+
export type MigrationEventHandler = (event: MigrationEvent) => void;
|
|
61
|
+
export declare class MigrationOrchestrator {
|
|
62
|
+
private state;
|
|
63
|
+
private bundle;
|
|
64
|
+
private eventHandlers;
|
|
65
|
+
private workDir;
|
|
66
|
+
private rollbackPlan;
|
|
67
|
+
constructor(source: Platform, target: Platform, options?: MigrationOptions);
|
|
68
|
+
/**
|
|
69
|
+
* Run the full migration pipeline.
|
|
70
|
+
*/
|
|
71
|
+
run(): Promise<LoadResult>;
|
|
72
|
+
/**
|
|
73
|
+
* Run only the extract phase (useful for debugging/inspection).
|
|
74
|
+
*/
|
|
75
|
+
extract(): Promise<MigrationBundle>;
|
|
76
|
+
/**
|
|
77
|
+
* Generate compatibility report without running full migration.
|
|
78
|
+
*/
|
|
79
|
+
analyze(): Promise<CompatibilityReport>;
|
|
80
|
+
/**
|
|
81
|
+
* Resume a failed or interrupted migration.
|
|
82
|
+
*/
|
|
83
|
+
static resume(migrationId: string, workDir?: string): Promise<MigrationOrchestrator>;
|
|
84
|
+
/**
|
|
85
|
+
* Resume and continue the migration from where it left off.
|
|
86
|
+
*/
|
|
87
|
+
continue(): Promise<LoadResult>;
|
|
88
|
+
/**
|
|
89
|
+
* Clean up migration artifacts.
|
|
90
|
+
*/
|
|
91
|
+
cleanup(): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Get the rollback plan for this migration (if one exists).
|
|
94
|
+
*/
|
|
95
|
+
getRollbackPlan(): RollbackPlan | null;
|
|
96
|
+
/**
|
|
97
|
+
* Check if rollback is available for this migration.
|
|
98
|
+
*/
|
|
99
|
+
canRollback(): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Rollback a completed migration.
|
|
102
|
+
*
|
|
103
|
+
* This attempts to undo changes made during the load phase.
|
|
104
|
+
* Note: Rollback may not be complete for all platforms.
|
|
105
|
+
*/
|
|
106
|
+
rollback(): Promise<RollbackResult>;
|
|
107
|
+
/**
|
|
108
|
+
* Execute a single rollback action.
|
|
109
|
+
* Override in subclasses for platform-specific implementations.
|
|
110
|
+
*/
|
|
111
|
+
protected executeRollbackAction(action: RollbackAction): Promise<void>;
|
|
112
|
+
/**
|
|
113
|
+
* Create a rollback plan from a load result.
|
|
114
|
+
*/
|
|
115
|
+
private createRollbackPlan;
|
|
116
|
+
/**
|
|
117
|
+
* Save rollback plan to disk.
|
|
118
|
+
*/
|
|
119
|
+
private saveRollbackPlan;
|
|
120
|
+
/**
|
|
121
|
+
* Load rollback plan from disk.
|
|
122
|
+
*/
|
|
123
|
+
private loadRollbackPlan;
|
|
124
|
+
/**
|
|
125
|
+
* List all migrations in the work directory.
|
|
126
|
+
*/
|
|
127
|
+
static listMigrations(baseDir?: string): Promise<MigrationState[]>;
|
|
128
|
+
/**
|
|
129
|
+
* Subscribe to migration events.
|
|
130
|
+
*/
|
|
131
|
+
on(handler: MigrationEventHandler): () => void;
|
|
132
|
+
/**
|
|
133
|
+
* Get current migration state.
|
|
134
|
+
*/
|
|
135
|
+
getState(): MigrationState;
|
|
136
|
+
/**
|
|
137
|
+
* Get the migration bundle (if extracted).
|
|
138
|
+
*/
|
|
139
|
+
getBundle(): MigrationBundle | null;
|
|
140
|
+
private runExtractPhase;
|
|
141
|
+
private runTransformPhase;
|
|
142
|
+
private runLoadPhase;
|
|
143
|
+
private generateId;
|
|
144
|
+
private ensureWorkDir;
|
|
145
|
+
private setPhase;
|
|
146
|
+
private saveState;
|
|
147
|
+
private saveCheckpoint;
|
|
148
|
+
private loadCheckpoint;
|
|
149
|
+
private emit;
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=orchestrator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/migrate/orchestrator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Coordinates the three-phase migration process:
|
|
5
|
+
* Extract → Transform → Load
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Phase checkpoint/resume capability
|
|
9
|
+
* - Progress tracking
|
|
10
|
+
* - Error recovery
|
|
11
|
+
* - Rollback support (undo loaded changes)
|
|
12
|
+
*/
|
|
13
|
+
import { randomBytes } from 'node:crypto';
|
|
14
|
+
import { mkdir, writeFile, readFile, rm, readdir } from 'node:fs/promises';
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
18
|
+
import { getExtractor } from './extractors/registry.js';
|
|
19
|
+
import { getTransformer } from './transformers/registry.js';
|
|
20
|
+
import { getLoader } from './loaders/registry.js';
|
|
21
|
+
// ─── Orchestrator ────────────────────────────────────────────
|
|
22
|
+
export class MigrationOrchestrator {
|
|
23
|
+
state;
|
|
24
|
+
bundle = null;
|
|
25
|
+
eventHandlers = [];
|
|
26
|
+
workDir;
|
|
27
|
+
rollbackPlan = null;
|
|
28
|
+
constructor(source, target, options = {}) {
|
|
29
|
+
const id = this.generateId();
|
|
30
|
+
this.workDir = options.workDir ?? join(process.cwd(), '.savestate', 'migrations', id);
|
|
31
|
+
this.state = {
|
|
32
|
+
id,
|
|
33
|
+
phase: 'pending',
|
|
34
|
+
source,
|
|
35
|
+
target,
|
|
36
|
+
startedAt: new Date().toISOString(),
|
|
37
|
+
phaseStartedAt: new Date().toISOString(),
|
|
38
|
+
checkpoints: [],
|
|
39
|
+
progress: 0,
|
|
40
|
+
options,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// ─── Public API ────────────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* Run the full migration pipeline.
|
|
46
|
+
*/
|
|
47
|
+
async run() {
|
|
48
|
+
await this.ensureWorkDir();
|
|
49
|
+
try {
|
|
50
|
+
// Phase 1: Extract
|
|
51
|
+
await this.runExtractPhase();
|
|
52
|
+
// Phase 2: Transform
|
|
53
|
+
await this.runTransformPhase();
|
|
54
|
+
// Phase 3: Load
|
|
55
|
+
const result = await this.runLoadPhase();
|
|
56
|
+
this.state.phase = 'complete';
|
|
57
|
+
this.state.completedAt = new Date().toISOString();
|
|
58
|
+
await this.saveState();
|
|
59
|
+
this.emit({ type: 'complete', data: result });
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
this.state.phase = 'failed';
|
|
64
|
+
this.state.error = error instanceof Error ? error.message : String(error);
|
|
65
|
+
await this.saveState();
|
|
66
|
+
this.emit({ type: 'error', error: error instanceof Error ? error : new Error(String(error)) });
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Run only the extract phase (useful for debugging/inspection).
|
|
72
|
+
*/
|
|
73
|
+
async extract() {
|
|
74
|
+
await this.ensureWorkDir();
|
|
75
|
+
await this.runExtractPhase();
|
|
76
|
+
return this.bundle;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Generate compatibility report without running full migration.
|
|
80
|
+
*/
|
|
81
|
+
async analyze() {
|
|
82
|
+
await this.ensureWorkDir();
|
|
83
|
+
// Extract first if we don't have a bundle
|
|
84
|
+
if (!this.bundle) {
|
|
85
|
+
await this.runExtractPhase();
|
|
86
|
+
}
|
|
87
|
+
const transformer = getTransformer(this.state.source, this.state.target);
|
|
88
|
+
if (!transformer) {
|
|
89
|
+
throw new Error(`No transformer available for ${this.state.source} → ${this.state.target}`);
|
|
90
|
+
}
|
|
91
|
+
return transformer.analyze(this.bundle);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Resume a failed or interrupted migration.
|
|
95
|
+
*/
|
|
96
|
+
static async resume(migrationId, workDir) {
|
|
97
|
+
const baseDir = workDir ?? join(process.cwd(), '.savestate', 'migrations', migrationId);
|
|
98
|
+
const statePath = join(baseDir, 'state.json');
|
|
99
|
+
if (!existsSync(statePath)) {
|
|
100
|
+
throw new Error(`Migration ${migrationId} not found at ${baseDir}`);
|
|
101
|
+
}
|
|
102
|
+
const stateJson = await readFile(statePath, 'utf-8');
|
|
103
|
+
const state = JSON.parse(stateJson);
|
|
104
|
+
const orchestrator = new MigrationOrchestrator(state.source, state.target, state.options);
|
|
105
|
+
orchestrator.state = state;
|
|
106
|
+
orchestrator.workDir = baseDir;
|
|
107
|
+
// Load bundle if we have one
|
|
108
|
+
if (state.bundlePath && existsSync(state.bundlePath)) {
|
|
109
|
+
const bundleJson = await readFile(state.bundlePath, 'utf-8');
|
|
110
|
+
orchestrator.bundle = JSON.parse(bundleJson);
|
|
111
|
+
}
|
|
112
|
+
// Load rollback plan if available
|
|
113
|
+
await orchestrator.loadRollbackPlan();
|
|
114
|
+
return orchestrator;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Resume and continue the migration from where it left off.
|
|
118
|
+
*/
|
|
119
|
+
async continue() {
|
|
120
|
+
const lastCheckpoint = this.state.checkpoints[this.state.checkpoints.length - 1];
|
|
121
|
+
if (!lastCheckpoint) {
|
|
122
|
+
// No checkpoint, start from beginning
|
|
123
|
+
return this.run();
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
let result;
|
|
127
|
+
// Resume from the phase after the last checkpoint
|
|
128
|
+
switch (lastCheckpoint.phase) {
|
|
129
|
+
case 'extracting':
|
|
130
|
+
// Extract completed, continue with transform
|
|
131
|
+
await this.loadCheckpoint(lastCheckpoint);
|
|
132
|
+
await this.runTransformPhase();
|
|
133
|
+
result = await this.runLoadPhase();
|
|
134
|
+
break;
|
|
135
|
+
case 'transforming':
|
|
136
|
+
// Transform completed, continue with load
|
|
137
|
+
await this.loadCheckpoint(lastCheckpoint);
|
|
138
|
+
result = await this.runLoadPhase();
|
|
139
|
+
break;
|
|
140
|
+
case 'loading':
|
|
141
|
+
// Load was in progress, need to restart it
|
|
142
|
+
await this.loadCheckpoint(lastCheckpoint);
|
|
143
|
+
result = await this.runLoadPhase();
|
|
144
|
+
break;
|
|
145
|
+
default:
|
|
146
|
+
return this.run();
|
|
147
|
+
}
|
|
148
|
+
// Mark migration as complete
|
|
149
|
+
this.state.phase = 'complete';
|
|
150
|
+
this.state.completedAt = new Date().toISOString();
|
|
151
|
+
await this.saveState();
|
|
152
|
+
this.emit({ type: 'complete', data: result });
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
this.state.phase = 'failed';
|
|
157
|
+
this.state.error = error instanceof Error ? error.message : String(error);
|
|
158
|
+
await this.saveState();
|
|
159
|
+
this.emit({ type: 'error', error: error instanceof Error ? error : new Error(String(error)) });
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Clean up migration artifacts.
|
|
165
|
+
*/
|
|
166
|
+
async cleanup() {
|
|
167
|
+
if (existsSync(this.workDir)) {
|
|
168
|
+
await rm(this.workDir, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get the rollback plan for this migration (if one exists).
|
|
173
|
+
*/
|
|
174
|
+
getRollbackPlan() {
|
|
175
|
+
return this.rollbackPlan;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Check if rollback is available for this migration.
|
|
179
|
+
*/
|
|
180
|
+
canRollback() {
|
|
181
|
+
return this.rollbackPlan !== null && !this.rollbackPlan.executed;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Rollback a completed migration.
|
|
185
|
+
*
|
|
186
|
+
* This attempts to undo changes made during the load phase.
|
|
187
|
+
* Note: Rollback may not be complete for all platforms.
|
|
188
|
+
*/
|
|
189
|
+
async rollback() {
|
|
190
|
+
if (!this.rollbackPlan) {
|
|
191
|
+
throw new Error('No rollback plan available - migration may not have completed');
|
|
192
|
+
}
|
|
193
|
+
if (this.rollbackPlan.executed) {
|
|
194
|
+
throw new Error('Rollback has already been executed');
|
|
195
|
+
}
|
|
196
|
+
this.emit({
|
|
197
|
+
type: 'phase:start',
|
|
198
|
+
phase: 'failed', // Reusing 'failed' for rollback phase
|
|
199
|
+
message: 'Starting rollback...',
|
|
200
|
+
});
|
|
201
|
+
const result = {
|
|
202
|
+
success: true,
|
|
203
|
+
succeeded: [],
|
|
204
|
+
failed: [],
|
|
205
|
+
warnings: [],
|
|
206
|
+
};
|
|
207
|
+
// Execute actions in reverse order
|
|
208
|
+
const actions = [...this.rollbackPlan.actions].reverse();
|
|
209
|
+
for (const action of actions) {
|
|
210
|
+
try {
|
|
211
|
+
await this.executeRollbackAction(action);
|
|
212
|
+
result.succeeded.push(action);
|
|
213
|
+
this.emit({
|
|
214
|
+
type: 'progress',
|
|
215
|
+
message: `Rolled back: ${action.description}`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
220
|
+
result.failed.push({ action, error: errorMsg });
|
|
221
|
+
result.warnings.push(`Failed to rollback: ${action.description}`);
|
|
222
|
+
// Continue with other rollback actions
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Mark rollback as executed
|
|
226
|
+
this.rollbackPlan.executed = true;
|
|
227
|
+
this.rollbackPlan.executedAt = new Date().toISOString();
|
|
228
|
+
await this.saveRollbackPlan();
|
|
229
|
+
// Update overall success
|
|
230
|
+
result.success = result.failed.length === 0;
|
|
231
|
+
this.emit({
|
|
232
|
+
type: result.success ? 'complete' : 'error',
|
|
233
|
+
message: result.success
|
|
234
|
+
? 'Rollback completed successfully'
|
|
235
|
+
: `Rollback completed with ${result.failed.length} failures`,
|
|
236
|
+
data: result,
|
|
237
|
+
});
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Execute a single rollback action.
|
|
242
|
+
* Override in subclasses for platform-specific implementations.
|
|
243
|
+
*/
|
|
244
|
+
async executeRollbackAction(action) {
|
|
245
|
+
// Base implementation logs the action
|
|
246
|
+
// Real implementations would call platform APIs
|
|
247
|
+
this.emit({
|
|
248
|
+
type: 'progress',
|
|
249
|
+
message: `Executing rollback: ${action.type} on ${action.platform}`,
|
|
250
|
+
data: action,
|
|
251
|
+
});
|
|
252
|
+
// For now, simulate the rollback
|
|
253
|
+
// Real implementations would be provided by loaders
|
|
254
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Create a rollback plan from a load result.
|
|
258
|
+
*/
|
|
259
|
+
createRollbackPlan(result) {
|
|
260
|
+
const actions = [];
|
|
261
|
+
// Track created project for deletion
|
|
262
|
+
if (result.created?.projectId) {
|
|
263
|
+
actions.push({
|
|
264
|
+
type: 'delete-project',
|
|
265
|
+
description: `Delete created project: ${result.created.projectId}`,
|
|
266
|
+
platform: this.state.target,
|
|
267
|
+
resourceIds: [result.created.projectId],
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
// Track loaded memories for deletion
|
|
271
|
+
if (result.loaded.memories > 0) {
|
|
272
|
+
actions.push({
|
|
273
|
+
type: 'delete-memories',
|
|
274
|
+
description: `Delete ${result.loaded.memories} loaded memories`,
|
|
275
|
+
platform: this.state.target,
|
|
276
|
+
resourceIds: [], // Would be populated by the loader
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
// Track loaded files for deletion
|
|
280
|
+
if (result.loaded.files > 0) {
|
|
281
|
+
actions.push({
|
|
282
|
+
type: 'delete-files',
|
|
283
|
+
description: `Delete ${result.loaded.files} loaded files`,
|
|
284
|
+
platform: this.state.target,
|
|
285
|
+
resourceIds: [], // Would be populated by the loader
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
migrationId: this.state.id,
|
|
290
|
+
actions,
|
|
291
|
+
createdAt: new Date().toISOString(),
|
|
292
|
+
executed: false,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Save rollback plan to disk.
|
|
297
|
+
*/
|
|
298
|
+
async saveRollbackPlan() {
|
|
299
|
+
if (!this.rollbackPlan)
|
|
300
|
+
return;
|
|
301
|
+
const planPath = join(this.workDir, 'rollback-plan.json');
|
|
302
|
+
await writeFile(planPath, JSON.stringify(this.rollbackPlan, null, 2));
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Load rollback plan from disk.
|
|
306
|
+
*/
|
|
307
|
+
async loadRollbackPlan() {
|
|
308
|
+
const planPath = join(this.workDir, 'rollback-plan.json');
|
|
309
|
+
if (existsSync(planPath)) {
|
|
310
|
+
const data = await readFile(planPath, 'utf-8');
|
|
311
|
+
this.rollbackPlan = JSON.parse(data);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* List all migrations in the work directory.
|
|
316
|
+
*/
|
|
317
|
+
static async listMigrations(baseDir) {
|
|
318
|
+
const migrationsDir = baseDir ?? join(process.cwd(), '.savestate', 'migrations');
|
|
319
|
+
if (!existsSync(migrationsDir)) {
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
const migrations = [];
|
|
323
|
+
const entries = await readdir(migrationsDir, { withFileTypes: true });
|
|
324
|
+
for (const entry of entries) {
|
|
325
|
+
if (entry.isDirectory() && entry.name.startsWith('mig_')) {
|
|
326
|
+
const statePath = join(migrationsDir, entry.name, 'state.json');
|
|
327
|
+
if (existsSync(statePath)) {
|
|
328
|
+
try {
|
|
329
|
+
const data = await readFile(statePath, 'utf-8');
|
|
330
|
+
migrations.push(JSON.parse(data));
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
// Skip corrupted state files
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return migrations.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Subscribe to migration events.
|
|
342
|
+
*/
|
|
343
|
+
on(handler) {
|
|
344
|
+
this.eventHandlers.push(handler);
|
|
345
|
+
return () => {
|
|
346
|
+
const index = this.eventHandlers.indexOf(handler);
|
|
347
|
+
if (index >= 0)
|
|
348
|
+
this.eventHandlers.splice(index, 1);
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Get current migration state.
|
|
353
|
+
*/
|
|
354
|
+
getState() {
|
|
355
|
+
return { ...this.state };
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Get the migration bundle (if extracted).
|
|
359
|
+
*/
|
|
360
|
+
getBundle() {
|
|
361
|
+
return this.bundle;
|
|
362
|
+
}
|
|
363
|
+
// ─── Phase Runners ─────────────────────────────────────────
|
|
364
|
+
async runExtractPhase() {
|
|
365
|
+
this.setPhase('extracting');
|
|
366
|
+
this.emit({ type: 'phase:start', phase: 'extracting', message: 'Starting extraction...' });
|
|
367
|
+
const extractor = getExtractor(this.state.source);
|
|
368
|
+
if (!extractor) {
|
|
369
|
+
throw new Error(`No extractor available for ${this.state.source}`);
|
|
370
|
+
}
|
|
371
|
+
const canExtract = await extractor.canExtract();
|
|
372
|
+
if (!canExtract) {
|
|
373
|
+
throw new Error(`Cannot extract from ${this.state.source} - check authentication`);
|
|
374
|
+
}
|
|
375
|
+
this.bundle = await extractor.extract({
|
|
376
|
+
include: this.state.options.include,
|
|
377
|
+
workDir: this.workDir,
|
|
378
|
+
onProgress: (progress, message) => {
|
|
379
|
+
this.state.progress = progress * 0.33; // Extract is 0-33%
|
|
380
|
+
this.emit({ type: 'progress', progress: this.state.progress, message });
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
// Save bundle to disk
|
|
384
|
+
const bundlePath = join(this.workDir, 'bundle.json');
|
|
385
|
+
await writeFile(bundlePath, JSON.stringify(this.bundle, null, 2));
|
|
386
|
+
this.state.bundlePath = bundlePath;
|
|
387
|
+
await this.saveCheckpoint('extracting');
|
|
388
|
+
this.emit({ type: 'phase:complete', phase: 'extracting', message: 'Extraction complete' });
|
|
389
|
+
}
|
|
390
|
+
async runTransformPhase() {
|
|
391
|
+
if (!this.bundle) {
|
|
392
|
+
throw new Error('No bundle to transform - run extract phase first');
|
|
393
|
+
}
|
|
394
|
+
this.setPhase('transforming');
|
|
395
|
+
this.emit({ type: 'phase:start', phase: 'transforming', message: 'Starting transformation...' });
|
|
396
|
+
const transformer = getTransformer(this.state.source, this.state.target);
|
|
397
|
+
if (!transformer) {
|
|
398
|
+
throw new Error(`No transformer available for ${this.state.source} → ${this.state.target}`);
|
|
399
|
+
}
|
|
400
|
+
this.bundle = await transformer.transform(this.bundle, {
|
|
401
|
+
overflowStrategy: 'summarize',
|
|
402
|
+
onProgress: (progress, message) => {
|
|
403
|
+
this.state.progress = 33 + progress * 0.34; // Transform is 33-67%
|
|
404
|
+
this.emit({ type: 'progress', progress: this.state.progress, message });
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
// Update bundle on disk
|
|
408
|
+
const bundlePath = join(this.workDir, 'bundle.json');
|
|
409
|
+
await writeFile(bundlePath, JSON.stringify(this.bundle, null, 2));
|
|
410
|
+
await this.saveCheckpoint('transforming');
|
|
411
|
+
this.emit({ type: 'phase:complete', phase: 'transforming', message: 'Transformation complete' });
|
|
412
|
+
}
|
|
413
|
+
async runLoadPhase() {
|
|
414
|
+
if (!this.bundle) {
|
|
415
|
+
throw new Error('No bundle to load - run extract and transform phases first');
|
|
416
|
+
}
|
|
417
|
+
this.setPhase('loading');
|
|
418
|
+
this.emit({ type: 'phase:start', phase: 'loading', message: 'Starting load...' });
|
|
419
|
+
// Handle dry run
|
|
420
|
+
if (this.state.options.dryRun) {
|
|
421
|
+
const dryRunResult = {
|
|
422
|
+
success: true,
|
|
423
|
+
loaded: {
|
|
424
|
+
instructions: !!this.bundle.contents.instructions,
|
|
425
|
+
memories: this.bundle.contents.memories?.count ?? 0,
|
|
426
|
+
files: this.bundle.contents.files?.count ?? 0,
|
|
427
|
+
customBots: this.bundle.contents.customBots?.count ?? 0,
|
|
428
|
+
},
|
|
429
|
+
warnings: ['Dry run - no changes made'],
|
|
430
|
+
errors: [],
|
|
431
|
+
};
|
|
432
|
+
this.emit({ type: 'phase:complete', phase: 'loading', message: 'Dry run complete' });
|
|
433
|
+
return dryRunResult;
|
|
434
|
+
}
|
|
435
|
+
const loader = getLoader(this.state.target);
|
|
436
|
+
if (!loader) {
|
|
437
|
+
throw new Error(`No loader available for ${this.state.target}`);
|
|
438
|
+
}
|
|
439
|
+
const canLoad = await loader.canLoad();
|
|
440
|
+
if (!canLoad) {
|
|
441
|
+
throw new Error(`Cannot load to ${this.state.target} - check authentication`);
|
|
442
|
+
}
|
|
443
|
+
const result = await loader.load(this.bundle, {
|
|
444
|
+
dryRun: false,
|
|
445
|
+
projectName: `Migrated from ${this.state.source} (${new Date().toISOString().split('T')[0]})`,
|
|
446
|
+
onProgress: (progress, message) => {
|
|
447
|
+
this.state.progress = 67 + progress * 0.33; // Load is 67-100%
|
|
448
|
+
this.emit({ type: 'progress', progress: this.state.progress, message });
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
// Create rollback plan for successful loads
|
|
452
|
+
if (result.success) {
|
|
453
|
+
this.rollbackPlan = this.createRollbackPlan(result);
|
|
454
|
+
await this.saveRollbackPlan();
|
|
455
|
+
}
|
|
456
|
+
await this.saveCheckpoint('loading');
|
|
457
|
+
this.emit({ type: 'phase:complete', phase: 'loading', message: 'Load complete', data: result });
|
|
458
|
+
return result;
|
|
459
|
+
}
|
|
460
|
+
// ─── Helpers ───────────────────────────────────────────────
|
|
461
|
+
generateId() {
|
|
462
|
+
return `mig_${randomBytes(8).toString('hex')}`;
|
|
463
|
+
}
|
|
464
|
+
async ensureWorkDir() {
|
|
465
|
+
await mkdir(this.workDir, { recursive: true });
|
|
466
|
+
}
|
|
467
|
+
setPhase(phase) {
|
|
468
|
+
this.state.phase = phase;
|
|
469
|
+
this.state.phaseStartedAt = new Date().toISOString();
|
|
470
|
+
}
|
|
471
|
+
async saveState() {
|
|
472
|
+
const statePath = join(this.workDir, 'state.json');
|
|
473
|
+
await writeFile(statePath, JSON.stringify(this.state, null, 2));
|
|
474
|
+
}
|
|
475
|
+
async saveCheckpoint(phase) {
|
|
476
|
+
const checkpointId = `checkpoint_${phase}_${Date.now()}`;
|
|
477
|
+
const checkpointPath = join(this.workDir, `${checkpointId}.json`);
|
|
478
|
+
const checkpointData = {
|
|
479
|
+
phase,
|
|
480
|
+
bundle: this.bundle,
|
|
481
|
+
state: this.state,
|
|
482
|
+
};
|
|
483
|
+
const dataStr = JSON.stringify(checkpointData);
|
|
484
|
+
await writeFile(checkpointPath, dataStr);
|
|
485
|
+
const checksum = createHash('sha256').update(dataStr).digest('hex');
|
|
486
|
+
const checkpoint = {
|
|
487
|
+
phase,
|
|
488
|
+
timestamp: new Date().toISOString(),
|
|
489
|
+
dataPath: checkpointPath,
|
|
490
|
+
checksum,
|
|
491
|
+
};
|
|
492
|
+
this.state.checkpoints.push(checkpoint);
|
|
493
|
+
await this.saveState();
|
|
494
|
+
this.emit({ type: 'checkpoint', phase, message: `Checkpoint saved: ${checkpointId}` });
|
|
495
|
+
}
|
|
496
|
+
async loadCheckpoint(checkpoint) {
|
|
497
|
+
const dataStr = await readFile(checkpoint.dataPath, 'utf-8');
|
|
498
|
+
// Verify checksum
|
|
499
|
+
const actualChecksum = createHash('sha256').update(dataStr).digest('hex');
|
|
500
|
+
if (actualChecksum !== checkpoint.checksum) {
|
|
501
|
+
throw new Error(`Checkpoint corrupted: ${checkpoint.dataPath}`);
|
|
502
|
+
}
|
|
503
|
+
const data = JSON.parse(dataStr);
|
|
504
|
+
this.bundle = data.bundle;
|
|
505
|
+
// Don't overwrite current state - we want to keep progress info
|
|
506
|
+
}
|
|
507
|
+
emit(event) {
|
|
508
|
+
for (const handler of this.eventHandlers) {
|
|
509
|
+
try {
|
|
510
|
+
handler(event);
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
// Don't let handler errors break the migration
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
//# sourceMappingURL=orchestrator.js.map
|