ccmv 1.0.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/CLAUDE.md +159 -0
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/bin/ccmv.js +5 -0
- package/docs/ARCHITECTURE.md +438 -0
- package/lib/claude.js +311 -0
- package/lib/cursor.js +582 -0
- package/lib/index.js +352 -0
- package/lib/logger.js +60 -0
- package/lib/utils.js +93 -0
- package/package.json +36 -0
package/lib/claude.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, cpSync, rmSync, renameSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { logInfo, logOk, logError, logBackup, logFile, logReplace, logRename, logDryrun, logRollback } from './logger.js';
|
|
5
|
+
import { encodePath, formatTimestamp } from './utils.js';
|
|
6
|
+
|
|
7
|
+
const CLAUDE_DIR = join(homedir(), '.claude');
|
|
8
|
+
const PROJECTS_DIR = join(CLAUDE_DIR, 'projects');
|
|
9
|
+
const HISTORY_FILE = join(CLAUDE_DIR, 'history.jsonl');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check if Claude Code project data exists
|
|
13
|
+
* Returns true if exists, false if not (but doesn't fail - Cursor-only migration is OK)
|
|
14
|
+
*/
|
|
15
|
+
export function checkClaudeExists(encodedOld) {
|
|
16
|
+
const oldProjectDir = join(PROJECTS_DIR, encodedOld);
|
|
17
|
+
|
|
18
|
+
if (existsSync(oldProjectDir)) {
|
|
19
|
+
logOk(`Claude project dir exists: ${oldProjectDir.replace(homedir(), '~')}/`);
|
|
20
|
+
return true;
|
|
21
|
+
} else {
|
|
22
|
+
logInfo(`No Claude project data found (OK if Cursor-only)`);
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create backup of Claude data
|
|
29
|
+
*/
|
|
30
|
+
export function createBackup(encodedOld, dryRun, claudeExists) {
|
|
31
|
+
const timestamp = formatTimestamp();
|
|
32
|
+
const backupDir = join(CLAUDE_DIR, 'backups', `ccmv-${timestamp}`);
|
|
33
|
+
|
|
34
|
+
logBackup('Creating backups...');
|
|
35
|
+
|
|
36
|
+
if (dryRun) {
|
|
37
|
+
logDryrun(`Would create backup at: ${backupDir.replace(homedir(), '~')}/`);
|
|
38
|
+
return backupDir;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
mkdirSync(join(backupDir, 'projects'), { recursive: true });
|
|
42
|
+
|
|
43
|
+
// Backup project directory (only if Claude data exists)
|
|
44
|
+
if (claudeExists) {
|
|
45
|
+
const oldProjectDir = join(PROJECTS_DIR, encodedOld);
|
|
46
|
+
if (existsSync(oldProjectDir)) {
|
|
47
|
+
cpSync(oldProjectDir, join(backupDir, 'projects', encodedOld), { recursive: true });
|
|
48
|
+
logBackup(`${oldProjectDir.replace(homedir(), '~')}/ -> ${backupDir.replace(homedir(), '~')}/projects/`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Backup history.jsonl
|
|
53
|
+
if (existsSync(HISTORY_FILE)) {
|
|
54
|
+
cpSync(HISTORY_FILE, join(backupDir, 'history.jsonl'));
|
|
55
|
+
logBackup(`${HISTORY_FILE.replace(homedir(), '~')} -> ${backupDir.replace(homedir(), '~')}/history.jsonl`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log('');
|
|
59
|
+
return backupDir;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Rename Claude projects directory
|
|
64
|
+
*/
|
|
65
|
+
export function renameClaudeDir(encodedOld, encodedNew, dryRun, claudeExists) {
|
|
66
|
+
if (!claudeExists) return;
|
|
67
|
+
|
|
68
|
+
const oldProjectDir = join(PROJECTS_DIR, encodedOld);
|
|
69
|
+
const newProjectDir = join(PROJECTS_DIR, encodedNew);
|
|
70
|
+
|
|
71
|
+
if (dryRun) {
|
|
72
|
+
logDryrun(`Would rename: ${oldProjectDir.replace(homedir(), '~')}/ -> ${newProjectDir.replace(homedir(), '~')}/`);
|
|
73
|
+
console.log('');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
logRename(`${oldProjectDir.replace(homedir(), '~')}/ -> ${newProjectDir.replace(homedir(), '~')}/`);
|
|
78
|
+
renameSync(oldProjectDir, newProjectDir);
|
|
79
|
+
console.log('');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Update all files containing cwd field in Claude projects directory
|
|
84
|
+
*/
|
|
85
|
+
export function updateProjectFiles(oldPath, newPath, encodedOld, encodedNew, dryRun, claudeExists) {
|
|
86
|
+
if (!claudeExists) return;
|
|
87
|
+
|
|
88
|
+
// For dry-run, use old path since rename hasn't happened yet
|
|
89
|
+
const projectDir = dryRun
|
|
90
|
+
? join(PROJECTS_DIR, encodedOld)
|
|
91
|
+
: join(PROJECTS_DIR, encodedNew);
|
|
92
|
+
|
|
93
|
+
logInfo('Updating project files...');
|
|
94
|
+
|
|
95
|
+
const cwdPattern = `"cwd":"${oldPath}"`;
|
|
96
|
+
const cwdReplacement = `"cwd":"${newPath}"`;
|
|
97
|
+
|
|
98
|
+
const filesToUpdate = findFilesWithContent(projectDir, cwdPattern);
|
|
99
|
+
|
|
100
|
+
if (filesToUpdate.length === 0) {
|
|
101
|
+
logInfo('No files with cwd field found');
|
|
102
|
+
console.log('');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const file of filesToUpdate) {
|
|
107
|
+
const relpath = relative(projectDir, file);
|
|
108
|
+
const content = readFileSync(file, 'utf-8');
|
|
109
|
+
const count = (content.match(new RegExp(escapeRegex(cwdPattern), 'g')) || []).length;
|
|
110
|
+
|
|
111
|
+
if (count > 0) {
|
|
112
|
+
logFile(relpath);
|
|
113
|
+
|
|
114
|
+
if (dryRun) {
|
|
115
|
+
logReplace(`${count} occurrences would be updated`);
|
|
116
|
+
} else {
|
|
117
|
+
const newContent = content.split(cwdPattern).join(cwdReplacement);
|
|
118
|
+
writeFileSync(file, newContent);
|
|
119
|
+
logReplace(`${count} occurrences updated`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log('');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Update global history file
|
|
129
|
+
*/
|
|
130
|
+
export function updateHistory(oldPath, newPath, dryRun) {
|
|
131
|
+
if (!existsSync(HISTORY_FILE)) {
|
|
132
|
+
logInfo('No global history file found');
|
|
133
|
+
console.log('');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
logInfo('Updating global history...');
|
|
138
|
+
|
|
139
|
+
const cwdPattern = `"cwd":"${oldPath}"`;
|
|
140
|
+
const cwdReplacement = `"cwd":"${newPath}"`;
|
|
141
|
+
|
|
142
|
+
const content = readFileSync(HISTORY_FILE, 'utf-8');
|
|
143
|
+
const count = (content.match(new RegExp(escapeRegex(cwdPattern), 'g')) || []).length;
|
|
144
|
+
|
|
145
|
+
if (count > 0) {
|
|
146
|
+
if (dryRun) {
|
|
147
|
+
logReplace(`${count} entries would be updated`);
|
|
148
|
+
} else {
|
|
149
|
+
const newContent = content.split(cwdPattern).join(cwdReplacement);
|
|
150
|
+
writeFileSync(HISTORY_FILE, newContent);
|
|
151
|
+
logReplace(`${count} entries updated`);
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
logInfo('No matching entries found in history');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log('');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Verify the migration
|
|
162
|
+
*/
|
|
163
|
+
export function verifyClaude(oldPath, encodedNew, dryRun, claudeExists) {
|
|
164
|
+
if (!claudeExists) return true;
|
|
165
|
+
|
|
166
|
+
logInfo('Verifying Claude migration...');
|
|
167
|
+
|
|
168
|
+
if (dryRun) {
|
|
169
|
+
logDryrun('Verification skipped in dry-run mode');
|
|
170
|
+
console.log('');
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const newProjectDir = join(PROJECTS_DIR, encodedNew);
|
|
175
|
+
|
|
176
|
+
// Check new Claude projects dir exists
|
|
177
|
+
if (existsSync(newProjectDir)) {
|
|
178
|
+
logOk('New Claude project dir exists');
|
|
179
|
+
} else {
|
|
180
|
+
logError('New Claude project dir not found!');
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check JSONL files are valid (spot check first file)
|
|
185
|
+
const jsonlFiles = findFiles(newProjectDir, /\.jsonl$/);
|
|
186
|
+
if (jsonlFiles.length > 0) {
|
|
187
|
+
const firstJsonl = jsonlFiles[0];
|
|
188
|
+
try {
|
|
189
|
+
const content = readFileSync(firstJsonl, 'utf-8');
|
|
190
|
+
const firstLine = content.split('\n')[0];
|
|
191
|
+
if (firstLine) {
|
|
192
|
+
JSON.parse(firstLine);
|
|
193
|
+
}
|
|
194
|
+
logOk('JSONL files appear valid');
|
|
195
|
+
} catch {
|
|
196
|
+
logError(`JSONL file may be corrupted: ${firstJsonl}`);
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Verify no old path references remain
|
|
202
|
+
const cwdPattern = `"cwd":"${oldPath}"`;
|
|
203
|
+
const filesWithOldPath = findFilesWithContent(newProjectDir, cwdPattern);
|
|
204
|
+
let remaining = 0;
|
|
205
|
+
|
|
206
|
+
for (const file of filesWithOldPath) {
|
|
207
|
+
const content = readFileSync(file, 'utf-8');
|
|
208
|
+
remaining += (content.match(new RegExp(escapeRegex(cwdPattern), 'g')) || []).length;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (remaining > 0) {
|
|
212
|
+
logError(`Found ${remaining} remaining references to old path!`);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
logOk('No stale path references found');
|
|
216
|
+
|
|
217
|
+
console.log('');
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Rollback Claude data from backup
|
|
223
|
+
*/
|
|
224
|
+
export function rollbackClaude(backupDir, encodedOld, encodedNew) {
|
|
225
|
+
// Restore history.jsonl
|
|
226
|
+
const backupHistory = join(backupDir, 'history.jsonl');
|
|
227
|
+
if (existsSync(backupHistory)) {
|
|
228
|
+
cpSync(backupHistory, HISTORY_FILE);
|
|
229
|
+
logRollback('Restored history.jsonl');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Restore project dir
|
|
233
|
+
const backupProjectDir = join(backupDir, 'projects', encodedOld);
|
|
234
|
+
const oldProjectDir = join(PROJECTS_DIR, encodedOld);
|
|
235
|
+
const newProjectDir = join(PROJECTS_DIR, encodedNew);
|
|
236
|
+
|
|
237
|
+
if (existsSync(backupProjectDir)) {
|
|
238
|
+
if (existsSync(newProjectDir)) {
|
|
239
|
+
rmSync(newProjectDir, { recursive: true });
|
|
240
|
+
}
|
|
241
|
+
cpSync(backupProjectDir, oldProjectDir, { recursive: true });
|
|
242
|
+
logRollback('Restored project directory');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Remove backup directory
|
|
248
|
+
*/
|
|
249
|
+
export function removeBackup(backupDir) {
|
|
250
|
+
if (backupDir && existsSync(backupDir)) {
|
|
251
|
+
rmSync(backupDir, { recursive: true });
|
|
252
|
+
logInfo('Backup removed');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Helper functions
|
|
257
|
+
|
|
258
|
+
function escapeRegex(string) {
|
|
259
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function findFiles(dir, pattern) {
|
|
263
|
+
const results = [];
|
|
264
|
+
|
|
265
|
+
function walk(currentDir) {
|
|
266
|
+
if (!existsSync(currentDir)) return;
|
|
267
|
+
|
|
268
|
+
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
269
|
+
for (const entry of entries) {
|
|
270
|
+
const fullPath = join(currentDir, entry.name);
|
|
271
|
+
if (entry.isDirectory()) {
|
|
272
|
+
walk(fullPath);
|
|
273
|
+
} else if (pattern.test(entry.name)) {
|
|
274
|
+
results.push(fullPath);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
walk(dir);
|
|
280
|
+
return results;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function findFilesWithContent(dir, searchString) {
|
|
284
|
+
const results = [];
|
|
285
|
+
|
|
286
|
+
function walk(currentDir) {
|
|
287
|
+
if (!existsSync(currentDir)) return;
|
|
288
|
+
|
|
289
|
+
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
290
|
+
for (const entry of entries) {
|
|
291
|
+
const fullPath = join(currentDir, entry.name);
|
|
292
|
+
if (entry.isDirectory()) {
|
|
293
|
+
walk(fullPath);
|
|
294
|
+
} else {
|
|
295
|
+
try {
|
|
296
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
297
|
+
if (content.includes(searchString)) {
|
|
298
|
+
results.push(fullPath);
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
// Skip files that can't be read
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
walk(dir);
|
|
308
|
+
return results;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export { CLAUDE_DIR, PROJECTS_DIR, HISTORY_FILE };
|