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/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 };