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/index.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { existsSync, renameSync, rmSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import {
|
|
4
|
+
logInfo,
|
|
5
|
+
logOk,
|
|
6
|
+
logCheck,
|
|
7
|
+
logError,
|
|
8
|
+
logDone,
|
|
9
|
+
logDryrun,
|
|
10
|
+
logRollback,
|
|
11
|
+
setVerbose,
|
|
12
|
+
} from './logger.js';
|
|
13
|
+
import {
|
|
14
|
+
encodePath,
|
|
15
|
+
pathToFileUri,
|
|
16
|
+
resolvePath,
|
|
17
|
+
resolveNewPath,
|
|
18
|
+
} from './utils.js';
|
|
19
|
+
import {
|
|
20
|
+
checkClaudeExists,
|
|
21
|
+
createBackup,
|
|
22
|
+
renameClaudeDir,
|
|
23
|
+
updateProjectFiles,
|
|
24
|
+
updateHistory,
|
|
25
|
+
verifyClaude,
|
|
26
|
+
rollbackClaude,
|
|
27
|
+
removeBackup,
|
|
28
|
+
} from './claude.js';
|
|
29
|
+
import {
|
|
30
|
+
detectCursor,
|
|
31
|
+
checkCursorNotRunning,
|
|
32
|
+
findCursorWorkspaces,
|
|
33
|
+
backupCursorData,
|
|
34
|
+
renameCursorWorkspace,
|
|
35
|
+
updateCursorStorageJson,
|
|
36
|
+
updateCursorStateVscdb,
|
|
37
|
+
updateCursorWorkspaceStorage,
|
|
38
|
+
verifyCursorMigration,
|
|
39
|
+
rollbackCursor,
|
|
40
|
+
} from './cursor.js';
|
|
41
|
+
|
|
42
|
+
const USAGE = `Usage: ccmv [OPTIONS] <old-path> <new-path>
|
|
43
|
+
|
|
44
|
+
Moves a project directory and updates all Claude Code references.
|
|
45
|
+
If Cursor is installed, also updates Cursor's workspace data automatically.
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
--refs-only Only update Claude's references (don't move the actual directory)
|
|
49
|
+
--dry-run Show what would be done without making changes
|
|
50
|
+
--keep-backup Don't remove backups after successful migration
|
|
51
|
+
--no-cursor Skip Cursor data updates even if Cursor is installed
|
|
52
|
+
--verbose Show detailed logs (default: on)
|
|
53
|
+
--quiet Suppress detailed logs
|
|
54
|
+
--help Show this help message
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
ccmv /Users/jane/old-project /Users/jane/new-project
|
|
58
|
+
ccmv --dry-run ~/projects/myapp ~/work/myapp
|
|
59
|
+
ccmv --refs-only /old/path /new/path
|
|
60
|
+
ccmv --no-cursor ~/proj-a ~/proj-b
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse command line arguments
|
|
65
|
+
*/
|
|
66
|
+
function parseArgs(args) {
|
|
67
|
+
const options = {
|
|
68
|
+
dryRun: false,
|
|
69
|
+
refsOnly: false,
|
|
70
|
+
keepBackup: false,
|
|
71
|
+
verbose: true,
|
|
72
|
+
noCursor: false,
|
|
73
|
+
help: false,
|
|
74
|
+
paths: [],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
for (const arg of args) {
|
|
78
|
+
switch (arg) {
|
|
79
|
+
case '--dry-run':
|
|
80
|
+
options.dryRun = true;
|
|
81
|
+
break;
|
|
82
|
+
case '--refs-only':
|
|
83
|
+
options.refsOnly = true;
|
|
84
|
+
break;
|
|
85
|
+
case '--keep-backup':
|
|
86
|
+
options.keepBackup = true;
|
|
87
|
+
break;
|
|
88
|
+
case '--verbose':
|
|
89
|
+
options.verbose = true;
|
|
90
|
+
break;
|
|
91
|
+
case '--quiet':
|
|
92
|
+
options.verbose = false;
|
|
93
|
+
break;
|
|
94
|
+
case '--no-cursor':
|
|
95
|
+
options.noCursor = true;
|
|
96
|
+
break;
|
|
97
|
+
case '--help':
|
|
98
|
+
case '-h':
|
|
99
|
+
options.help = true;
|
|
100
|
+
break;
|
|
101
|
+
default:
|
|
102
|
+
if (arg.startsWith('-')) {
|
|
103
|
+
logError(`Unknown option: ${arg}`);
|
|
104
|
+
console.log('Use --help for usage information');
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
options.paths.push(arg);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return options;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Validate paths and environment
|
|
116
|
+
*/
|
|
117
|
+
function validate(oldPath, newPath, refsOnly) {
|
|
118
|
+
logCheck('Validating paths...');
|
|
119
|
+
|
|
120
|
+
// Check old path exists
|
|
121
|
+
if (!existsSync(oldPath)) {
|
|
122
|
+
if (refsOnly) {
|
|
123
|
+
logInfo("Old path doesn't exist (OK for --refs-only mode)");
|
|
124
|
+
} else {
|
|
125
|
+
logError(`Old path does not exist: ${oldPath}`);
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
logOk('Old path exists');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check new path doesn't exist (unless refs-only)
|
|
133
|
+
if (!refsOnly) {
|
|
134
|
+
if (existsSync(newPath)) {
|
|
135
|
+
logError(`New path already exists: ${newPath}`);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
logOk('New path does not exist');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log('');
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Move the actual project directory
|
|
147
|
+
*/
|
|
148
|
+
function moveProject(oldPath, newPath, refsOnly, dryRun) {
|
|
149
|
+
if (refsOnly) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (dryRun) {
|
|
154
|
+
logDryrun(`Would move: ${oldPath} -> ${newPath}`);
|
|
155
|
+
console.log('');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
logInfo('Moving project directory...');
|
|
160
|
+
renameSync(oldPath, newPath);
|
|
161
|
+
logOk(`Moved: ${oldPath} -> ${newPath}`);
|
|
162
|
+
console.log('');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Rollback on error
|
|
167
|
+
*/
|
|
168
|
+
function rollback(backupDir, oldPath, newPath, encodedOld, encodedNew, refsOnly, cursorDetected) {
|
|
169
|
+
if (!backupDir || !existsSync(backupDir)) {
|
|
170
|
+
logError('No backup available for rollback');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
logError('Migration failed! Rolling back...');
|
|
175
|
+
|
|
176
|
+
// Rollback Claude data
|
|
177
|
+
rollbackClaude(backupDir, encodedOld, encodedNew);
|
|
178
|
+
|
|
179
|
+
// Restore original project location if moved
|
|
180
|
+
if (!refsOnly) {
|
|
181
|
+
if (existsSync(newPath) && !existsSync(oldPath)) {
|
|
182
|
+
renameSync(newPath, oldPath);
|
|
183
|
+
logRollback('Moved project back to original location');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Rollback Cursor data
|
|
188
|
+
if (cursorDetected) {
|
|
189
|
+
rollbackCursor(backupDir);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
logRollback('Complete. Your project is unchanged.');
|
|
193
|
+
console.log('');
|
|
194
|
+
console.log(`Backup kept at: ${backupDir}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Main entry point
|
|
199
|
+
*/
|
|
200
|
+
export async function main(args) {
|
|
201
|
+
const options = parseArgs(args);
|
|
202
|
+
|
|
203
|
+
if (options.help) {
|
|
204
|
+
console.log(USAGE);
|
|
205
|
+
process.exit(0);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (options.paths.length !== 2) {
|
|
209
|
+
logError('Expected 2 arguments: old-path and new-path');
|
|
210
|
+
console.log('Use --help for usage information');
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
setVerbose(options.verbose);
|
|
215
|
+
|
|
216
|
+
// Resolve paths
|
|
217
|
+
const oldPath = resolvePath(options.paths[0]);
|
|
218
|
+
const newPath = resolveNewPath(options.paths[1]);
|
|
219
|
+
|
|
220
|
+
// Encode paths for Claude
|
|
221
|
+
const encodedOld = encodePath(oldPath);
|
|
222
|
+
const encodedNew = encodePath(newPath);
|
|
223
|
+
|
|
224
|
+
// Generate file:// URIs for Cursor
|
|
225
|
+
const oldFileUri = pathToFileUri(oldPath);
|
|
226
|
+
const newFileUri = pathToFileUri(newPath);
|
|
227
|
+
|
|
228
|
+
// Display info
|
|
229
|
+
console.log('');
|
|
230
|
+
logInfo(`Old path: ${oldPath}`);
|
|
231
|
+
logInfo(`New path: ${newPath}`);
|
|
232
|
+
logInfo(`Encoded old: ${encodedOld}`);
|
|
233
|
+
logInfo(`Encoded new: ${encodedNew}`);
|
|
234
|
+
|
|
235
|
+
if (options.dryRun) {
|
|
236
|
+
console.log('');
|
|
237
|
+
logDryrun('Running in dry-run mode - no changes will be made');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (options.refsOnly) {
|
|
241
|
+
console.log('');
|
|
242
|
+
logInfo('Running in refs-only mode - project directory will not be moved');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
console.log('');
|
|
246
|
+
|
|
247
|
+
// State for rollback
|
|
248
|
+
let backupDir = null;
|
|
249
|
+
let cursorDetected = false;
|
|
250
|
+
let claudeExists = false;
|
|
251
|
+
let cursorWorkspaceInfo = { oldWorkspaceHash: null, workspaceDirs: [] };
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
// Validate paths
|
|
255
|
+
if (!validate(oldPath, newPath, options.refsOnly)) {
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check Claude data exists
|
|
260
|
+
claudeExists = checkClaudeExists(encodedOld);
|
|
261
|
+
|
|
262
|
+
// Detect and check Cursor
|
|
263
|
+
cursorDetected = detectCursor(options.noCursor);
|
|
264
|
+
if (!checkCursorNotRunning(cursorDetected, options.dryRun)) {
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Find Cursor workspaces
|
|
269
|
+
cursorWorkspaceInfo = findCursorWorkspaces(cursorDetected, oldPath);
|
|
270
|
+
|
|
271
|
+
// Fail if neither Claude nor Cursor data exists
|
|
272
|
+
const hasCursorData = cursorWorkspaceInfo.workspaceDirs.length > 0;
|
|
273
|
+
if (!claudeExists && !hasCursorData) {
|
|
274
|
+
logError('No Claude Code or Cursor data found for this project.');
|
|
275
|
+
logError('Use regular mv command to move the directory.');
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Create backups
|
|
280
|
+
backupDir = createBackup(encodedOld, options.dryRun, claudeExists);
|
|
281
|
+
backupCursorData(cursorDetected, cursorWorkspaceInfo.workspaceDirs, backupDir, options.dryRun);
|
|
282
|
+
|
|
283
|
+
// Move project
|
|
284
|
+
moveProject(oldPath, newPath, options.refsOnly, options.dryRun);
|
|
285
|
+
|
|
286
|
+
// Rename Cursor workspace (after move, uses new birthtime)
|
|
287
|
+
const renamedWorkspace = renameCursorWorkspace(
|
|
288
|
+
cursorDetected,
|
|
289
|
+
cursorWorkspaceInfo.oldWorkspaceHash,
|
|
290
|
+
newPath,
|
|
291
|
+
oldFileUri,
|
|
292
|
+
newFileUri,
|
|
293
|
+
options.dryRun
|
|
294
|
+
);
|
|
295
|
+
if (renamedWorkspace.workspaceDirs.length > 0) {
|
|
296
|
+
cursorWorkspaceInfo.workspaceDirs = renamedWorkspace.workspaceDirs;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Rename Claude dir
|
|
300
|
+
renameClaudeDir(encodedOld, encodedNew, options.dryRun, claudeExists);
|
|
301
|
+
|
|
302
|
+
// Update files
|
|
303
|
+
updateProjectFiles(oldPath, newPath, encodedOld, encodedNew, options.dryRun, claudeExists);
|
|
304
|
+
updateHistory(oldPath, newPath, options.dryRun);
|
|
305
|
+
|
|
306
|
+
// Update Cursor data
|
|
307
|
+
updateCursorStorageJson(cursorDetected, oldPath, newPath, oldFileUri, newFileUri, options.dryRun);
|
|
308
|
+
await updateCursorStateVscdb(cursorDetected, oldPath, newPath, oldFileUri, newFileUri, options.dryRun);
|
|
309
|
+
await updateCursorWorkspaceStorage(
|
|
310
|
+
cursorDetected,
|
|
311
|
+
cursorWorkspaceInfo.workspaceDirs,
|
|
312
|
+
oldPath,
|
|
313
|
+
newPath,
|
|
314
|
+
oldFileUri,
|
|
315
|
+
newFileUri,
|
|
316
|
+
options.dryRun
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// Verify
|
|
320
|
+
if (!verifyClaude(oldPath, encodedNew, options.dryRun, claudeExists)) {
|
|
321
|
+
throw new Error('Claude verification failed');
|
|
322
|
+
}
|
|
323
|
+
if (!verifyCursorMigration(cursorDetected, cursorWorkspaceInfo.workspaceDirs, oldPath, newPath, options.dryRun)) {
|
|
324
|
+
throw new Error('Cursor verification failed');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Cleanup
|
|
328
|
+
if (!options.dryRun) {
|
|
329
|
+
if (options.keepBackup) {
|
|
330
|
+
logInfo(`Backup kept at: ${backupDir.replace(homedir(), '~')}/`);
|
|
331
|
+
} else {
|
|
332
|
+
removeBackup(backupDir);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Success
|
|
337
|
+
if (options.dryRun) {
|
|
338
|
+
logDone('Dry-run complete! No changes were made.');
|
|
339
|
+
} else {
|
|
340
|
+
logDone('Migration complete!');
|
|
341
|
+
if (!options.refsOnly) {
|
|
342
|
+
logInfo(`Your project is now at: ${newPath}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} catch (err) {
|
|
346
|
+
if (!options.dryRun && backupDir) {
|
|
347
|
+
rollback(backupDir, oldPath, newPath, encodedOld, encodedNew, options.refsOnly, cursorDetected);
|
|
348
|
+
}
|
|
349
|
+
logError(err.message);
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
}
|
package/lib/logger.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// ANSI color codes
|
|
2
|
+
const colors = {
|
|
3
|
+
red: '\x1b[0;31m',
|
|
4
|
+
green: '\x1b[0;32m',
|
|
5
|
+
yellow: '\x1b[0;33m',
|
|
6
|
+
blue: '\x1b[0;34m',
|
|
7
|
+
reset: '\x1b[0m',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
let verbose = true;
|
|
11
|
+
|
|
12
|
+
export function setVerbose(value) {
|
|
13
|
+
verbose = value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function logInfo(message) {
|
|
17
|
+
console.log(`${colors.blue}[INFO]${colors.reset} ${message}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function logOk(message) {
|
|
21
|
+
console.log(`${colors.green}[OK]${colors.reset} ${message}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function logCheck(message) {
|
|
25
|
+
console.log(`${colors.yellow}[CHECK]${colors.reset} ${message}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function logBackup(message) {
|
|
29
|
+
console.log(`${colors.yellow}[BACKUP]${colors.reset} ${message}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function logFile(message) {
|
|
33
|
+
console.log(`${colors.blue}[FILE]${colors.reset} ${message}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function logReplace(message) {
|
|
37
|
+
if (verbose) {
|
|
38
|
+
console.log(` ${colors.green}[REPLACE]${colors.reset} ${message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function logRename(message) {
|
|
43
|
+
console.log(`${colors.blue}[RENAME]${colors.reset} ${message}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function logDone(message) {
|
|
47
|
+
console.log(`${colors.green}[DONE]${colors.reset} ${message}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function logError(message) {
|
|
51
|
+
console.error(`${colors.red}[ERROR]${colors.reset} ${message}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function logRollback(message) {
|
|
55
|
+
console.log(`${colors.yellow}[ROLLBACK]${colors.reset} ${message}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function logDryrun(message) {
|
|
59
|
+
console.log(`${colors.yellow}[DRY-RUN]${colors.reset} ${message}`);
|
|
60
|
+
}
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { statSync, existsSync } from 'node:fs';
|
|
3
|
+
import { resolve, dirname, basename } from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Encode path for Claude projects directory
|
|
7
|
+
* Rule: / -> -, : -> -, space -> -
|
|
8
|
+
*/
|
|
9
|
+
export function encodePath(path) {
|
|
10
|
+
return path.replace(/[\/: ]/g, '-');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Convert filesystem path to file:// URI with URL encoding
|
|
15
|
+
*/
|
|
16
|
+
export function pathToFileUri(path) {
|
|
17
|
+
// URL encode the path, keeping / as safe character
|
|
18
|
+
const encoded = path
|
|
19
|
+
.split('/')
|
|
20
|
+
.map((segment) => encodeURIComponent(segment))
|
|
21
|
+
.join('/');
|
|
22
|
+
return `file://${encoded}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Calculate VSCode/Cursor workspace hash from path
|
|
27
|
+
* Hash = MD5(path + birthtime_ms)
|
|
28
|
+
*/
|
|
29
|
+
export function getWorkspaceHash(path) {
|
|
30
|
+
if (!existsSync(path)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const stat = statSync(path);
|
|
36
|
+
const birthtimeMs = stat.birthtime.getTime();
|
|
37
|
+
const hash = createHash('md5')
|
|
38
|
+
.update(path)
|
|
39
|
+
.update(String(birthtimeMs))
|
|
40
|
+
.digest('hex');
|
|
41
|
+
return hash;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve path to absolute, handling relative paths
|
|
49
|
+
*/
|
|
50
|
+
export function resolvePath(inputPath) {
|
|
51
|
+
// Expand ~ to home directory
|
|
52
|
+
if (inputPath.startsWith('~')) {
|
|
53
|
+
inputPath = inputPath.replace(/^~/, process.env.HOME || '');
|
|
54
|
+
}
|
|
55
|
+
return resolve(inputPath).replace(/\/$/, ''); // Remove trailing slash
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve new path (which may not exist yet)
|
|
60
|
+
*/
|
|
61
|
+
export function resolveNewPath(inputPath) {
|
|
62
|
+
// Expand ~ to home directory
|
|
63
|
+
if (inputPath.startsWith('~')) {
|
|
64
|
+
inputPath = inputPath.replace(/^~/, process.env.HOME || '');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const parent = dirname(inputPath);
|
|
68
|
+
const name = basename(inputPath);
|
|
69
|
+
|
|
70
|
+
// Resolve parent (which should exist)
|
|
71
|
+
const resolvedParent = resolve(parent);
|
|
72
|
+
return `${resolvedParent}/${name}`.replace(/\/$/, '');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Convert path to tilde form if under home directory
|
|
77
|
+
*/
|
|
78
|
+
export function pathToTilde(path) {
|
|
79
|
+
const home = process.env.HOME || '';
|
|
80
|
+
if (path.startsWith(home)) {
|
|
81
|
+
return path.replace(home, '~');
|
|
82
|
+
}
|
|
83
|
+
return path;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Format timestamp for backup directory
|
|
88
|
+
*/
|
|
89
|
+
export function formatTimestamp() {
|
|
90
|
+
const now = new Date();
|
|
91
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
92
|
+
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
93
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ccmv",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Claude Code project directory migration tool",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ccmv": "./bin/ccmv.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"claude",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"migration",
|
|
17
|
+
"cursor",
|
|
18
|
+
"vscode"
|
|
19
|
+
],
|
|
20
|
+
"author": "Saqoosha",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/saqoosha/ccmv.git"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/saqoosha/ccmv/issues"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/saqoosha/ccmv#readme",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"better-sqlite3": "^11.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|