@rhseung/claude-mv 0.1.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/LICENSE +21 -0
- package/README.md +151 -0
- package/dist/chunk-JJSX2UL6.js +187 -0
- package/dist/index.js +2801 -0
- package/dist/reporter-4S4IPLQW.js +195 -0
- package/package.json +91 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2801 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
PlainReporter
|
|
4
|
+
} from "./chunk-JJSX2UL6.js";
|
|
5
|
+
|
|
6
|
+
// src/index.ts
|
|
7
|
+
import { existsSync as existsSync8, readdirSync as readdirSync5 } from "fs";
|
|
8
|
+
import { homedir as homedir2 } from "os";
|
|
9
|
+
import { join as join12 } from "path";
|
|
10
|
+
|
|
11
|
+
// package.json
|
|
12
|
+
var package_default = {
|
|
13
|
+
name: "@rhseung/claude-mv",
|
|
14
|
+
version: "0.1.0",
|
|
15
|
+
description: "\uD504\uB85C\uC81D\uD2B8 \uB514\uB809\uD130\uB9AC\uB97C \uC62E\uAE30\uBA74\uC11C Claude Code \uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC\uAE4C\uC9C0 \uAC19\uC774 \uC62E\uAE41\uB2C8\uB2E4",
|
|
16
|
+
type: "module",
|
|
17
|
+
license: "MIT",
|
|
18
|
+
author: "rhseung <ryu@rhseung.me>",
|
|
19
|
+
homepage: "https://github.com/rhseung/claude-mv#readme",
|
|
20
|
+
repository: {
|
|
21
|
+
type: "git",
|
|
22
|
+
url: "git+https://github.com/rhseung/claude-mv.git"
|
|
23
|
+
},
|
|
24
|
+
bugs: {
|
|
25
|
+
url: "https://github.com/rhseung/claude-mv/issues"
|
|
26
|
+
},
|
|
27
|
+
keywords: [
|
|
28
|
+
"claude",
|
|
29
|
+
"claude-code",
|
|
30
|
+
"cli",
|
|
31
|
+
"mv",
|
|
32
|
+
"rename",
|
|
33
|
+
"migration"
|
|
34
|
+
],
|
|
35
|
+
bin: {
|
|
36
|
+
"claude-mv": "dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
files: [
|
|
39
|
+
"dist",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
engines: {
|
|
43
|
+
node: ">=20"
|
|
44
|
+
},
|
|
45
|
+
scripts: {
|
|
46
|
+
build: "tsup",
|
|
47
|
+
dev: "tsup --watch",
|
|
48
|
+
format: "prettier --check .",
|
|
49
|
+
check: "prettier --write . && eslint . --fix",
|
|
50
|
+
lint: "eslint .",
|
|
51
|
+
typecheck: "tsc --noEmit",
|
|
52
|
+
test: "vitest run",
|
|
53
|
+
"test:watch": "vitest",
|
|
54
|
+
"sync:version": "bun scripts/sync-version.ts",
|
|
55
|
+
verify: "bun run format && bun run lint && bun run typecheck && bun run test",
|
|
56
|
+
version: "bun run sync:version && bun run build && git add plugin",
|
|
57
|
+
prepublishOnly: "bun run build",
|
|
58
|
+
prepare: "husky"
|
|
59
|
+
},
|
|
60
|
+
dependencies: {
|
|
61
|
+
"@inkjs/ui": "^2.0.0",
|
|
62
|
+
"env-paths": "^3.0.0",
|
|
63
|
+
execa: "^9.6.0",
|
|
64
|
+
"fastest-levenshtein": "^1.0.16",
|
|
65
|
+
"fs-extra": "^11.3.2",
|
|
66
|
+
fzf: "^0.5.2",
|
|
67
|
+
ink: "^5.2.1",
|
|
68
|
+
"jsonc-parser": "^3.3.1",
|
|
69
|
+
"p-limit": "^7.3.3",
|
|
70
|
+
"pretty-bytes": "^7.1.3",
|
|
71
|
+
"pretty-ms": "^9.3.1",
|
|
72
|
+
"proper-lockfile": "^4.1.2",
|
|
73
|
+
"ps-list": "^9.0.0",
|
|
74
|
+
react: "^18.3.1",
|
|
75
|
+
"string-width": "^8.2.2",
|
|
76
|
+
tar: "^7.5.7",
|
|
77
|
+
ulid: "^3.0.2",
|
|
78
|
+
"write-file-atomic": "^8.0.0"
|
|
79
|
+
},
|
|
80
|
+
devDependencies: {
|
|
81
|
+
"@eslint/js": "^10.0.1",
|
|
82
|
+
"@types/fs-extra": "^11.0.4",
|
|
83
|
+
"@types/node": "^26.2.0",
|
|
84
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
85
|
+
"@types/react": "^18.3.12",
|
|
86
|
+
"@types/write-file-atomic": "^4.0.3",
|
|
87
|
+
eslint: "^10.8.1",
|
|
88
|
+
"eslint-config-prettier": "^10.1.8",
|
|
89
|
+
"eslint-plugin-check-file": "^3.3.2",
|
|
90
|
+
"eslint-plugin-import-x": "^4.17.1",
|
|
91
|
+
"eslint-plugin-unused-imports": "^4.4.1",
|
|
92
|
+
globals: "^17.9.0",
|
|
93
|
+
husky: "^9.1.7",
|
|
94
|
+
"ink-testing-library": "^4.0.0",
|
|
95
|
+
prettier: "^3.9.6",
|
|
96
|
+
tempy: "^3.1.0",
|
|
97
|
+
tsup: "^8.5.0",
|
|
98
|
+
typescript: "^6.0.3",
|
|
99
|
+
"typescript-eslint": "^8.66.0",
|
|
100
|
+
vitest: "^4.1.10"
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/cli/commands.ts
|
|
105
|
+
var MUTATING_ARGS = {
|
|
106
|
+
"dry-run": { kind: "boolean", alias: "n", description: "\uACC4\uD68D\uB9CC \uBCF4\uC5EC\uC8FC\uACE0 \uC544\uBB34\uAC83\uB3C4 \uC4F0\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4" },
|
|
107
|
+
yes: { kind: "boolean", alias: "y", description: "\uD655\uC778 \uD504\uB86C\uD504\uD2B8\uB97C \uAC74\uB108\uB701\uB2C8\uB2E4" },
|
|
108
|
+
force: { kind: "boolean", alias: "f", description: "lock \uACFC \uC0C1\uD0DC \uCDA9\uB3CC\uC744 \uACBD\uACE0\uB85C \uB0AE\uCDA5\uB2C8\uB2E4" },
|
|
109
|
+
"no-backup": { kind: "boolean", description: "\uBC31\uC5C5\uC744 \uB9CC\uB4E4\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 (rollback \uBD88\uAC00)" }
|
|
110
|
+
};
|
|
111
|
+
var COMMON_ARGS = {
|
|
112
|
+
json: { kind: "boolean", description: "\uAE30\uACC4\uC6A9 JSON \uC73C\uB85C \uCD9C\uB825\uD569\uB2C8\uB2E4" },
|
|
113
|
+
quiet: { kind: "boolean", alias: "q", description: "\uC624\uB958\uB9CC \uCD9C\uB825\uD569\uB2C8\uB2E4" },
|
|
114
|
+
"claude-home": { kind: "string", description: "~/.claude \uC704\uCE58\uB97C \uC7AC\uC815\uC758\uD569\uB2C8\uB2E4" }
|
|
115
|
+
};
|
|
116
|
+
var COMMANDS = [
|
|
117
|
+
{
|
|
118
|
+
name: "mv",
|
|
119
|
+
summary: "\uB514\uB809\uD130\uB9AC\uB97C \uC62E\uAE30\uACE0 Claude \uC0C1\uD0DC\uB97C \uB530\uB77C \uC62E\uAE41\uB2C8\uB2E4",
|
|
120
|
+
positionals: [
|
|
121
|
+
{ name: "src", complete: "project", required: true },
|
|
122
|
+
{ name: "dst", complete: "directory", required: true }
|
|
123
|
+
],
|
|
124
|
+
args: {
|
|
125
|
+
...MUTATING_ARGS,
|
|
126
|
+
...COMMON_ARGS,
|
|
127
|
+
"state-only": { kind: "boolean", description: "\uB514\uB809\uD130\uB9AC\uB294 \uB450\uACE0 \uC0C1\uD0DC\uB9CC \uC62E\uAE41\uB2C8\uB2E4" },
|
|
128
|
+
"rewrite-prose": { kind: "boolean", description: "\uB300\uD654 \uBCF8\uBB38\uC758 \uACBD\uB85C\uAE4C\uC9C0 \uBC14\uAFC9\uB2C8\uB2E4" },
|
|
129
|
+
"allow-ancestors": { kind: "boolean", description: "\uC0C1\uC704\uC5D0\uC11C \uB3C4\uB294 \uC138\uC158\uC744 \uD5C8\uC6A9\uD569\uB2C8\uB2E4" },
|
|
130
|
+
"skip-live": {
|
|
131
|
+
kind: "boolean",
|
|
132
|
+
description: "\uC0B4\uC544 \uC788\uB294 \uC138\uC158\uC758 \uD2B8\uB79C\uC2A4\uD06C\uB9BD\uD2B8\uB294 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "ls",
|
|
138
|
+
summary: "Claude \uAC00 \uC544\uB294 \uD504\uB85C\uC81D\uD2B8\uB97C \uBAA8\uB450 \uBCF4\uC5EC\uC90D\uB2C8\uB2E4",
|
|
139
|
+
positionals: [],
|
|
140
|
+
args: {
|
|
141
|
+
...COMMON_ARGS,
|
|
142
|
+
orphans: { kind: "boolean", description: "\uC5F0\uACB0\uC774 \uB04A\uAE34 \uC0C1\uD0DC\uB9CC \uBCF4\uC5EC\uC90D\uB2C8\uB2E4" },
|
|
143
|
+
sort: {
|
|
144
|
+
kind: "string",
|
|
145
|
+
description: "\uC815\uB82C \uAE30\uC900",
|
|
146
|
+
values: ["activity", "size", "path"],
|
|
147
|
+
default: "activity"
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "info",
|
|
153
|
+
summary: "\uD55C \uD504\uB85C\uC81D\uD2B8\uC758 \uC0C1\uD0DC\uB97C \uC790\uC138\uD788 \uBCF4\uC5EC\uC90D\uB2C8\uB2E4",
|
|
154
|
+
positionals: [{ name: "target", complete: "project", required: false }],
|
|
155
|
+
args: COMMON_ARGS
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "doctor",
|
|
159
|
+
summary: "\uC5F0\uACB0\uC774 \uB04A\uAE34 \uC0C1\uD0DC\uB97C \uCC3E\uC544\uB0B4\uACE0 \uC5B4\uB514\uB85C \uC62E\uACA8\uC84C\uB294\uC9C0 \uCD94\uC815\uD569\uB2C8\uB2E4",
|
|
160
|
+
positionals: [],
|
|
161
|
+
args: COMMON_ARGS
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "rm",
|
|
165
|
+
summary: "\uD55C \uD504\uB85C\uC81D\uD2B8\uC758 Claude \uC0C1\uD0DC\uB97C \uC9C0\uC6C1\uB2C8\uB2E4",
|
|
166
|
+
positionals: [{ name: "target", complete: "project", required: false }],
|
|
167
|
+
args: { ...MUTATING_ARGS, ...COMMON_ARGS }
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: "merge",
|
|
171
|
+
summary: "\uB450 \uD504\uB85C\uC81D\uD2B8 \uB514\uB809\uD130\uB9AC\uB97C \uD569\uCE69\uB2C8\uB2E4",
|
|
172
|
+
positionals: [
|
|
173
|
+
{ name: "from", complete: "project", required: true },
|
|
174
|
+
{ name: "to", complete: "project", required: true }
|
|
175
|
+
],
|
|
176
|
+
args: { ...MUTATING_ARGS, ...COMMON_ARGS }
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "backup",
|
|
180
|
+
summary: "\uC774\uB3D9 \uC5C6\uC774 \uD604\uC7AC \uC0C1\uD0DC\uC758 \uC2A4\uB0C5\uC0F7\uB9CC \uB9CC\uB4ED\uB2C8\uB2E4",
|
|
181
|
+
positionals: [{ name: "target", complete: "project", required: false }],
|
|
182
|
+
args: COMMON_ARGS
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "rollback",
|
|
186
|
+
summary: "\uBC31\uC5C5 \uBAA9\uB85D\uC744 \uBCF4\uAC70\uB098 \uC9C0\uC815\uD55C \uBC31\uC5C5\uC73C\uB85C \uB418\uB3CC\uB9BD\uB2C8\uB2E4",
|
|
187
|
+
positionals: [{ name: "id", complete: "backup", required: false }],
|
|
188
|
+
args: COMMON_ARGS
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "export",
|
|
192
|
+
summary: "\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC\uB97C tarball \uB85C \uB0B4\uBCF4\uB0C5\uB2C8\uB2E4",
|
|
193
|
+
positionals: [{ name: "target", complete: "project", required: false }],
|
|
194
|
+
args: { ...COMMON_ARGS, out: { kind: "string", alias: "o", description: "\uCD9C\uB825 \uD30C\uC77C" } }
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "import",
|
|
198
|
+
summary: "tarball \uC5D0\uC11C \uC0C1\uD0DC\uB97C \uB4E4\uC5EC\uC635\uB2C8\uB2E4",
|
|
199
|
+
positionals: [{ name: "file", complete: "bundle", required: true }],
|
|
200
|
+
args: {
|
|
201
|
+
...COMMON_ARGS,
|
|
202
|
+
to: { kind: "string", description: "\uB4E4\uC5EC\uC624\uBA74\uC11C \uC774 \uACBD\uB85C\uB85C \uC774\uAD00\uD569\uB2C8\uB2E4" }
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "completion",
|
|
207
|
+
summary: "\uC178 \uC790\uB3D9\uC644\uC131 \uC2A4\uD06C\uB9BD\uD2B8\uB97C \uCD9C\uB825\uD569\uB2C8\uB2E4",
|
|
208
|
+
positionals: [{ name: "shell", complete: "shell", required: true }],
|
|
209
|
+
args: {}
|
|
210
|
+
}
|
|
211
|
+
];
|
|
212
|
+
var COMMAND_NAMES = COMMANDS.map((c) => c.name);
|
|
213
|
+
function findCommand(name) {
|
|
214
|
+
return COMMANDS.find((c) => c.name === name);
|
|
215
|
+
}
|
|
216
|
+
var VALUE_FLAGS = new Set(
|
|
217
|
+
COMMANDS.flatMap(
|
|
218
|
+
(command) => Object.entries(command.args).filter(([, arg]) => arg.kind === "string").flatMap(([name, arg]) => arg.alias ? [`--${name}`, `-${arg.alias}`] : [`--${name}`])
|
|
219
|
+
)
|
|
220
|
+
);
|
|
221
|
+
function dispatch(argv) {
|
|
222
|
+
for (let i = 0; i < argv.length; i++) {
|
|
223
|
+
const token = argv[i];
|
|
224
|
+
if (token === "--") return { command: "mv", rest: argv.slice(i + 1) };
|
|
225
|
+
if (token === "__complete") return { command: "__complete", rest: argv.slice(i + 1) };
|
|
226
|
+
if (token.startsWith("-")) {
|
|
227
|
+
if (!token.includes("=") && VALUE_FLAGS.has(token)) i++;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (COMMAND_NAMES.includes(token)) {
|
|
231
|
+
return { command: token, rest: [...argv.slice(0, i), ...argv.slice(i + 1)] };
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
return { command: "mv", rest: argv };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/cli/complete.ts
|
|
239
|
+
function complete(argv, sources) {
|
|
240
|
+
const [commandName, ...rest] = argv;
|
|
241
|
+
if (rest.length === 0 && !COMMANDS.some((c) => c.name === commandName)) {
|
|
242
|
+
return COMMANDS.filter((c) => c.name.startsWith(commandName ?? "")).map((c) => ({
|
|
243
|
+
value: c.name,
|
|
244
|
+
description: c.summary
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
const spec = findCommand(commandName ?? "") ?? findCommand("mv");
|
|
248
|
+
const current = rest.at(-1) ?? "";
|
|
249
|
+
if (current.startsWith("-")) {
|
|
250
|
+
const [flagName, inlineValue] = current.split("=");
|
|
251
|
+
const arg = spec.args[flagName?.replace(/^--?/, "") ?? ""];
|
|
252
|
+
if (arg?.values && inlineValue !== void 0) {
|
|
253
|
+
return arg.values.filter((v) => v.startsWith(inlineValue)).map((v) => ({ value: `${flagName}=${v}` }));
|
|
254
|
+
}
|
|
255
|
+
return Object.entries(spec.args).map(([name, meta]) => ({ value: `--${name}`, description: meta.description })).filter((c) => c.value.startsWith(current));
|
|
256
|
+
}
|
|
257
|
+
const consumed = rest.filter((token) => !token.startsWith("-"));
|
|
258
|
+
const index = Math.max(0, consumed.length - 1);
|
|
259
|
+
const positional = spec.positionals[index];
|
|
260
|
+
return filterByPrefix(candidatesFor(positional?.complete ?? "none", current, sources), current);
|
|
261
|
+
}
|
|
262
|
+
function candidatesFor(source, prefix, sources) {
|
|
263
|
+
switch (source) {
|
|
264
|
+
case "project":
|
|
265
|
+
return sources.projects();
|
|
266
|
+
case "backup":
|
|
267
|
+
return sources.backups();
|
|
268
|
+
case "directory":
|
|
269
|
+
return sources.directories(prefix);
|
|
270
|
+
case "bundle":
|
|
271
|
+
return sources.bundles(prefix);
|
|
272
|
+
case "shell":
|
|
273
|
+
return [{ value: "zsh" }, { value: "bash" }, { value: "fish" }];
|
|
274
|
+
case "none":
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function filterByPrefix(candidates, prefix) {
|
|
279
|
+
return prefix ? candidates.filter((c) => c.value.startsWith(prefix)) : candidates;
|
|
280
|
+
}
|
|
281
|
+
function formatCandidates(candidates) {
|
|
282
|
+
return candidates.map((c) => c.description ? `${c.value} ${c.description}` : c.value).join("\n");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/cli/completion/index.ts
|
|
286
|
+
function completionScript(shell, bin = "claude-mv") {
|
|
287
|
+
switch (shell) {
|
|
288
|
+
case "zsh":
|
|
289
|
+
return zsh(bin);
|
|
290
|
+
case "bash":
|
|
291
|
+
return bash(bin);
|
|
292
|
+
case "fish":
|
|
293
|
+
return fish(bin);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function zsh(bin) {
|
|
297
|
+
return `#compdef ${bin}
|
|
298
|
+
|
|
299
|
+
_${bin.replace(/-/g, "_")}() {
|
|
300
|
+
local -a lines values descriptions
|
|
301
|
+
local line value
|
|
302
|
+
|
|
303
|
+
lines=(\${(f)"$(${bin} __complete "\${(@)words[2,$CURRENT]}" 2>/dev/null)"})
|
|
304
|
+
|
|
305
|
+
for line in $lines; do
|
|
306
|
+
[[ -z "$line" ]] && continue
|
|
307
|
+
value=\${line%%$'\\t'*}
|
|
308
|
+
values+=("$value")
|
|
309
|
+
if [[ "$line" == *$'\\t'* ]]; then
|
|
310
|
+
descriptions+=("\${value} -- \${line#*$'\\t'}")
|
|
311
|
+
else
|
|
312
|
+
descriptions+=("$value")
|
|
313
|
+
fi
|
|
314
|
+
done
|
|
315
|
+
|
|
316
|
+
(( \${#values} )) || return 1
|
|
317
|
+
compadd -Q -d descriptions -a values
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
compdef _${bin.replace(/-/g, "_")} ${bin}
|
|
321
|
+
`;
|
|
322
|
+
}
|
|
323
|
+
function bash(bin) {
|
|
324
|
+
return `_${bin.replace(/-/g, "_")}() {
|
|
325
|
+
local IFS=$'\\n'
|
|
326
|
+
local candidates
|
|
327
|
+
candidates=( $(${bin} __complete "\${COMP_WORDS[@]:1:COMP_CWORD}" 2>/dev/null | cut -f1) )
|
|
328
|
+
COMPREPLY=( $(compgen -W "\${candidates[*]}" -- "\${COMP_WORDS[COMP_CWORD]}") )
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
complete -o default -F _${bin.replace(/-/g, "_")} ${bin}
|
|
332
|
+
`;
|
|
333
|
+
}
|
|
334
|
+
function fish(bin) {
|
|
335
|
+
return `function __${bin.replace(/-/g, "_")}_complete
|
|
336
|
+
set -l tokens (commandline -opc) (commandline -ct)
|
|
337
|
+
${bin} __complete $tokens[2..-1] 2>/dev/null
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
complete -c ${bin} -f -a '(__${bin.replace(/-/g, "_")}_complete)'
|
|
341
|
+
`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/cli/exit.ts
|
|
345
|
+
var EXITS = {
|
|
346
|
+
ok: [0, "\uC131\uACF5"],
|
|
347
|
+
internal: [1, "\uC608\uAE30\uCE58 \uBABB\uD55C \uC624\uB958"],
|
|
348
|
+
usage: [2, "\uC0AC\uC6A9\uBC95 \uC624\uB958"],
|
|
349
|
+
planned: [10, "\uACC4\uD68D \uC0DD\uC131 \uC644\uB8CC. \uC2E4\uD328\uAC00 \uC544\uB2D8"],
|
|
350
|
+
orphans: [11, "\uC5F0\uACB0\uC774 \uB04A\uAE34 \uC0C1\uD0DC \uBC1C\uACAC"],
|
|
351
|
+
nothingToDo: [12, "\uC774\uAD00 \uB300\uC0C1 \uC5C6\uC74C"],
|
|
352
|
+
locked: [20, "\uC61B \uACBD\uB85C\uC5D0\uC11C \uC138\uC158 \uC2E4\uD589 \uC911"],
|
|
353
|
+
conflict: [21, "\uB300\uC0C1 \uACBD\uB85C\uC5D0 \uC0C1\uD0DC\uAC00 \uC774\uBBF8 \uC874\uC7AC"],
|
|
354
|
+
precondition: [22, "\uC804\uC81C \uC870\uAC74 \uC704\uBC18"],
|
|
355
|
+
failedRolledBack: [30, "\uC2E4\uD328 \uD6C4 \uC804\uCCB4 \uBCF5\uAD6C \uC644\uB8CC"],
|
|
356
|
+
failedDirty: [31, "\uC2E4\uD328 \uD6C4 \uBCF5\uAD6C\uB3C4 \uC2E4\uD328. \uC218\uB3D9 \uBCF5\uAD6C \uD544\uC694"],
|
|
357
|
+
io: [40, "\uD30C\uC77C\uC2DC\uC2A4\uD15C \uC624\uB958"],
|
|
358
|
+
interrupted: [130, "\uC0AC\uC6A9\uC790 \uC911\uB2E8"]
|
|
359
|
+
};
|
|
360
|
+
var ExitCode = Object.fromEntries(
|
|
361
|
+
Object.entries(EXITS).map(([name, [code]]) => [name, code])
|
|
362
|
+
);
|
|
363
|
+
var exitCodeTable = Object.values(EXITS);
|
|
364
|
+
var CliError = class extends Error {
|
|
365
|
+
constructor(message, code = "usage", hint) {
|
|
366
|
+
super(message);
|
|
367
|
+
this.code = code;
|
|
368
|
+
this.hint = hint;
|
|
369
|
+
this.name = "CliError";
|
|
370
|
+
}
|
|
371
|
+
code;
|
|
372
|
+
hint;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// src/cli/help.ts
|
|
376
|
+
function helpText(commandName) {
|
|
377
|
+
const spec = commandName ? findCommand(commandName) : void 0;
|
|
378
|
+
if (!spec) return rootHelp();
|
|
379
|
+
const lines = [
|
|
380
|
+
`claude-mv ${spec.name} - ${spec.summary}`,
|
|
381
|
+
"",
|
|
382
|
+
`\uC0AC\uC6A9\uBC95 claude-mv ${spec.name} ${spec.positionals.map((p) => p.required ? `<${p.name}>` : `[${p.name}]`).join(" ")} [\uC635\uC158]`
|
|
383
|
+
];
|
|
384
|
+
const args = Object.entries(spec.args);
|
|
385
|
+
if (args.length > 0) {
|
|
386
|
+
lines.push("", "\uC635\uC158");
|
|
387
|
+
const width = Math.max(...args.map(([n, a]) => n.length + (a.alias ? 4 : 0)));
|
|
388
|
+
for (const [name, arg] of args) {
|
|
389
|
+
const flag = `${arg.alias ? `-${arg.alias}, ` : ""}--${name}`;
|
|
390
|
+
lines.push(` ${flag.padEnd(width + 6)}${arg.description}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return `${lines.join("\n")}
|
|
394
|
+
`;
|
|
395
|
+
}
|
|
396
|
+
function rootHelp() {
|
|
397
|
+
const width = Math.max(...COMMANDS.map((c) => c.name.length));
|
|
398
|
+
return [
|
|
399
|
+
"claude-mv - \uD504\uB85C\uC81D\uD2B8 \uB514\uB809\uD130\uB9AC\uB97C \uC62E\uAE30\uBA74\uC11C Claude Code \uC0C1\uD0DC\uAE4C\uC9C0 \uAC19\uC774 \uC62E\uAE41\uB2C8\uB2E4",
|
|
400
|
+
"",
|
|
401
|
+
"\uC0AC\uC6A9\uBC95 claude-mv <src> <dst> \uB514\uB809\uD130\uB9AC\uC640 \uC0C1\uD0DC\uB97C \uD568\uAED8 \uC62E\uAE41\uB2C8\uB2E4",
|
|
402
|
+
" claude-mv <\uBA85\uB839> [\uC778\uC790]",
|
|
403
|
+
"",
|
|
404
|
+
"\uBA85\uB839",
|
|
405
|
+
...COMMANDS.map((c) => ` ${c.name.padEnd(width + 2)}${c.summary}`),
|
|
406
|
+
"",
|
|
407
|
+
"\uC885\uB8CC \uCF54\uB4DC",
|
|
408
|
+
...exitCodeTable.map(([code, why]) => ` ${String(code).padStart(3)} ${why}`),
|
|
409
|
+
"",
|
|
410
|
+
'\uC790\uB3D9\uC644\uC131 eval "$(claude-mv completion zsh)"',
|
|
411
|
+
""
|
|
412
|
+
].join("\n");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/cli/parse.ts
|
|
416
|
+
function parseArgs(spec, argv) {
|
|
417
|
+
const positionals = [];
|
|
418
|
+
const flags = {};
|
|
419
|
+
const byAlias = /* @__PURE__ */ new Map();
|
|
420
|
+
for (const [name, arg] of Object.entries(spec.args)) {
|
|
421
|
+
if (arg.alias) byAlias.set(arg.alias, name);
|
|
422
|
+
}
|
|
423
|
+
for (let i = 0; i < argv.length; i++) {
|
|
424
|
+
const token = argv[i];
|
|
425
|
+
if (token === "--") {
|
|
426
|
+
positionals.push(...argv.slice(i + 1));
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
if (!token.startsWith("-")) {
|
|
430
|
+
positionals.push(token);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
const isLong = token.startsWith("--");
|
|
434
|
+
const body = isLong ? token.slice(2) : token.slice(1);
|
|
435
|
+
const [rawName, inlineValue] = splitOnce(body, "=");
|
|
436
|
+
const name = isLong ? rawName : byAlias.get(rawName) ?? rawName;
|
|
437
|
+
const arg = spec.args[name];
|
|
438
|
+
if (!arg && isLong && name.startsWith("no-")) {
|
|
439
|
+
const positive = name.slice(3);
|
|
440
|
+
if (spec.args[positive]?.kind === "boolean") {
|
|
441
|
+
flags[positive] = false;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (!arg) throw new CliError(`\uC54C \uC218 \uC5C6\uB294 \uC635\uC158\uC785\uB2C8\uB2E4: ${token}`, "usage");
|
|
446
|
+
if (arg.kind === "boolean") {
|
|
447
|
+
flags[name] = inlineValue === void 0 ? true : inlineValue !== "false";
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const value = inlineValue ?? argv[++i];
|
|
451
|
+
if (value === void 0) throw new CliError(`${token}\uC5D0 \uAC12\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.`, "usage");
|
|
452
|
+
if (arg.values && !arg.values.includes(value)) {
|
|
453
|
+
throw new CliError(`${token}\uC740 ${arg.values.join(" | ")} \uC911 \uD558\uB098\uC5EC\uC57C \uD569\uB2C8\uB2E4.`, "usage");
|
|
454
|
+
}
|
|
455
|
+
flags[name] = value;
|
|
456
|
+
}
|
|
457
|
+
for (const [name, arg] of Object.entries(spec.args)) {
|
|
458
|
+
if (arg.default !== void 0 && flags[name] === void 0) flags[name] = arg.default;
|
|
459
|
+
}
|
|
460
|
+
return { positionals, flags };
|
|
461
|
+
}
|
|
462
|
+
function splitOnce(value, separator) {
|
|
463
|
+
const index = value.indexOf(separator);
|
|
464
|
+
return index === -1 ? [value, void 0] : [value.slice(0, index), value.slice(index + 1)];
|
|
465
|
+
}
|
|
466
|
+
function requirePositionals(spec, parsed) {
|
|
467
|
+
const required = spec.positionals.filter((p) => p.required);
|
|
468
|
+
if (parsed.positionals.length < required.length) {
|
|
469
|
+
throw new CliError(
|
|
470
|
+
`${spec.name}\uC5D0\uB294 ${required.map((p) => `<${p.name}>`).join(" ")}\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.`,
|
|
471
|
+
"usage"
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
return parsed.positionals;
|
|
475
|
+
}
|
|
476
|
+
var bool = (parsed, name) => parsed.flags[name] === true;
|
|
477
|
+
var str = (parsed, name) => typeof parsed.flags[name] === "string" ? parsed.flags[name] : void 0;
|
|
478
|
+
|
|
479
|
+
// src/cli/picker.ts
|
|
480
|
+
import { execa } from "execa";
|
|
481
|
+
function resolvePickerMode(env) {
|
|
482
|
+
if (!env.isTTY) return "none";
|
|
483
|
+
if (env.override === "none") return "none";
|
|
484
|
+
if (env.override === "ink") return "ink";
|
|
485
|
+
if (env.override === "fzf") return "fzf";
|
|
486
|
+
return env.hasFzf ? "fzf" : "ink";
|
|
487
|
+
}
|
|
488
|
+
async function hasFzf() {
|
|
489
|
+
try {
|
|
490
|
+
await execa("fzf", ["--version"]);
|
|
491
|
+
return true;
|
|
492
|
+
} catch {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async function pickWithFzf(candidates, opts) {
|
|
497
|
+
if (candidates.length === 0) return null;
|
|
498
|
+
const args = ["--height=40%", "--reverse", `--prompt=${opts.prompt} `, "--with-nth=1.."];
|
|
499
|
+
if (opts.preview) args.push("--preview", opts.preview, "--preview-window=right,60%");
|
|
500
|
+
try {
|
|
501
|
+
const { stdout } = await execa("fzf", args, {
|
|
502
|
+
input: candidates.map((c) => c.description ? `${c.value} ${c.description}` : c.value).join("\n"),
|
|
503
|
+
stdin: "pipe",
|
|
504
|
+
stdout: "pipe",
|
|
505
|
+
stderr: "inherit"
|
|
506
|
+
});
|
|
507
|
+
const picked = stdout.trim().split(" ")[0];
|
|
508
|
+
return picked || null;
|
|
509
|
+
} catch {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/commands/manage.ts
|
|
515
|
+
import {
|
|
516
|
+
existsSync as existsSync6,
|
|
517
|
+
mkdirSync as mkdirSync4,
|
|
518
|
+
readFileSync as readFileSync5,
|
|
519
|
+
readdirSync as readdirSync4,
|
|
520
|
+
renameSync as renameSync3,
|
|
521
|
+
rmSync as rmSync3,
|
|
522
|
+
statSync as statSync5
|
|
523
|
+
} from "fs";
|
|
524
|
+
import { join as join7 } from "path";
|
|
525
|
+
import { ulid } from "ulid";
|
|
526
|
+
import writeFileAtomic2 from "write-file-atomic";
|
|
527
|
+
|
|
528
|
+
// src/core/doctor.ts
|
|
529
|
+
import { distance } from "fastest-levenshtein";
|
|
530
|
+
|
|
531
|
+
// src/core/mangle.ts
|
|
532
|
+
var MANGLE_MAX = 200;
|
|
533
|
+
function mangleHash(input) {
|
|
534
|
+
let h = 0;
|
|
535
|
+
for (let i = 0; i < input.length; i++) {
|
|
536
|
+
h = (h << 5) - h + input.charCodeAt(i) | 0;
|
|
537
|
+
}
|
|
538
|
+
return h;
|
|
539
|
+
}
|
|
540
|
+
function mangle(absolutePath) {
|
|
541
|
+
const replaced = absolutePath.replace(/[^a-zA-Z0-9]/g, "-");
|
|
542
|
+
if (replaced.length <= MANGLE_MAX) return replaced;
|
|
543
|
+
return `${replaced.slice(0, MANGLE_MAX)}-${Math.abs(mangleHash(absolutePath)).toString(36)}`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/core/paths.ts
|
|
547
|
+
import { realpathSync, statSync } from "fs";
|
|
548
|
+
import path from "path";
|
|
549
|
+
function defaultCasePolicy(platform) {
|
|
550
|
+
if (platform === "win32") return "insensitive";
|
|
551
|
+
if (platform === "darwin") return "insensitive";
|
|
552
|
+
return "sensitive";
|
|
553
|
+
}
|
|
554
|
+
function probeCasePolicy(dir, fallback) {
|
|
555
|
+
const flipped = flipCase(dir);
|
|
556
|
+
if (flipped === dir) return fallback;
|
|
557
|
+
try {
|
|
558
|
+
const a = statSync(dir);
|
|
559
|
+
const b = statSync(flipped);
|
|
560
|
+
return a.ino === b.ino && a.dev === b.dev ? "insensitive" : "sensitive";
|
|
561
|
+
} catch {
|
|
562
|
+
return "sensitive";
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function flipCase(input) {
|
|
566
|
+
return input.replace(
|
|
567
|
+
/[a-zA-Z]/,
|
|
568
|
+
(c) => c === c.toLowerCase() ? c.toUpperCase() : c.toLowerCase()
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
function normalizePath(input, opts) {
|
|
572
|
+
const p = opts.platform === "win32" ? path.win32 : path.posix;
|
|
573
|
+
let out = input;
|
|
574
|
+
if (out === "~") out = opts.home;
|
|
575
|
+
else if (out.startsWith("~/") || opts.platform === "win32" && out.startsWith("~\\")) {
|
|
576
|
+
out = p.join(opts.home, out.slice(2));
|
|
577
|
+
}
|
|
578
|
+
if (opts.platform === "win32") {
|
|
579
|
+
out = stripLongPathPrefix(out);
|
|
580
|
+
}
|
|
581
|
+
out = p.resolve(out);
|
|
582
|
+
if (opts.realpath !== false) {
|
|
583
|
+
try {
|
|
584
|
+
out = realpathSync.native(out);
|
|
585
|
+
if (opts.platform === "win32") out = stripLongPathPrefix(out);
|
|
586
|
+
} catch {
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
out = stripTrailingSeparator(out, p);
|
|
590
|
+
if (opts.platform === "win32") out = upperDriveLetter(out);
|
|
591
|
+
return out;
|
|
592
|
+
}
|
|
593
|
+
function stripLongPathPrefix(input) {
|
|
594
|
+
if (input.startsWith("\\\\?\\UNC\\")) return `\\\\${input.slice(8)}`;
|
|
595
|
+
if (input.startsWith("\\\\?\\")) return input.slice(4);
|
|
596
|
+
return input;
|
|
597
|
+
}
|
|
598
|
+
function upperDriveLetter(input) {
|
|
599
|
+
return /^[a-z]:/.test(input) ? input[0].toUpperCase() + input.slice(1) : input;
|
|
600
|
+
}
|
|
601
|
+
function stripTrailingSeparator(input, p) {
|
|
602
|
+
if (input === p.parse(input).root) return input;
|
|
603
|
+
let out = input;
|
|
604
|
+
while (out.length > 1 && (out.endsWith(p.sep) || out.endsWith("/"))) out = out.slice(0, -1);
|
|
605
|
+
return out;
|
|
606
|
+
}
|
|
607
|
+
function fold(input, policy) {
|
|
608
|
+
return policy === "insensitive" ? input.replace(/[A-Z]/g, (c) => c.toLowerCase()) : input;
|
|
609
|
+
}
|
|
610
|
+
function pathEquals(a, b, policy) {
|
|
611
|
+
return fold(a, policy) === fold(b, policy);
|
|
612
|
+
}
|
|
613
|
+
function isUnder(child, parent, policy, platform = "linux") {
|
|
614
|
+
const p = platform === "win32" ? path.win32 : path.posix;
|
|
615
|
+
const a = fold(child, policy);
|
|
616
|
+
const b = fold(parent, policy);
|
|
617
|
+
const rel = p.relative(b, a);
|
|
618
|
+
return Boolean(rel && rel !== ".." && !rel.startsWith(`..${p.sep}`) && !p.isAbsolute(rel));
|
|
619
|
+
}
|
|
620
|
+
function reparent(value, from, to, policy, platform) {
|
|
621
|
+
const p = platform === "win32" ? path.win32 : path.posix;
|
|
622
|
+
if (!p.isAbsolute(value)) return void 0;
|
|
623
|
+
if (pathEquals(value, from, policy)) return to;
|
|
624
|
+
if (!isUnder(value, from, policy, platform)) return void 0;
|
|
625
|
+
return p.join(to, value.slice(from.length));
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/core/doctor.ts
|
|
629
|
+
function doctor(opts) {
|
|
630
|
+
const { index } = opts;
|
|
631
|
+
const orphans = [];
|
|
632
|
+
const healthy = [];
|
|
633
|
+
const other = [];
|
|
634
|
+
for (const project of index.projects) {
|
|
635
|
+
if (project.health === "healthy") {
|
|
636
|
+
healthy.push(project);
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const entry = {
|
|
640
|
+
project,
|
|
641
|
+
alsoIn: locateElsewhere(index, project, opts.policy),
|
|
642
|
+
suggestions: project.primaryCwd ? suggest(project.primaryCwd, opts) : []
|
|
643
|
+
};
|
|
644
|
+
if (project.health === "orphaned") orphans.push(entry);
|
|
645
|
+
else other.push(entry);
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
orphans: orphans.sort((a, b) => b.project.bytes - a.project.bytes),
|
|
649
|
+
healthy,
|
|
650
|
+
other,
|
|
651
|
+
collisions: findCollisions(index)
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
function locateElsewhere(index, project, policy) {
|
|
655
|
+
const path3 = project.primaryCwd;
|
|
656
|
+
if (!path3) return [];
|
|
657
|
+
const out = [];
|
|
658
|
+
if (index.configProjectKeys.some((k) => pathEquals(k, path3, policy))) out.push("config");
|
|
659
|
+
if ([...index.historyProjects.keys()].some((p) => pathEquals(p, path3, policy))) {
|
|
660
|
+
out.push("history");
|
|
661
|
+
}
|
|
662
|
+
if (index.cacheDirs.includes(project.dirName)) out.push("cache");
|
|
663
|
+
return out;
|
|
664
|
+
}
|
|
665
|
+
function findCollisions(index) {
|
|
666
|
+
const out = [];
|
|
667
|
+
for (const project of index.projects) {
|
|
668
|
+
const distinct = [...project.cwdCensus.keys()].filter((p) => mangle(p) === project.dirName);
|
|
669
|
+
if (distinct.length > 1) out.push({ dirName: project.dirName, paths: distinct });
|
|
670
|
+
}
|
|
671
|
+
return out;
|
|
672
|
+
}
|
|
673
|
+
function suggest(missing, opts) {
|
|
674
|
+
const name = opts.basename(missing);
|
|
675
|
+
const parent = opts.dirname(missing);
|
|
676
|
+
const found = /* @__PURE__ */ new Map();
|
|
677
|
+
const consider = (candidate, weight, evidence) => {
|
|
678
|
+
if (!opts.exists(candidate) || pathEquals(candidate, missing, opts.policy)) return;
|
|
679
|
+
const prev = found.get(candidate);
|
|
680
|
+
if (prev) {
|
|
681
|
+
prev.confidence = Math.min(1, prev.confidence + weight);
|
|
682
|
+
prev.evidence.push(evidence);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
found.set(candidate, { candidate, confidence: weight, evidence: [evidence] });
|
|
686
|
+
};
|
|
687
|
+
for (const root of /* @__PURE__ */ new Set([parent, ...opts.searchRoots])) {
|
|
688
|
+
for (const child of opts.listDirs(root)) {
|
|
689
|
+
const candidate = opts.join(root, child);
|
|
690
|
+
if (child === name) consider(candidate, 0.45, "\uC774\uB984\uC774 \uAC19\uC2B5\uB2C8\uB2E4");
|
|
691
|
+
else if (distance(child, name) <= Math.max(2, Math.floor(name.length / 3))) {
|
|
692
|
+
consider(candidate, 0.2, "\uC774\uB984\uC774 \uBE44\uC2B7\uD569\uB2C8\uB2E4");
|
|
693
|
+
}
|
|
694
|
+
if (root === parent && opts.exists(parent)) consider(candidate, 0.15, "\uAC19\uC740 \uBD80\uBAA8 \uC548\uC785\uB2C8\uB2E4");
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return [...found.values()].filter((s) => s.confidence >= 0.4).sort((a, b) => b.confidence - a.confidence).slice(0, 3);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// src/shared/context.ts
|
|
701
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
702
|
+
import { basename, dirname, join as join3 } from "path";
|
|
703
|
+
|
|
704
|
+
// src/shared/env.ts
|
|
705
|
+
import { existsSync, readdirSync } from "fs";
|
|
706
|
+
import { homedir } from "os";
|
|
707
|
+
import { join } from "path";
|
|
708
|
+
import envPaths from "env-paths";
|
|
709
|
+
function readRawEnv() {
|
|
710
|
+
return { env: process.env, home: homedir(), platform: process.platform };
|
|
711
|
+
}
|
|
712
|
+
function resolveClaudeHome(raw) {
|
|
713
|
+
return (raw.env.CLAUDE_CONFIG_DIR ?? join(raw.home, ".claude")).normalize("NFC");
|
|
714
|
+
}
|
|
715
|
+
function resolveConfigPath(raw, claudeHome) {
|
|
716
|
+
const preferred = join(claudeHome, ".config.json");
|
|
717
|
+
if (existsSync(preferred)) return preferred;
|
|
718
|
+
const base = raw.env.CLAUDE_CONFIG_DIR ?? raw.home;
|
|
719
|
+
const plain = join(base, ".claude.json");
|
|
720
|
+
if (existsSync(plain)) return plain;
|
|
721
|
+
const variant = safeReaddir(base).find(
|
|
722
|
+
(name) => name.startsWith(".claude-") && name.endsWith("-oauth.json")
|
|
723
|
+
);
|
|
724
|
+
return variant ? join(base, variant) : plain;
|
|
725
|
+
}
|
|
726
|
+
function safeReaddir(dir) {
|
|
727
|
+
try {
|
|
728
|
+
return readdirSync(dir);
|
|
729
|
+
} catch {
|
|
730
|
+
return [];
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
function resolveCacheRoot() {
|
|
734
|
+
const root = envPaths("claude-cli").cache;
|
|
735
|
+
return existsSync(root) ? root : null;
|
|
736
|
+
}
|
|
737
|
+
function resolveClaudeEnv(overrides) {
|
|
738
|
+
const raw = readRawEnv();
|
|
739
|
+
const claudeHome = overrides?.claudeHome ? overrides.claudeHome.normalize("NFC") : resolveClaudeHome(raw);
|
|
740
|
+
return {
|
|
741
|
+
claudeHome,
|
|
742
|
+
configPath: resolveConfigPath(raw, claudeHome),
|
|
743
|
+
projectsDir: join(claudeHome, "projects"),
|
|
744
|
+
historyPath: join(claudeHome, "history.jsonl"),
|
|
745
|
+
sessionsDir: join(claudeHome, "sessions"),
|
|
746
|
+
plansDir: join(claudeHome, "plans"),
|
|
747
|
+
cacheRoot: resolveCacheRoot(),
|
|
748
|
+
toolDir: join(claudeHome, ".claude-mv"),
|
|
749
|
+
platform: raw.platform
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function readCliEnv() {
|
|
753
|
+
return {
|
|
754
|
+
ci: Boolean(process.env.CI),
|
|
755
|
+
term: process.env.TERM,
|
|
756
|
+
picker: process.env.CLAUDE_MV_PICKER,
|
|
757
|
+
isStdoutTTY: Boolean(process.stdout.isTTY),
|
|
758
|
+
isStdinTTY: Boolean(process.stdin.isTTY)
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// src/shared/process.ts
|
|
763
|
+
import { execa as execa2 } from "execa";
|
|
764
|
+
async function startedAt(pid, platform) {
|
|
765
|
+
try {
|
|
766
|
+
if (platform === "win32") {
|
|
767
|
+
const { stdout: stdout2 } = await execa2("powershell.exe", [
|
|
768
|
+
"-NoProfile",
|
|
769
|
+
"-Command",
|
|
770
|
+
`(Get-Process -Id ${pid}).StartTime.ToUniversalTime().ToString('o')`
|
|
771
|
+
]);
|
|
772
|
+
const parsed2 = Date.parse(stdout2.trim());
|
|
773
|
+
return Number.isNaN(parsed2) ? null : parsed2;
|
|
774
|
+
}
|
|
775
|
+
const { stdout } = await execa2("ps", ["-o", "lstart=", "-p", String(pid)]);
|
|
776
|
+
const parsed = Date.parse(stdout.trim());
|
|
777
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
778
|
+
} catch {
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
function createProcessProbe(platform) {
|
|
783
|
+
return {
|
|
784
|
+
async exists(pid) {
|
|
785
|
+
try {
|
|
786
|
+
process.kill(pid, 0);
|
|
787
|
+
return true;
|
|
788
|
+
} catch (error) {
|
|
789
|
+
return error.code === "EPERM";
|
|
790
|
+
}
|
|
791
|
+
},
|
|
792
|
+
startedAt: (pid) => startedAt(pid, platform),
|
|
793
|
+
self: process.pid
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// src/core/scan.ts
|
|
798
|
+
import { readFile, readdir, stat as stat2 } from "fs/promises";
|
|
799
|
+
import { join as join2 } from "path";
|
|
800
|
+
|
|
801
|
+
// src/core/jsonl.ts
|
|
802
|
+
import { createHash } from "crypto";
|
|
803
|
+
import { createReadStream, createWriteStream } from "fs";
|
|
804
|
+
import { chmod, open, rename, stat, unlink } from "fs/promises";
|
|
805
|
+
import path2 from "path";
|
|
806
|
+
|
|
807
|
+
// src/core/fields.ts
|
|
808
|
+
function isRecord(value) {
|
|
809
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
810
|
+
}
|
|
811
|
+
function visitField(holder, key, visit) {
|
|
812
|
+
if (!isRecord(holder)) return false;
|
|
813
|
+
const current = holder[key];
|
|
814
|
+
if (typeof current !== "string") return false;
|
|
815
|
+
const next = visit(current);
|
|
816
|
+
if (next === void 0 || next === current) return false;
|
|
817
|
+
holder[key] = next;
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
function visitArray(holder, key, visit) {
|
|
821
|
+
if (!isRecord(holder)) return false;
|
|
822
|
+
const arr = holder[key];
|
|
823
|
+
if (!Array.isArray(arr)) return false;
|
|
824
|
+
let changed = false;
|
|
825
|
+
for (let i = 0; i < arr.length; i++) {
|
|
826
|
+
const item = arr[i];
|
|
827
|
+
if (typeof item !== "string") continue;
|
|
828
|
+
const next = visit(item);
|
|
829
|
+
if (next !== void 0 && next !== item) {
|
|
830
|
+
arr[i] = next;
|
|
831
|
+
changed = true;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return changed;
|
|
835
|
+
}
|
|
836
|
+
function visitCwdFields(record, visit) {
|
|
837
|
+
if (!isRecord(record)) return false;
|
|
838
|
+
let changed = false;
|
|
839
|
+
changed = visitField(record, "cwd", visit) || changed;
|
|
840
|
+
const wire = record.wireIngestContext;
|
|
841
|
+
if (isRecord(wire)) {
|
|
842
|
+
for (const key of Object.keys(wire)) {
|
|
843
|
+
changed = visitField(wire[key], "cwd", visit) || changed;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const snapshot = isRecord(record.attachment) ? record.attachment.snapshot : void 0;
|
|
847
|
+
if (isRecord(snapshot)) {
|
|
848
|
+
changed = visitField(snapshot, "workingDirectory", visit) || changed;
|
|
849
|
+
changed = visitArray(snapshot, "additionalWorkingDirectories", visit) || changed;
|
|
850
|
+
}
|
|
851
|
+
return changed;
|
|
852
|
+
}
|
|
853
|
+
function visitOtherStructuralFields(record, visit) {
|
|
854
|
+
if (!isRecord(record)) return false;
|
|
855
|
+
let changed = false;
|
|
856
|
+
changed = visitField(record, "trackingPath", visit) || changed;
|
|
857
|
+
const attachment = record.attachment;
|
|
858
|
+
if (isRecord(attachment)) {
|
|
859
|
+
changed = visitField(attachment, "path", visit) || changed;
|
|
860
|
+
changed = visitField(attachment, "planFilePath", visit) || changed;
|
|
861
|
+
if (Array.isArray(attachment.changes)) {
|
|
862
|
+
for (const change of attachment.changes) {
|
|
863
|
+
changed = visitField(change, "from", visit) || changed;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const message = record.message;
|
|
868
|
+
if (isRecord(message) && Array.isArray(message.content)) {
|
|
869
|
+
for (const block of message.content) {
|
|
870
|
+
if (!isRecord(block)) continue;
|
|
871
|
+
changed = visitField(block.input, "planFilePath", visit) || changed;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return changed;
|
|
875
|
+
}
|
|
876
|
+
function visitStructuralPaths(record, visit) {
|
|
877
|
+
const cwds = visitCwdFields(record, visit);
|
|
878
|
+
const others = visitOtherStructuralFields(record, visit);
|
|
879
|
+
return cwds || others;
|
|
880
|
+
}
|
|
881
|
+
function visitProsePaths(record, visit) {
|
|
882
|
+
if (!isRecord(record)) return false;
|
|
883
|
+
let changed = false;
|
|
884
|
+
changed = visitField(record.toolUseResult, "stdout", visit) || changed;
|
|
885
|
+
changed = visitField(record.toolUseResult, "stderr", visit) || changed;
|
|
886
|
+
const message = record.message;
|
|
887
|
+
if (isRecord(message) && Array.isArray(message.content)) {
|
|
888
|
+
for (const block of message.content) {
|
|
889
|
+
if (!isRecord(block)) continue;
|
|
890
|
+
changed = visitField(block, "text", visit) || changed;
|
|
891
|
+
changed = visitField(block, "content", visit) || changed;
|
|
892
|
+
changed = visitField(block.input, "command", visit) || changed;
|
|
893
|
+
changed = visitField(block.input, "file_path", visit) || changed;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if (Array.isArray(record.rendered)) {
|
|
897
|
+
for (const item of record.rendered) {
|
|
898
|
+
changed = visitField(item, "content", visit) || changed;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
const wire = record.wireToolInputs;
|
|
902
|
+
if (isRecord(wire)) {
|
|
903
|
+
for (const key of Object.keys(wire)) {
|
|
904
|
+
changed = visitField(wire[key], "command", visit) || changed;
|
|
905
|
+
changed = visitField(wire[key], "file_path", visit) || changed;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
const attachment = record.attachment;
|
|
909
|
+
if (isRecord(attachment)) {
|
|
910
|
+
changed = visitField(attachment, "filename", visit) || changed;
|
|
911
|
+
if (Array.isArray(attachment.files)) {
|
|
912
|
+
for (const file of attachment.files) {
|
|
913
|
+
changed = visitField(file, "path", visit) || changed;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return changed;
|
|
918
|
+
}
|
|
919
|
+
function visitPaths(record, visit, opts) {
|
|
920
|
+
const structural = visitStructuralPaths(record, visit);
|
|
921
|
+
const prose = opts?.includeProse ? visitProsePaths(record, visit) : false;
|
|
922
|
+
return structural || prose;
|
|
923
|
+
}
|
|
924
|
+
var PATH_LIKE = /(?:[A-Za-z]:[\\/]|\/|\\\\)[^\s"'`,;:()[\]{}]*/g;
|
|
925
|
+
function extractPaths(value) {
|
|
926
|
+
return value.match(PATH_LIKE) ?? [];
|
|
927
|
+
}
|
|
928
|
+
function replacePathIn(value, from, to) {
|
|
929
|
+
let out = "";
|
|
930
|
+
let index = 0;
|
|
931
|
+
for (; ; ) {
|
|
932
|
+
const found = value.indexOf(from, index);
|
|
933
|
+
if (found === -1) {
|
|
934
|
+
out += value.slice(index);
|
|
935
|
+
return out;
|
|
936
|
+
}
|
|
937
|
+
const after = value[found + from.length];
|
|
938
|
+
const isBoundary = after === void 0 || after === "/" || after === "\\" || !/[\w.-]/.test(after);
|
|
939
|
+
out += value.slice(index, found) + (isBoundary ? to : from);
|
|
940
|
+
index = found + from.length;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/core/jsonl.ts
|
|
945
|
+
var LF = 10;
|
|
946
|
+
function isAbsolutePathish(value) {
|
|
947
|
+
return value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
|
|
948
|
+
}
|
|
949
|
+
async function* readLines(filePath) {
|
|
950
|
+
const stream = createReadStream(filePath);
|
|
951
|
+
let carry = Buffer.alloc(0);
|
|
952
|
+
let index = 0;
|
|
953
|
+
for await (const chunk of stream) {
|
|
954
|
+
carry = carry.length === 0 ? chunk : Buffer.concat([carry, chunk]);
|
|
955
|
+
let start = 0;
|
|
956
|
+
for (; ; ) {
|
|
957
|
+
const nl = carry.indexOf(LF, start);
|
|
958
|
+
if (nl === -1) break;
|
|
959
|
+
const hasCr = nl > start && carry[nl - 1] === 13;
|
|
960
|
+
yield {
|
|
961
|
+
raw: carry.subarray(start, hasCr ? nl - 1 : nl),
|
|
962
|
+
terminator: carry.subarray(hasCr ? nl - 1 : nl, nl + 1),
|
|
963
|
+
index: index++
|
|
964
|
+
};
|
|
965
|
+
start = nl + 1;
|
|
966
|
+
}
|
|
967
|
+
carry = carry.subarray(start);
|
|
968
|
+
}
|
|
969
|
+
if (carry.length > 0) {
|
|
970
|
+
yield { raw: carry, terminator: Buffer.alloc(0), index };
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
async function censusTranscript(filePath, opts) {
|
|
974
|
+
const census = {
|
|
975
|
+
file: filePath,
|
|
976
|
+
lines: 0,
|
|
977
|
+
bytes: (await stat(filePath)).size,
|
|
978
|
+
paths: /* @__PURE__ */ new Map(),
|
|
979
|
+
cwds: /* @__PURE__ */ new Map(),
|
|
980
|
+
prose: /* @__PURE__ */ new Map(),
|
|
981
|
+
pathless: 0,
|
|
982
|
+
malformed: []
|
|
983
|
+
};
|
|
984
|
+
for await (const line of readLines(filePath)) {
|
|
985
|
+
census.lines++;
|
|
986
|
+
if (line.raw.length === 0) continue;
|
|
987
|
+
let record;
|
|
988
|
+
try {
|
|
989
|
+
record = JSON.parse(line.raw.toString("utf8"));
|
|
990
|
+
} catch {
|
|
991
|
+
census.malformed.push(line.index);
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
const onThisLine = /* @__PURE__ */ new Set();
|
|
995
|
+
const cwdsOnThisLine = /* @__PURE__ */ new Set();
|
|
996
|
+
const collect = (into) => (value) => {
|
|
997
|
+
if (isAbsolutePathish(value)) into.add(value);
|
|
998
|
+
return void 0;
|
|
999
|
+
};
|
|
1000
|
+
const proseOnThisLine = /* @__PURE__ */ new Set();
|
|
1001
|
+
visitPaths(record, collect(onThisLine), opts);
|
|
1002
|
+
visitCwdFields(record, collect(cwdsOnThisLine));
|
|
1003
|
+
visitProsePaths(record, (value) => {
|
|
1004
|
+
for (const path3 of extractPaths(value)) proseOnThisLine.add(path3);
|
|
1005
|
+
return void 0;
|
|
1006
|
+
});
|
|
1007
|
+
for (const value of onThisLine) {
|
|
1008
|
+
census.paths.set(value, (census.paths.get(value) ?? 0) + 1);
|
|
1009
|
+
}
|
|
1010
|
+
for (const value of cwdsOnThisLine) {
|
|
1011
|
+
census.cwds.set(value, (census.cwds.get(value) ?? 0) + 1);
|
|
1012
|
+
}
|
|
1013
|
+
for (const value of proseOnThisLine) {
|
|
1014
|
+
census.prose.set(value, (census.prose.get(value) ?? 0) + 1);
|
|
1015
|
+
}
|
|
1016
|
+
if (onThisLine.size === 0) census.pathless++;
|
|
1017
|
+
}
|
|
1018
|
+
return census;
|
|
1019
|
+
}
|
|
1020
|
+
function stagingPathFor(filePath, suffix = "tmp", impl = path2) {
|
|
1021
|
+
return impl.join(impl.dirname(filePath), `${impl.basename(filePath)}.claude-mv-${suffix}`);
|
|
1022
|
+
}
|
|
1023
|
+
var ConcurrentModificationError = class extends Error {
|
|
1024
|
+
constructor(file) {
|
|
1025
|
+
super(`\uC7AC\uC791\uC131\uD558\uB294 \uB3D9\uC548 \uD30C\uC77C\uC774 \uBC14\uB00C\uC5C8\uC2B5\uB2C8\uB2E4: ${file}`);
|
|
1026
|
+
this.file = file;
|
|
1027
|
+
this.name = "ConcurrentModificationError";
|
|
1028
|
+
}
|
|
1029
|
+
file;
|
|
1030
|
+
};
|
|
1031
|
+
async function rewriteTranscript(filePath, visit, opts) {
|
|
1032
|
+
return rewriteJsonl(filePath, (record) => visitPaths(record, visit, opts), opts);
|
|
1033
|
+
}
|
|
1034
|
+
async function rewriteJsonl(filePath, mutate, opts) {
|
|
1035
|
+
const staging = stagingPathFor(filePath, opts?.stagingSuffix);
|
|
1036
|
+
const before = await stat(filePath);
|
|
1037
|
+
const hashIn = createHash("sha256");
|
|
1038
|
+
const hashOut = createHash("sha256");
|
|
1039
|
+
const out = createWriteStream(staging);
|
|
1040
|
+
const result = {
|
|
1041
|
+
linesTotal: 0,
|
|
1042
|
+
linesChanged: 0,
|
|
1043
|
+
linesMalformed: 0,
|
|
1044
|
+
sha256In: "",
|
|
1045
|
+
sha256Out: "",
|
|
1046
|
+
staged: staging
|
|
1047
|
+
};
|
|
1048
|
+
const write = (buf) => new Promise((resolve, reject) => {
|
|
1049
|
+
out.write(buf, (err) => err ? reject(err) : resolve());
|
|
1050
|
+
});
|
|
1051
|
+
try {
|
|
1052
|
+
for await (const line of readLines(filePath)) {
|
|
1053
|
+
result.linesTotal++;
|
|
1054
|
+
hashIn.update(line.raw).update(line.terminator);
|
|
1055
|
+
const emit = async (buf) => {
|
|
1056
|
+
hashOut.update(buf);
|
|
1057
|
+
await write(buf);
|
|
1058
|
+
};
|
|
1059
|
+
if (line.raw.length === 0) {
|
|
1060
|
+
await emit(line.raw);
|
|
1061
|
+
await emit(line.terminator);
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
let record;
|
|
1065
|
+
try {
|
|
1066
|
+
record = JSON.parse(line.raw.toString("utf8"));
|
|
1067
|
+
} catch {
|
|
1068
|
+
result.linesMalformed++;
|
|
1069
|
+
await emit(line.raw);
|
|
1070
|
+
await emit(line.terminator);
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
if (!mutate(record)) {
|
|
1074
|
+
await emit(line.raw);
|
|
1075
|
+
await emit(line.terminator);
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
result.linesChanged++;
|
|
1079
|
+
await emit(Buffer.from(JSON.stringify(record), "utf8"));
|
|
1080
|
+
await emit(line.terminator);
|
|
1081
|
+
}
|
|
1082
|
+
const after = await stat(filePath);
|
|
1083
|
+
if (after.size !== before.size || after.ino !== before.ino) {
|
|
1084
|
+
throw new ConcurrentModificationError(filePath);
|
|
1085
|
+
}
|
|
1086
|
+
await new Promise((resolve, reject) => {
|
|
1087
|
+
out.end((err) => err ? reject(err) : resolve());
|
|
1088
|
+
});
|
|
1089
|
+
const handle = await open(staging, "r+");
|
|
1090
|
+
try {
|
|
1091
|
+
await handle.sync();
|
|
1092
|
+
} finally {
|
|
1093
|
+
await handle.close();
|
|
1094
|
+
}
|
|
1095
|
+
await chmod(staging, (await stat(filePath)).mode);
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
out.destroy();
|
|
1098
|
+
await unlink(staging).catch(() => {
|
|
1099
|
+
});
|
|
1100
|
+
result.staged = null;
|
|
1101
|
+
throw error;
|
|
1102
|
+
}
|
|
1103
|
+
result.sha256In = hashIn.digest("hex");
|
|
1104
|
+
result.sha256Out = hashOut.digest("hex");
|
|
1105
|
+
if (result.linesChanged === 0) {
|
|
1106
|
+
await unlink(staging).catch(() => {
|
|
1107
|
+
});
|
|
1108
|
+
result.staged = null;
|
|
1109
|
+
}
|
|
1110
|
+
return result;
|
|
1111
|
+
}
|
|
1112
|
+
async function commitStaged(staging, target) {
|
|
1113
|
+
await rename(staging, target);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// src/core/scan.ts
|
|
1117
|
+
function pickPrimaryCwd(dirName, census) {
|
|
1118
|
+
const byCount = [...census].sort((a, b) => b[1] - a[1]);
|
|
1119
|
+
const matching = byCount.find(([path3]) => mangle(path3) === dirName);
|
|
1120
|
+
if (matching) return { cwd: matching[0], matchedKey: true };
|
|
1121
|
+
return { cwd: byCount[0]?.[0] ?? null, matchedKey: false };
|
|
1122
|
+
}
|
|
1123
|
+
function classify(primaryCwd, matchedKey, hasTranscripts, exists2) {
|
|
1124
|
+
if (!hasTranscripts) return "empty";
|
|
1125
|
+
if (primaryCwd === null) return "undeterminable";
|
|
1126
|
+
if (!matchedKey) return "key-mismatch";
|
|
1127
|
+
return exists2 ? "healthy" : "orphaned";
|
|
1128
|
+
}
|
|
1129
|
+
async function exists(path3) {
|
|
1130
|
+
try {
|
|
1131
|
+
return (await stat2(path3)).isDirectory();
|
|
1132
|
+
} catch {
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
async function collectTranscripts(dirPath) {
|
|
1137
|
+
const transcripts = [];
|
|
1138
|
+
const otherFiles = [];
|
|
1139
|
+
const walk = async (dir, kind, sessionId) => {
|
|
1140
|
+
let entries;
|
|
1141
|
+
try {
|
|
1142
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1143
|
+
} catch {
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
for (const entry of entries) {
|
|
1147
|
+
const full = join2(dir, entry.name);
|
|
1148
|
+
if (entry.isDirectory()) {
|
|
1149
|
+
if (entry.name === "subagents") await walk(full, "subagent", sessionId);
|
|
1150
|
+
else if (kind === "session") await walk(join2(full), "session", entry.name);
|
|
1151
|
+
else otherFiles.push(full);
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
if (!entry.name.endsWith(".jsonl")) {
|
|
1155
|
+
otherFiles.push(full);
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
const census = await censusTranscript(full);
|
|
1159
|
+
transcripts.push({
|
|
1160
|
+
...census,
|
|
1161
|
+
kind,
|
|
1162
|
+
sessionId: sessionId ?? entry.name.replace(/\.jsonl$/, ""),
|
|
1163
|
+
agentId: kind === "subagent" ? entry.name.replace(/\.jsonl$/, "") : void 0,
|
|
1164
|
+
mtimeMs: (await stat2(full)).mtimeMs
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
await walk(dirPath, "session");
|
|
1169
|
+
return { transcripts, otherFiles };
|
|
1170
|
+
}
|
|
1171
|
+
async function scanProjectDir(projectsDir, dirName) {
|
|
1172
|
+
const dirPath = join2(projectsDir, dirName);
|
|
1173
|
+
const { transcripts, otherFiles } = await collectTranscripts(dirPath);
|
|
1174
|
+
const census = /* @__PURE__ */ new Map();
|
|
1175
|
+
const cwdCensus = /* @__PURE__ */ new Map();
|
|
1176
|
+
for (const t of transcripts) {
|
|
1177
|
+
for (const [path3, count] of t.paths) census.set(path3, (census.get(path3) ?? 0) + count);
|
|
1178
|
+
for (const [path3, count] of t.cwds) cwdCensus.set(path3, (cwdCensus.get(path3) ?? 0) + count);
|
|
1179
|
+
}
|
|
1180
|
+
const { cwd, matchedKey } = pickPrimaryCwd(dirName, cwdCensus);
|
|
1181
|
+
const health = classify(cwd, matchedKey, transcripts.length > 0, cwd ? await exists(cwd) : false);
|
|
1182
|
+
return {
|
|
1183
|
+
dirName,
|
|
1184
|
+
dirPath,
|
|
1185
|
+
census,
|
|
1186
|
+
cwdCensus,
|
|
1187
|
+
primaryCwd: cwd,
|
|
1188
|
+
health,
|
|
1189
|
+
transcripts,
|
|
1190
|
+
otherFiles,
|
|
1191
|
+
bytes: transcripts.reduce((sum, t) => sum + t.bytes, 0),
|
|
1192
|
+
lastActivityMs: transcripts.length ? Math.max(...transcripts.map((t) => t.mtimeMs)) : null
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
var PROC_START_IS_UTC = " UTC";
|
|
1196
|
+
function parseProcStart(value) {
|
|
1197
|
+
if (!value) return null;
|
|
1198
|
+
const parsed = Date.parse(value + PROC_START_IS_UTC);
|
|
1199
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
1200
|
+
}
|
|
1201
|
+
async function readConfig(configPath) {
|
|
1202
|
+
try {
|
|
1203
|
+
const parsed = JSON.parse(await readFile(configPath, "utf8"));
|
|
1204
|
+
return {
|
|
1205
|
+
keys: Object.keys(parsed.projects ?? {}),
|
|
1206
|
+
githubRepoPaths: parsed.githubRepoPaths ?? {}
|
|
1207
|
+
};
|
|
1208
|
+
} catch {
|
|
1209
|
+
return { keys: [], githubRepoPaths: {} };
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
async function readHistory(historyPath) {
|
|
1213
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1214
|
+
try {
|
|
1215
|
+
for await (const line of readLines(historyPath)) {
|
|
1216
|
+
if (line.raw.length === 0) continue;
|
|
1217
|
+
try {
|
|
1218
|
+
const rec = JSON.parse(line.raw.toString("utf8"));
|
|
1219
|
+
if (typeof rec.project === "string") {
|
|
1220
|
+
counts.set(rec.project, (counts.get(rec.project) ?? 0) + 1);
|
|
1221
|
+
}
|
|
1222
|
+
} catch {
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
} catch {
|
|
1226
|
+
}
|
|
1227
|
+
return counts;
|
|
1228
|
+
}
|
|
1229
|
+
async function readSessions(sessionsDir) {
|
|
1230
|
+
const out = [];
|
|
1231
|
+
let names;
|
|
1232
|
+
try {
|
|
1233
|
+
names = await readdir(sessionsDir);
|
|
1234
|
+
} catch {
|
|
1235
|
+
return out;
|
|
1236
|
+
}
|
|
1237
|
+
for (const name of names) {
|
|
1238
|
+
if (!name.endsWith(".json")) continue;
|
|
1239
|
+
try {
|
|
1240
|
+
const raw = JSON.parse(await readFile(join2(sessionsDir, name), "utf8"));
|
|
1241
|
+
if (typeof raw.pid !== "number" || typeof raw.cwd !== "string") continue;
|
|
1242
|
+
const { procStart, ...rest } = raw;
|
|
1243
|
+
out.push({ ...rest, startedAtMs: parseProcStart(procStart) });
|
|
1244
|
+
} catch {
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return out;
|
|
1248
|
+
}
|
|
1249
|
+
async function listDirs(dir) {
|
|
1250
|
+
try {
|
|
1251
|
+
return (await readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1252
|
+
} catch {
|
|
1253
|
+
return [];
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
async function scan(target) {
|
|
1257
|
+
const dirNames = await listDirs(target.projectsDir);
|
|
1258
|
+
const config = await readConfig(target.configPath);
|
|
1259
|
+
return {
|
|
1260
|
+
projects: await Promise.all(dirNames.map((name) => scanProjectDir(target.projectsDir, name))),
|
|
1261
|
+
configProjectKeys: config.keys,
|
|
1262
|
+
githubRepoPaths: config.githubRepoPaths,
|
|
1263
|
+
historyProjects: await readHistory(target.historyPath),
|
|
1264
|
+
cacheDirs: target.cacheRoot ? await listDirs(target.cacheRoot) : [],
|
|
1265
|
+
sessions: await readSessions(target.sessionsDir)
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// src/shared/context.ts
|
|
1270
|
+
async function createContext(opts) {
|
|
1271
|
+
const env = resolveClaudeEnv(opts);
|
|
1272
|
+
const platform = env.platform;
|
|
1273
|
+
return {
|
|
1274
|
+
env,
|
|
1275
|
+
platform,
|
|
1276
|
+
policy: probeCasePolicy(env.claudeHome, defaultCasePolicy(platform)),
|
|
1277
|
+
probe: createProcessProbe(platform),
|
|
1278
|
+
index: await scan(env),
|
|
1279
|
+
exists: existsSync2,
|
|
1280
|
+
listDirs: listDirs2
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
function listDirs2(dir) {
|
|
1284
|
+
try {
|
|
1285
|
+
return readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1286
|
+
} catch {
|
|
1287
|
+
return [];
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function searchRoots(index, home) {
|
|
1291
|
+
const roots = /* @__PURE__ */ new Set([
|
|
1292
|
+
join3(home, "Developer"),
|
|
1293
|
+
join3(home, "Projects"),
|
|
1294
|
+
join3(home, "src"),
|
|
1295
|
+
join3(home, "code")
|
|
1296
|
+
]);
|
|
1297
|
+
for (const project of index.projects) {
|
|
1298
|
+
if (project.health === "healthy" && project.primaryCwd) roots.add(dirname(project.primaryCwd));
|
|
1299
|
+
}
|
|
1300
|
+
return [...roots].filter((r) => {
|
|
1301
|
+
try {
|
|
1302
|
+
return statSync2(r).isDirectory();
|
|
1303
|
+
} catch {
|
|
1304
|
+
return false;
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
var pathHelpers = { basename, dirname, join: join3, exists: existsSync2 };
|
|
1309
|
+
|
|
1310
|
+
// src/commands/query.ts
|
|
1311
|
+
function runList(ctx, reporter, opts) {
|
|
1312
|
+
let projects = ctx.index.projects;
|
|
1313
|
+
if (opts.orphansOnly) projects = projects.filter((p) => p.health !== "healthy");
|
|
1314
|
+
const sorted = [...projects].sort((a, b) => {
|
|
1315
|
+
if (opts.sort === "size") return b.bytes - a.bytes;
|
|
1316
|
+
if (opts.sort === "activity") return (b.lastActivityMs ?? 0) - (a.lastActivityMs ?? 0);
|
|
1317
|
+
return (a.primaryCwd ?? a.dirName).localeCompare(b.primaryCwd ?? b.dirName);
|
|
1318
|
+
});
|
|
1319
|
+
reporter.list(sorted);
|
|
1320
|
+
return sorted.some((p) => p.health === "orphaned") ? "orphans" : "ok";
|
|
1321
|
+
}
|
|
1322
|
+
function findProject(ctx, needle, home) {
|
|
1323
|
+
const byDirName = ctx.index.projects.find((p) => p.dirName === needle);
|
|
1324
|
+
if (byDirName) return byDirName;
|
|
1325
|
+
const normalized = normalizePath(needle, { platform: ctx.platform, home, realpath: false });
|
|
1326
|
+
return ctx.index.projects.find(
|
|
1327
|
+
(p) => p.primaryCwd && pathEquals(p.primaryCwd, normalized, ctx.policy)
|
|
1328
|
+
) ?? ctx.index.projects.find((p) => p.dirName === mangle(normalized));
|
|
1329
|
+
}
|
|
1330
|
+
function runInfo(ctx, reporter, opts) {
|
|
1331
|
+
const project = findProject(ctx, opts.target, opts.home);
|
|
1332
|
+
if (!project) throw new CliError(`\uB4F1\uB85D\uB41C \uD504\uB85C\uC81D\uD2B8\uAC00 \uC544\uB2D9\uB2C8\uB2E4: ${opts.target}`, "usage");
|
|
1333
|
+
reporter.info(project);
|
|
1334
|
+
return project.health === "orphaned" ? "orphans" : "ok";
|
|
1335
|
+
}
|
|
1336
|
+
function runDoctor(ctx, reporter) {
|
|
1337
|
+
const report = doctor({
|
|
1338
|
+
index: ctx.index,
|
|
1339
|
+
policy: ctx.policy,
|
|
1340
|
+
searchRoots: searchRoots(ctx.index, ctx.env.claudeHome.replace(/\/\.claude$/, "")),
|
|
1341
|
+
listDirs: ctx.listDirs,
|
|
1342
|
+
...pathHelpers
|
|
1343
|
+
});
|
|
1344
|
+
reporter.doctor(report);
|
|
1345
|
+
return report.orphans.length > 0 ? "orphans" : "ok";
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// src/core/apply.ts
|
|
1349
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, rmSync, statSync as statSync4 } from "fs";
|
|
1350
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
1351
|
+
import fsExtra from "fs-extra";
|
|
1352
|
+
import writeFileAtomic from "write-file-atomic";
|
|
1353
|
+
|
|
1354
|
+
// src/core/backup.ts
|
|
1355
|
+
import { createHash as createHash2 } from "crypto";
|
|
1356
|
+
import {
|
|
1357
|
+
copyFileSync,
|
|
1358
|
+
existsSync as existsSync3,
|
|
1359
|
+
linkSync,
|
|
1360
|
+
mkdirSync,
|
|
1361
|
+
readFileSync,
|
|
1362
|
+
readdirSync as readdirSync3,
|
|
1363
|
+
statSync as statSync3,
|
|
1364
|
+
writeFileSync
|
|
1365
|
+
} from "fs";
|
|
1366
|
+
import { dirname as dirname2, join as join4, relative } from "path";
|
|
1367
|
+
function sha256(path3) {
|
|
1368
|
+
return createHash2("sha256").update(readFileSync(path3)).digest("hex");
|
|
1369
|
+
}
|
|
1370
|
+
function stash(original, storedAbs) {
|
|
1371
|
+
mkdirSync(dirname2(storedAbs), { recursive: true });
|
|
1372
|
+
try {
|
|
1373
|
+
linkSync(original, storedAbs);
|
|
1374
|
+
} catch {
|
|
1375
|
+
copyFileSync(original, storedAbs);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
function collectFiles(dir, out = []) {
|
|
1379
|
+
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
1380
|
+
const full = join4(dir, entry.name);
|
|
1381
|
+
if (entry.isDirectory()) collectFiles(full, out);
|
|
1382
|
+
else out.push(full);
|
|
1383
|
+
}
|
|
1384
|
+
return out;
|
|
1385
|
+
}
|
|
1386
|
+
function createBackup(input) {
|
|
1387
|
+
const dir = join4(input.root, input.backupId);
|
|
1388
|
+
const filesDir = join4(dir, "files");
|
|
1389
|
+
mkdirSync(filesDir, { recursive: true });
|
|
1390
|
+
const entries = [];
|
|
1391
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1392
|
+
const addFile = (path3) => {
|
|
1393
|
+
if (seen.has(path3) || !existsSync3(path3)) return;
|
|
1394
|
+
seen.add(path3);
|
|
1395
|
+
const stats = statSync3(path3);
|
|
1396
|
+
const stored = join4("files", path3.replace(/^[/\\]/, "").replace(/:/g, "_"));
|
|
1397
|
+
stash(path3, join4(dir, stored));
|
|
1398
|
+
entries.push({
|
|
1399
|
+
type: "file",
|
|
1400
|
+
original: path3,
|
|
1401
|
+
stored,
|
|
1402
|
+
sha256: sha256(path3),
|
|
1403
|
+
size: stats.size,
|
|
1404
|
+
mode: stats.mode
|
|
1405
|
+
});
|
|
1406
|
+
};
|
|
1407
|
+
for (const step of input.plan.steps) {
|
|
1408
|
+
switch (step.kind) {
|
|
1409
|
+
case "rewrite-transcript":
|
|
1410
|
+
case "rewrite-plan":
|
|
1411
|
+
addFile(step.file);
|
|
1412
|
+
break;
|
|
1413
|
+
case "edit-config":
|
|
1414
|
+
case "rewrite-history":
|
|
1415
|
+
addFile(step.file);
|
|
1416
|
+
break;
|
|
1417
|
+
case "move-project-dir":
|
|
1418
|
+
entries.push({ type: "dir-move", from: step.from, to: step.to });
|
|
1419
|
+
entries.push({ type: "absent", original: step.to });
|
|
1420
|
+
break;
|
|
1421
|
+
case "move-cache-dir":
|
|
1422
|
+
entries.push({ type: "dir-move", from: step.from, to: step.to });
|
|
1423
|
+
entries.push({ type: "absent", original: step.to });
|
|
1424
|
+
break;
|
|
1425
|
+
case "move-directory":
|
|
1426
|
+
entries.push({ type: "dir-move", from: step.from, to: step.to });
|
|
1427
|
+
entries.push({ type: "absent", original: step.to });
|
|
1428
|
+
break;
|
|
1429
|
+
case "repair-worktrees":
|
|
1430
|
+
for (const file of collectFiles(join4(step.repoRoot, ".git", "worktrees")).filter(
|
|
1431
|
+
(f) => f.endsWith("gitdir")
|
|
1432
|
+
)) {
|
|
1433
|
+
addFile(file);
|
|
1434
|
+
}
|
|
1435
|
+
break;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
const manifest = {
|
|
1439
|
+
version: 1,
|
|
1440
|
+
backupId: input.backupId,
|
|
1441
|
+
createdAt: Date.now(),
|
|
1442
|
+
tool: { name: "claude-mv", version: input.version },
|
|
1443
|
+
src: input.plan.request.src,
|
|
1444
|
+
dst: input.plan.request.dst,
|
|
1445
|
+
entries,
|
|
1446
|
+
sealed: false
|
|
1447
|
+
};
|
|
1448
|
+
const manifestPath = join4(dir, "manifest.json");
|
|
1449
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
1450
|
+
return {
|
|
1451
|
+
id: input.backupId,
|
|
1452
|
+
dir,
|
|
1453
|
+
manifestPath,
|
|
1454
|
+
journalPath: join4(dir, "journal.jsonl"),
|
|
1455
|
+
entries
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
function seal(handle) {
|
|
1459
|
+
const manifest = readManifest(handle.manifestPath);
|
|
1460
|
+
manifest.sealed = true;
|
|
1461
|
+
writeFileSync(handle.manifestPath, JSON.stringify(manifest, null, 2));
|
|
1462
|
+
}
|
|
1463
|
+
function readManifest(path3) {
|
|
1464
|
+
return JSON.parse(readFileSync(path3, "utf8"));
|
|
1465
|
+
}
|
|
1466
|
+
function listBackups(root) {
|
|
1467
|
+
let names;
|
|
1468
|
+
try {
|
|
1469
|
+
names = readdirSync3(root);
|
|
1470
|
+
} catch {
|
|
1471
|
+
return [];
|
|
1472
|
+
}
|
|
1473
|
+
const out = [];
|
|
1474
|
+
for (const name of names) {
|
|
1475
|
+
try {
|
|
1476
|
+
out.push(readManifest(join4(root, name, "manifest.json")));
|
|
1477
|
+
} catch {
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
return out.sort((a, b) => b.createdAt - a.createdAt);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// src/core/config-edit.ts
|
|
1484
|
+
import { modify, parseTree } from "jsonc-parser";
|
|
1485
|
+
function findProperty(node, key) {
|
|
1486
|
+
if (node?.type !== "object") return void 0;
|
|
1487
|
+
return node.children?.find((child) => child.children?.[0]?.value === key);
|
|
1488
|
+
}
|
|
1489
|
+
function planConfigEdits(text, opts) {
|
|
1490
|
+
const root = parseTree(text);
|
|
1491
|
+
if (!root) return [];
|
|
1492
|
+
const edits = [];
|
|
1493
|
+
const projects = findProperty(root, "projects");
|
|
1494
|
+
const projectsValue = projects?.children?.[1];
|
|
1495
|
+
if (projectsValue?.type === "object") {
|
|
1496
|
+
for (const property of projectsValue.children ?? []) {
|
|
1497
|
+
const keyNode = property.children?.[0];
|
|
1498
|
+
if (typeof keyNode?.value !== "string") continue;
|
|
1499
|
+
const next = reparent(keyNode.value, opts.src, opts.dst, opts.policy, opts.platform);
|
|
1500
|
+
if (next === void 0) continue;
|
|
1501
|
+
edits.push({
|
|
1502
|
+
offset: keyNode.offset,
|
|
1503
|
+
length: keyNode.length,
|
|
1504
|
+
replacement: JSON.stringify(next)
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
const github = findProperty(root, "githubRepoPaths");
|
|
1509
|
+
const githubValue = github?.children?.[1];
|
|
1510
|
+
if (githubValue?.type === "object") {
|
|
1511
|
+
for (const property of githubValue.children ?? []) {
|
|
1512
|
+
const list = property.children?.[1];
|
|
1513
|
+
if (list?.type !== "array") continue;
|
|
1514
|
+
for (const item of list.children ?? []) {
|
|
1515
|
+
if (typeof item.value !== "string") continue;
|
|
1516
|
+
const next = reparent(item.value, opts.src, opts.dst, opts.policy, opts.platform);
|
|
1517
|
+
if (next === void 0) continue;
|
|
1518
|
+
edits.push({ offset: item.offset, length: item.length, replacement: JSON.stringify(next) });
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
return edits;
|
|
1523
|
+
}
|
|
1524
|
+
function applyEdits(text, edits) {
|
|
1525
|
+
let out = text;
|
|
1526
|
+
for (const edit of [...edits].sort((a, b) => b.offset - a.offset)) {
|
|
1527
|
+
out = out.slice(0, edit.offset) + edit.replacement + out.slice(edit.offset + edit.length);
|
|
1528
|
+
}
|
|
1529
|
+
return out;
|
|
1530
|
+
}
|
|
1531
|
+
function planConfigRemoval(text, key) {
|
|
1532
|
+
return modify(text, ["projects", key], void 0, {}).map((edit) => ({
|
|
1533
|
+
offset: edit.offset,
|
|
1534
|
+
length: edit.length,
|
|
1535
|
+
replacement: edit.content
|
|
1536
|
+
}));
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// src/core/journal.ts
|
|
1540
|
+
import { appendFileSync, closeSync, fsyncSync, openSync, readFileSync as readFileSync2, writeSync } from "fs";
|
|
1541
|
+
var Journal = class _Journal {
|
|
1542
|
+
constructor(path3) {
|
|
1543
|
+
this.path = path3;
|
|
1544
|
+
}
|
|
1545
|
+
path;
|
|
1546
|
+
seq = 0;
|
|
1547
|
+
append(entry) {
|
|
1548
|
+
const line = `${JSON.stringify({ seq: this.seq++, t: Date.now(), ...entry })}
|
|
1549
|
+
`;
|
|
1550
|
+
const fd = openSync(this.path, "a");
|
|
1551
|
+
try {
|
|
1552
|
+
writeSync(fd, line);
|
|
1553
|
+
fsyncSync(fd);
|
|
1554
|
+
} finally {
|
|
1555
|
+
closeSync(fd);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
static read(path3) {
|
|
1559
|
+
let raw;
|
|
1560
|
+
try {
|
|
1561
|
+
raw = readFileSync2(path3, "utf8");
|
|
1562
|
+
} catch {
|
|
1563
|
+
return [];
|
|
1564
|
+
}
|
|
1565
|
+
const entries = [];
|
|
1566
|
+
for (const line of raw.split("\n")) {
|
|
1567
|
+
if (!line) continue;
|
|
1568
|
+
try {
|
|
1569
|
+
entries.push(JSON.parse(line));
|
|
1570
|
+
} catch {
|
|
1571
|
+
break;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return entries;
|
|
1575
|
+
}
|
|
1576
|
+
static resume(path3) {
|
|
1577
|
+
const journal = new _Journal(path3);
|
|
1578
|
+
const entries = _Journal.read(path3);
|
|
1579
|
+
journal.seq = (entries.at(-1)?.seq ?? -1) + 1;
|
|
1580
|
+
return journal;
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
|
|
1584
|
+
// src/core/apply.ts
|
|
1585
|
+
async function apply(opts) {
|
|
1586
|
+
const { plan, journal, onEvent } = opts;
|
|
1587
|
+
const emit = (event) => onEvent?.(event);
|
|
1588
|
+
const completed = [];
|
|
1589
|
+
const commitOrder = [
|
|
1590
|
+
"move-cache-dir",
|
|
1591
|
+
"rewrite-transcript",
|
|
1592
|
+
"rewrite-plan",
|
|
1593
|
+
"move-project-dir",
|
|
1594
|
+
"rewrite-history",
|
|
1595
|
+
"edit-config",
|
|
1596
|
+
"move-directory",
|
|
1597
|
+
"repair-worktrees"
|
|
1598
|
+
];
|
|
1599
|
+
const ordered = [...plan.steps].sort(
|
|
1600
|
+
(a, b) => commitOrder.indexOf(a.kind) - commitOrder.indexOf(b.kind)
|
|
1601
|
+
);
|
|
1602
|
+
emit({ kind: "phase", phase: "backup" });
|
|
1603
|
+
if (opts.backup && !opts.dryRun) {
|
|
1604
|
+
journal.append({ kind: "backup-sealed" });
|
|
1605
|
+
seal(opts.backup);
|
|
1606
|
+
}
|
|
1607
|
+
emit({ kind: "phase", phase: "stage" });
|
|
1608
|
+
const staged = [];
|
|
1609
|
+
try {
|
|
1610
|
+
for (const step of ordered) {
|
|
1611
|
+
if (!needsStaging(step)) continue;
|
|
1612
|
+
emit({ kind: "step-start", step });
|
|
1613
|
+
const result = await stage(step, plan, opts.dryRun ?? false);
|
|
1614
|
+
if (result) {
|
|
1615
|
+
staged.push(result);
|
|
1616
|
+
if (!opts.dryRun) {
|
|
1617
|
+
journal.append({
|
|
1618
|
+
kind: "staged",
|
|
1619
|
+
stepId: step.id,
|
|
1620
|
+
staged: result.staged,
|
|
1621
|
+
target: result.target
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
emit({ kind: "step-done", step, detail: result?.detail });
|
|
1626
|
+
}
|
|
1627
|
+
} catch (error) {
|
|
1628
|
+
for (const s of staged) rmSync(s.staged, { force: true });
|
|
1629
|
+
emit({ kind: "step-failed", step: ordered[0], error });
|
|
1630
|
+
return { completed, failed: { step: ordered[0], error } };
|
|
1631
|
+
}
|
|
1632
|
+
if (opts.dryRun) return { completed: ordered, failed: null };
|
|
1633
|
+
journal.append({ kind: "commit-barrier" });
|
|
1634
|
+
emit({ kind: "phase", phase: "commit" });
|
|
1635
|
+
for (const step of ordered) {
|
|
1636
|
+
journal.append({ kind: "step-begin", stepId: step.id, step });
|
|
1637
|
+
try {
|
|
1638
|
+
emit({ kind: "step-start", step });
|
|
1639
|
+
const entry = staged.find((s) => s.step.id === step.id);
|
|
1640
|
+
if (entry) {
|
|
1641
|
+
await commitStaged(entry.staged, entry.target);
|
|
1642
|
+
journal.append({
|
|
1643
|
+
kind: "committed",
|
|
1644
|
+
stepId: step.id,
|
|
1645
|
+
target: entry.target,
|
|
1646
|
+
previousState: "existed"
|
|
1647
|
+
});
|
|
1648
|
+
} else {
|
|
1649
|
+
commitMove(step, journal);
|
|
1650
|
+
}
|
|
1651
|
+
completed.push(step);
|
|
1652
|
+
emit({ kind: "step-done", step });
|
|
1653
|
+
} catch (error) {
|
|
1654
|
+
journal.append({ kind: "step-failed", stepId: step.id, error: String(error) });
|
|
1655
|
+
emit({ kind: "step-failed", step, error });
|
|
1656
|
+
return { completed, failed: { step, error } };
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
journal.append({ kind: "end", ok: true });
|
|
1660
|
+
return { completed, failed: null };
|
|
1661
|
+
}
|
|
1662
|
+
function needsStaging(step) {
|
|
1663
|
+
return step.kind === "rewrite-transcript" || step.kind === "rewrite-history" || step.kind === "edit-config" || step.kind === "rewrite-plan";
|
|
1664
|
+
}
|
|
1665
|
+
async function stage(step, plan, dryRun) {
|
|
1666
|
+
const { src, dst, policy, platform, rewriteProse } = plan.request;
|
|
1667
|
+
const map = (value) => {
|
|
1668
|
+
const whole = reparent(value, src, dst, policy, platform);
|
|
1669
|
+
if (whole !== void 0) return whole;
|
|
1670
|
+
if (!rewriteProse || !value.includes(src)) return void 0;
|
|
1671
|
+
const replaced = replacePathIn(value, src, dst);
|
|
1672
|
+
return replaced === value ? void 0 : replaced;
|
|
1673
|
+
};
|
|
1674
|
+
switch (step.kind) {
|
|
1675
|
+
case "rewrite-transcript": {
|
|
1676
|
+
if (dryRun) return null;
|
|
1677
|
+
const result = await rewriteTranscript(step.file, map, {
|
|
1678
|
+
includeProse: rewriteProse,
|
|
1679
|
+
stagingSuffix: "stage"
|
|
1680
|
+
});
|
|
1681
|
+
if (!result.staged) return null;
|
|
1682
|
+
return {
|
|
1683
|
+
step,
|
|
1684
|
+
staged: result.staged,
|
|
1685
|
+
target: step.file,
|
|
1686
|
+
detail: `${result.linesChanged}/${result.linesTotal} \uC904`
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
case "rewrite-history": {
|
|
1690
|
+
if (dryRun) return null;
|
|
1691
|
+
const result = await rewriteJsonl(
|
|
1692
|
+
step.file,
|
|
1693
|
+
(record) => {
|
|
1694
|
+
if (typeof record !== "object" || record === null) return false;
|
|
1695
|
+
const holder = record;
|
|
1696
|
+
if (typeof holder.project !== "string") return false;
|
|
1697
|
+
const next = map(holder.project);
|
|
1698
|
+
if (next === void 0) return false;
|
|
1699
|
+
holder.project = next;
|
|
1700
|
+
return true;
|
|
1701
|
+
},
|
|
1702
|
+
{ stagingSuffix: "stage" }
|
|
1703
|
+
);
|
|
1704
|
+
if (!result.staged) return null;
|
|
1705
|
+
return {
|
|
1706
|
+
step,
|
|
1707
|
+
staged: result.staged,
|
|
1708
|
+
target: step.file,
|
|
1709
|
+
detail: `${result.linesChanged} \uC904`
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
case "edit-config": {
|
|
1713
|
+
if (dryRun) return null;
|
|
1714
|
+
const text = readFileSync3(step.file, "utf8");
|
|
1715
|
+
const edits = planConfigEdits(text, { src, dst, policy, platform });
|
|
1716
|
+
if (edits.length === 0) return null;
|
|
1717
|
+
const staging = `${step.file}.claude-mv-stage`;
|
|
1718
|
+
await writeFileAtomic(staging, applyEdits(text, edits));
|
|
1719
|
+
return { step, staged: staging, target: step.file, detail: `${edits.length}\uACF3` };
|
|
1720
|
+
}
|
|
1721
|
+
case "rewrite-plan": {
|
|
1722
|
+
if (dryRun) return null;
|
|
1723
|
+
const text = readFileSync3(step.file, "utf8");
|
|
1724
|
+
const next = text.split(src).join(dst);
|
|
1725
|
+
if (next === text) return null;
|
|
1726
|
+
const staging = `${step.file}.claude-mv-stage`;
|
|
1727
|
+
await writeFileAtomic(staging, next);
|
|
1728
|
+
return { step, staged: staging, target: step.file, detail: "\uACBD\uB85C \uCE58\uD658" };
|
|
1729
|
+
}
|
|
1730
|
+
default:
|
|
1731
|
+
return null;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
function commitMove(step, journal) {
|
|
1735
|
+
switch (step.kind) {
|
|
1736
|
+
case "move-project-dir":
|
|
1737
|
+
case "move-cache-dir":
|
|
1738
|
+
case "move-directory": {
|
|
1739
|
+
if (!existsSync4(step.from)) return;
|
|
1740
|
+
mkdirSync2(dirname3(step.to), { recursive: true });
|
|
1741
|
+
moveDirectory(step.from, step.to);
|
|
1742
|
+
journal.append({
|
|
1743
|
+
kind: "committed",
|
|
1744
|
+
stepId: step.id,
|
|
1745
|
+
target: step.to,
|
|
1746
|
+
previousState: "absent",
|
|
1747
|
+
movedFrom: step.from
|
|
1748
|
+
});
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
default:
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
function moveDirectory(from, to) {
|
|
1756
|
+
try {
|
|
1757
|
+
renameSync(from, to);
|
|
1758
|
+
} catch (error) {
|
|
1759
|
+
if (error.code !== "EXDEV") throw error;
|
|
1760
|
+
fsExtra.moveSync(from, to, { overwrite: false });
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
function backupRoot(toolDir) {
|
|
1764
|
+
return join5(toolDir, "backups");
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// src/core/rollback.ts
|
|
1768
|
+
import { createHash as createHash3 } from "crypto";
|
|
1769
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
1770
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
1771
|
+
function rollback(input) {
|
|
1772
|
+
const manifestPath = join6(input.backupDir, "manifest.json");
|
|
1773
|
+
const manifest = readManifest(manifestPath);
|
|
1774
|
+
const journalPath = join6(input.backupDir, "journal.jsonl");
|
|
1775
|
+
const entries = Journal.read(journalPath);
|
|
1776
|
+
const result = {
|
|
1777
|
+
backupId: manifest.backupId,
|
|
1778
|
+
restored: [],
|
|
1779
|
+
reverted: [],
|
|
1780
|
+
removed: [],
|
|
1781
|
+
residue: [],
|
|
1782
|
+
ok: true
|
|
1783
|
+
};
|
|
1784
|
+
if (!manifest.sealed) {
|
|
1785
|
+
cleanStaging(entries, result);
|
|
1786
|
+
return result;
|
|
1787
|
+
}
|
|
1788
|
+
const journal = Journal.resume(journalPath);
|
|
1789
|
+
journal.append({ kind: "rollback-begin" });
|
|
1790
|
+
for (const entry of [...entries].reverse()) {
|
|
1791
|
+
if (entry.kind !== "committed") continue;
|
|
1792
|
+
try {
|
|
1793
|
+
if (entry.movedFrom) {
|
|
1794
|
+
if (!existsSync5(entry.target)) continue;
|
|
1795
|
+
if (existsSync5(entry.movedFrom)) {
|
|
1796
|
+
result.residue.push({
|
|
1797
|
+
path: entry.movedFrom,
|
|
1798
|
+
why: "\uB418\uB3CC\uB9B4 \uC790\uB9AC\uC5D0 \uC774\uBBF8 \uBB54\uAC00 \uC788\uC2B5\uB2C8\uB2E4. \uB36E\uC5B4\uC4F0\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4."
|
|
1799
|
+
});
|
|
1800
|
+
result.ok = false;
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
mkdirSync3(dirname4(entry.movedFrom), { recursive: true });
|
|
1804
|
+
renameSync2(entry.target, entry.movedFrom);
|
|
1805
|
+
result.reverted.push(entry.target);
|
|
1806
|
+
continue;
|
|
1807
|
+
}
|
|
1808
|
+
if (entry.previousState === "absent") {
|
|
1809
|
+
rmSync2(entry.target, { recursive: true, force: true });
|
|
1810
|
+
result.removed.push(entry.target);
|
|
1811
|
+
continue;
|
|
1812
|
+
}
|
|
1813
|
+
const stored = manifest.entries.find((e) => e.type === "file" && e.original === entry.target);
|
|
1814
|
+
if (!stored || stored.type !== "file") {
|
|
1815
|
+
result.residue.push({ path: entry.target, why: "\uBC31\uC5C5\uC5D0 \uC6D0\uBCF8\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." });
|
|
1816
|
+
result.ok = false;
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
const source = join6(input.backupDir, stored.stored);
|
|
1820
|
+
const actual = createHash3("sha256").update(readFileSync4(source)).digest("hex");
|
|
1821
|
+
if (actual !== stored.sha256) {
|
|
1822
|
+
result.residue.push({ path: entry.target, why: "\uBC31\uC5C5 \uD30C\uC77C\uC758 \uCCB4\uD06C\uC12C\uC774 \uB2E4\uB985\uB2C8\uB2E4." });
|
|
1823
|
+
result.ok = false;
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
const staging = `${entry.target}.claude-mv-restore`;
|
|
1827
|
+
copyFileSync2(source, staging);
|
|
1828
|
+
renameSync2(staging, entry.target);
|
|
1829
|
+
result.restored.push(entry.target);
|
|
1830
|
+
} catch (error) {
|
|
1831
|
+
result.residue.push({ path: entry.target, why: String(error) });
|
|
1832
|
+
result.ok = false;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
cleanStaging(entries, result);
|
|
1836
|
+
journal.append({
|
|
1837
|
+
kind: "rollback-end",
|
|
1838
|
+
ok: result.ok,
|
|
1839
|
+
residue: result.residue.map((r) => r.path)
|
|
1840
|
+
});
|
|
1841
|
+
return result;
|
|
1842
|
+
}
|
|
1843
|
+
function cleanStaging(entries, result) {
|
|
1844
|
+
for (const entry of entries) {
|
|
1845
|
+
if (entry.kind !== "staged") continue;
|
|
1846
|
+
if (!existsSync5(entry.staged)) continue;
|
|
1847
|
+
try {
|
|
1848
|
+
rmSync2(entry.staged, { force: true });
|
|
1849
|
+
result.removed.push(entry.staged);
|
|
1850
|
+
} catch {
|
|
1851
|
+
result.residue.push({ path: entry.staged, why: "\uC784\uC2DC \uD30C\uC77C\uC744 \uC9C0\uC6B0\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4." });
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// src/commands/manage.ts
|
|
1857
|
+
function newBackupId() {
|
|
1858
|
+
return `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[-:T]/g, "")}-${ulid().slice(-6)}`;
|
|
1859
|
+
}
|
|
1860
|
+
function dirBytes(dir) {
|
|
1861
|
+
let total = 0;
|
|
1862
|
+
const walk = (path3) => {
|
|
1863
|
+
for (const entry of readdirSync4(path3, { withFileTypes: true })) {
|
|
1864
|
+
const full = join7(path3, entry.name);
|
|
1865
|
+
if (entry.isDirectory()) walk(full);
|
|
1866
|
+
else total += statSync5(full).size;
|
|
1867
|
+
}
|
|
1868
|
+
};
|
|
1869
|
+
try {
|
|
1870
|
+
walk(dir);
|
|
1871
|
+
} catch {
|
|
1872
|
+
return total;
|
|
1873
|
+
}
|
|
1874
|
+
return total;
|
|
1875
|
+
}
|
|
1876
|
+
function runBackups(ctx, reporter) {
|
|
1877
|
+
const root = backupRoot(ctx.env.toolDir);
|
|
1878
|
+
const items = listBackups(root).map((manifest) => ({
|
|
1879
|
+
id: manifest.backupId,
|
|
1880
|
+
createdAt: manifest.createdAt,
|
|
1881
|
+
src: manifest.src,
|
|
1882
|
+
dst: manifest.dst,
|
|
1883
|
+
bytes: dirBytes(join7(root, manifest.backupId)),
|
|
1884
|
+
restorable: manifest.sealed && !existsSync6(manifest.src)
|
|
1885
|
+
}));
|
|
1886
|
+
reporter.backups(items);
|
|
1887
|
+
return "ok";
|
|
1888
|
+
}
|
|
1889
|
+
function runRollback(ctx, reporter, opts) {
|
|
1890
|
+
const root = backupRoot(ctx.env.toolDir);
|
|
1891
|
+
const all = listBackups(root);
|
|
1892
|
+
const manifest = opts.id === "last" ? all[0] : all.find((m) => m.backupId === opts.id);
|
|
1893
|
+
if (!manifest) throw new CliError(`\uADF8\uB7F0 \uBC31\uC5C5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${opts.id}`, "usage");
|
|
1894
|
+
const result = rollback({ backupDir: join7(root, manifest.backupId) });
|
|
1895
|
+
for (const path3 of result.reverted) reporter.note(`\uB418\uB3CC\uB9BC ${path3}`);
|
|
1896
|
+
for (const path3 of result.restored) reporter.note(`\uBCF5\uC6D0 ${path3}`);
|
|
1897
|
+
for (const residue of result.residue) reporter.warn(`${residue.path}: ${residue.why}`);
|
|
1898
|
+
return result.ok ? "ok" : "failedDirty";
|
|
1899
|
+
}
|
|
1900
|
+
function runBackupOnly(ctx, reporter, opts) {
|
|
1901
|
+
const project = findProject(ctx, opts.target, opts.home);
|
|
1902
|
+
if (!project) throw new CliError(`\uB4F1\uB85D\uB41C \uD504\uB85C\uC81D\uD2B8\uAC00 \uC544\uB2D9\uB2C8\uB2E4: ${opts.target}`, "usage");
|
|
1903
|
+
const root = backupRoot(ctx.env.toolDir);
|
|
1904
|
+
mkdirSync4(root, { recursive: true });
|
|
1905
|
+
const backupId = newBackupId();
|
|
1906
|
+
const path3 = project.primaryCwd ?? project.dirPath;
|
|
1907
|
+
const handle = createBackup({
|
|
1908
|
+
root,
|
|
1909
|
+
backupId,
|
|
1910
|
+
version: opts.version,
|
|
1911
|
+
plan: {
|
|
1912
|
+
request: {
|
|
1913
|
+
src: path3,
|
|
1914
|
+
dst: path3,
|
|
1915
|
+
stateOnly: true,
|
|
1916
|
+
rewriteProse: false,
|
|
1917
|
+
policy: ctx.policy,
|
|
1918
|
+
platform: ctx.platform
|
|
1919
|
+
},
|
|
1920
|
+
steps: [
|
|
1921
|
+
...project.transcripts.map((t, i) => ({
|
|
1922
|
+
kind: "rewrite-transcript",
|
|
1923
|
+
id: `t${i}`,
|
|
1924
|
+
file: t.file,
|
|
1925
|
+
movesWithDir: false,
|
|
1926
|
+
affected: 0,
|
|
1927
|
+
total: t.lines
|
|
1928
|
+
})),
|
|
1929
|
+
{
|
|
1930
|
+
kind: "edit-config",
|
|
1931
|
+
id: "config",
|
|
1932
|
+
file: ctx.env.configPath,
|
|
1933
|
+
renameKey: false,
|
|
1934
|
+
githubRepos: []
|
|
1935
|
+
},
|
|
1936
|
+
{
|
|
1937
|
+
kind: "rewrite-history",
|
|
1938
|
+
id: "history",
|
|
1939
|
+
file: ctx.env.historyPath,
|
|
1940
|
+
affected: 0
|
|
1941
|
+
}
|
|
1942
|
+
],
|
|
1943
|
+
blockers: [],
|
|
1944
|
+
warnings: [],
|
|
1945
|
+
proseLeftBehind: 0
|
|
1946
|
+
}
|
|
1947
|
+
});
|
|
1948
|
+
seal(handle);
|
|
1949
|
+
reporter.note(`\uBC31\uC5C5 ${backupId}`);
|
|
1950
|
+
reporter.note(`\uBCF5\uAD6C\uD558\uB824\uBA74 claude-mv rollback ${backupId}`);
|
|
1951
|
+
return "ok";
|
|
1952
|
+
}
|
|
1953
|
+
async function runRemove(ctx, reporter, opts) {
|
|
1954
|
+
const project = findProject(ctx, opts.target, opts.home);
|
|
1955
|
+
if (!project) throw new CliError(`\uB4F1\uB85D\uB41C \uD504\uB85C\uC81D\uD2B8\uAC00 \uC544\uB2D9\uB2C8\uB2E4: ${opts.target}`, "usage");
|
|
1956
|
+
const path3 = project.primaryCwd;
|
|
1957
|
+
if (path3 && ctx.exists(path3) && !opts.force) {
|
|
1958
|
+
throw new CliError(
|
|
1959
|
+
`${path3}\uB294 \uC544\uC9C1 \uC874\uC7AC\uD569\uB2C8\uB2E4. \uC0C1\uD0DC\uB9CC \uC0AD\uC81C\uD558\uB824\uBA74 --force\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.`,
|
|
1960
|
+
"precondition"
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
reporter.info(project);
|
|
1964
|
+
if (!opts.yes && !await reporter.confirm("\uC774 \uD504\uB85C\uC81D\uD2B8\uC758 Claude \uC0C1\uD0DC\uB97C \uC9C0\uC6B8\uAE4C\uC694?")) {
|
|
1965
|
+
reporter.note("\uCDE8\uC18C\uD588\uC2B5\uB2C8\uB2E4.");
|
|
1966
|
+
return "ok";
|
|
1967
|
+
}
|
|
1968
|
+
const root = backupRoot(ctx.env.toolDir);
|
|
1969
|
+
mkdirSync4(root, { recursive: true });
|
|
1970
|
+
const backupId = newBackupId();
|
|
1971
|
+
runBackupOnly(ctx, { ...reporter, note: () => {
|
|
1972
|
+
}, info: () => {
|
|
1973
|
+
} }, opts);
|
|
1974
|
+
const journal = Journal.resume(join7(root, `${backupId}.journal.jsonl`));
|
|
1975
|
+
rmSync3(project.dirPath, { recursive: true, force: true });
|
|
1976
|
+
journal.append({
|
|
1977
|
+
kind: "committed",
|
|
1978
|
+
stepId: "remove-project-dir",
|
|
1979
|
+
target: project.dirPath,
|
|
1980
|
+
previousState: "existed"
|
|
1981
|
+
});
|
|
1982
|
+
if (path3 && ctx.index.configProjectKeys.includes(path3)) {
|
|
1983
|
+
const text = readFileSync5(ctx.env.configPath, "utf8");
|
|
1984
|
+
const edits = planConfigRemoval(text, path3);
|
|
1985
|
+
if (edits.length > 0) {
|
|
1986
|
+
await writeFileAtomic2(ctx.env.configPath, applyEdits(text, edits));
|
|
1987
|
+
journal.append({
|
|
1988
|
+
kind: "committed",
|
|
1989
|
+
stepId: "remove-config-key",
|
|
1990
|
+
target: ctx.env.configPath,
|
|
1991
|
+
previousState: "existed"
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
if (ctx.env.cacheRoot && ctx.index.cacheDirs.includes(project.dirName)) {
|
|
1996
|
+
rmSync3(join7(ctx.env.cacheRoot, project.dirName), { recursive: true, force: true });
|
|
1997
|
+
}
|
|
1998
|
+
reporter.note(`\uC0AD\uC81C\uD588\uC2B5\uB2C8\uB2E4. \uBCF5\uAD6C\uD558\uB824\uBA74 claude-mv rollback ${backupId}`);
|
|
1999
|
+
return "ok";
|
|
2000
|
+
}
|
|
2001
|
+
function runMerge(ctx, reporter, opts) {
|
|
2002
|
+
const from = findProject(ctx, opts.from, opts.home);
|
|
2003
|
+
const to = findProject(ctx, opts.to, opts.home);
|
|
2004
|
+
if (!from) throw new CliError(`\uB4F1\uB85D\uB41C \uD504\uB85C\uC81D\uD2B8\uAC00 \uC544\uB2D9\uB2C8\uB2E4: ${opts.from}`, "usage");
|
|
2005
|
+
if (!to) throw new CliError(`\uB4F1\uB85D\uB41C \uD504\uB85C\uC81D\uD2B8\uAC00 \uC544\uB2D9\uB2C8\uB2E4: ${opts.to}`, "usage");
|
|
2006
|
+
let moved = 0;
|
|
2007
|
+
for (const entry of readdirSync4(from.dirPath, { withFileTypes: true })) {
|
|
2008
|
+
const source = join7(from.dirPath, entry.name);
|
|
2009
|
+
const target = join7(to.dirPath, entry.name);
|
|
2010
|
+
if (existsSync6(target)) {
|
|
2011
|
+
reporter.warn(`\uC774\uBBF8 \uC874\uC7AC\uD574\uC11C \uAC74\uB108\uB701\uB2C8\uB2E4: ${entry.name}`);
|
|
2012
|
+
continue;
|
|
2013
|
+
}
|
|
2014
|
+
renameSync3(source, target);
|
|
2015
|
+
moved++;
|
|
2016
|
+
}
|
|
2017
|
+
reporter.note(`${moved}\uAC1C \uD56D\uBAA9\uC744 \uC62E\uACBC\uC2B5\uB2C8\uB2E4.`);
|
|
2018
|
+
if (readdirSync4(from.dirPath).length === 0) rmSync3(from.dirPath, { recursive: true });
|
|
2019
|
+
else reporter.warn(`${from.dirPath}\uAC00 \uBE44\uC5B4 \uC788\uC9C0 \uC54A\uC544 \uADF8\uB300\uB85C \uB461\uB2C8\uB2E4.`);
|
|
2020
|
+
return "ok";
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// src/commands/mv.ts
|
|
2024
|
+
import { mkdirSync as mkdirSync6 } from "fs";
|
|
2025
|
+
import { join as join10 } from "path";
|
|
2026
|
+
import { ulid as ulid3 } from "ulid";
|
|
2027
|
+
|
|
2028
|
+
// src/core/lock.ts
|
|
2029
|
+
var START_TOLERANCE_MS = 2e3;
|
|
2030
|
+
async function checkLocks(sessions, opts) {
|
|
2031
|
+
const findings = [];
|
|
2032
|
+
for (const session of sessions) {
|
|
2033
|
+
const atOrUnder = pathEquals(session.cwd, opts.src, opts.policy) || isUnder(session.cwd, opts.src, opts.policy, opts.platform);
|
|
2034
|
+
const ancestor = !atOrUnder && isUnder(opts.src, session.cwd, opts.policy, opts.platform);
|
|
2035
|
+
if (!atOrUnder && !ancestor) continue;
|
|
2036
|
+
const relation = atOrUnder ? "at-or-under" : "ancestor";
|
|
2037
|
+
findings.push({ session, relation, ...await judge(session, opts.probe, opts.platform) });
|
|
2038
|
+
}
|
|
2039
|
+
const blocking = findings.filter((f) => {
|
|
2040
|
+
if (f.verdict === "stale") return false;
|
|
2041
|
+
if (f.relation === "ancestor" && opts.allowAncestors) return false;
|
|
2042
|
+
return true;
|
|
2043
|
+
});
|
|
2044
|
+
return { findings, blocking };
|
|
2045
|
+
}
|
|
2046
|
+
async function judge(session, probe, platform) {
|
|
2047
|
+
if (session.pidDomain && session.pidDomain !== platform) {
|
|
2048
|
+
return { verdict: "unknown", why: `\uB2E4\uB978 OS(${session.pidDomain})\uC5D0\uC11C \uAE30\uB85D\uB41C \uC138\uC158\uC785\uB2C8\uB2E4.` };
|
|
2049
|
+
}
|
|
2050
|
+
if (!await probe.exists(session.pid)) {
|
|
2051
|
+
return { verdict: "stale", why: "\uD504\uB85C\uC138\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." };
|
|
2052
|
+
}
|
|
2053
|
+
const recorded = session.startedAtMs;
|
|
2054
|
+
const actual = await probe.startedAt(session.pid);
|
|
2055
|
+
if (recorded === null || actual === null) {
|
|
2056
|
+
return { verdict: "unknown", why: "\uC2DC\uC791 \uC2DC\uAC01\uC744 \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4." };
|
|
2057
|
+
}
|
|
2058
|
+
if (Math.abs(recorded - actual) > START_TOLERANCE_MS) {
|
|
2059
|
+
return { verdict: "stale", why: "pid \uAC00 \uC7AC\uC0AC\uC6A9\uB418\uC5C8\uC2B5\uB2C8\uB2E4." };
|
|
2060
|
+
}
|
|
2061
|
+
return { verdict: "live", why: "\uC2E4\uD589 \uC911\uC785\uB2C8\uB2E4." };
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// src/core/migration.ts
|
|
2065
|
+
import { mkdirSync as mkdirSync5 } from "fs";
|
|
2066
|
+
import { join as join9 } from "path";
|
|
2067
|
+
import { ulid as ulid2 } from "ulid";
|
|
2068
|
+
|
|
2069
|
+
// src/core/plan.ts
|
|
2070
|
+
import { join as join8 } from "path";
|
|
2071
|
+
function affectedCount(t, req) {
|
|
2072
|
+
let n = 0;
|
|
2073
|
+
for (const [path3, lines] of t.paths) {
|
|
2074
|
+
if (reparent(path3, req.src, req.dst, req.policy, req.platform) !== void 0) n += lines;
|
|
2075
|
+
}
|
|
2076
|
+
return Math.min(n, t.lines);
|
|
2077
|
+
}
|
|
2078
|
+
function isMixed(t, req) {
|
|
2079
|
+
const inside = [];
|
|
2080
|
+
const outside = [];
|
|
2081
|
+
for (const path3 of t.cwds.keys()) {
|
|
2082
|
+
if (reparent(path3, req.src, req.dst, req.policy, req.platform) !== void 0) inside.push(path3);
|
|
2083
|
+
else outside.push(path3);
|
|
2084
|
+
}
|
|
2085
|
+
return inside.length > 0 && outside.length > 0 ? [...inside, ...outside] : [];
|
|
2086
|
+
}
|
|
2087
|
+
function buildPlan(req, ctx) {
|
|
2088
|
+
const steps = [];
|
|
2089
|
+
const blockers = [];
|
|
2090
|
+
const warnings = [];
|
|
2091
|
+
const srcMangled = mangle(req.src);
|
|
2092
|
+
const dstMangled = mangle(req.dst);
|
|
2093
|
+
if (isUnder(req.dst, req.src, req.policy, req.platform)) {
|
|
2094
|
+
blockers.push({ kind: "dst-inside-src" });
|
|
2095
|
+
}
|
|
2096
|
+
if (srcMangled === dstMangled && !pathEquals(req.src, req.dst, req.policy)) {
|
|
2097
|
+
warnings.push({ kind: "mangle-collision", other: req.dst });
|
|
2098
|
+
}
|
|
2099
|
+
if (!req.stateOnly && !ctx.srcExists) blockers.push({ kind: "src-missing" });
|
|
2100
|
+
const blocking = ctx.locks.blocking.map((finding) => finding.session);
|
|
2101
|
+
if (blocking.length > 0) blockers.push({ kind: "locked", sessions: blocking });
|
|
2102
|
+
const planFiles = /* @__PURE__ */ new Set();
|
|
2103
|
+
for (const project of ctx.index.projects) {
|
|
2104
|
+
const owns = project.dirName === srcMangled;
|
|
2105
|
+
const touched = project.transcripts.filter((t) => affectedCount(t, req) > 0);
|
|
2106
|
+
if (!owns && touched.length === 0) continue;
|
|
2107
|
+
if (owns) {
|
|
2108
|
+
if (ctx.index.projects.some((p) => p.dirName === dstMangled)) {
|
|
2109
|
+
blockers.push({ kind: "dst-project-dir-exists", dirName: dstMangled });
|
|
2110
|
+
}
|
|
2111
|
+
steps.push({
|
|
2112
|
+
kind: "move-project-dir",
|
|
2113
|
+
id: "project-dir",
|
|
2114
|
+
from: project.dirPath,
|
|
2115
|
+
to: join8(ctx.projectsDir, dstMangled),
|
|
2116
|
+
files: project.transcripts.length + project.otherFiles.length
|
|
2117
|
+
});
|
|
2118
|
+
} else if (touched.length > 0) {
|
|
2119
|
+
warnings.push({
|
|
2120
|
+
kind: "rewritten-in-parent-dir",
|
|
2121
|
+
dirName: project.dirName,
|
|
2122
|
+
files: touched.length
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
const liveSessionIds = new Set(
|
|
2126
|
+
ctx.locks.findings.filter((f) => f.verdict !== "stale").map((f) => f.session.sessionId)
|
|
2127
|
+
);
|
|
2128
|
+
for (const t of touched) {
|
|
2129
|
+
for (const path3 of t.paths.keys()) {
|
|
2130
|
+
if (isUnder(path3, ctx.plansDir, req.policy, req.platform)) planFiles.add(path3);
|
|
2131
|
+
}
|
|
2132
|
+
if (ctx.skipLiveTranscripts && t.kind === "session" && liveSessionIds.has(t.sessionId)) {
|
|
2133
|
+
warnings.push({ kind: "skipped-live", sessionId: t.sessionId });
|
|
2134
|
+
continue;
|
|
2135
|
+
}
|
|
2136
|
+
const mixed = isMixed(t, req);
|
|
2137
|
+
if (mixed.length > 0) warnings.push({ kind: "mixed-cwd", file: t.file, paths: mixed });
|
|
2138
|
+
steps.push({
|
|
2139
|
+
kind: "rewrite-transcript",
|
|
2140
|
+
id: `transcript:${t.sessionId}${t.agentId ? `/${t.agentId}` : ""}`,
|
|
2141
|
+
file: t.file,
|
|
2142
|
+
movesWithDir: owns,
|
|
2143
|
+
affected: affectedCount(t, req),
|
|
2144
|
+
total: t.lines
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
for (const file of planFiles) {
|
|
2149
|
+
if (!ctx.fileExists(file)) continue;
|
|
2150
|
+
steps.push({ kind: "rewrite-plan", id: `plan:${file}`, file });
|
|
2151
|
+
}
|
|
2152
|
+
const located = locateInConfig(ctx.index, req);
|
|
2153
|
+
if (located.renameKey || located.githubRepos.length > 0) {
|
|
2154
|
+
if (located.dstKeyExists) blockers.push({ kind: "dst-config-key-exists", key: req.dst });
|
|
2155
|
+
steps.push({
|
|
2156
|
+
kind: "edit-config",
|
|
2157
|
+
id: "claude-json",
|
|
2158
|
+
file: ctx.configPath,
|
|
2159
|
+
renameKey: located.renameKey,
|
|
2160
|
+
githubRepos: located.githubRepos
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
const historyAffected = [...ctx.index.historyProjects].filter(([p]) => reparent(p, req.src, req.dst, req.policy, req.platform) !== void 0).reduce((sum, [, n]) => sum + n, 0);
|
|
2164
|
+
if (historyAffected > 0) {
|
|
2165
|
+
steps.push({
|
|
2166
|
+
kind: "rewrite-history",
|
|
2167
|
+
id: "history",
|
|
2168
|
+
file: ctx.historyPath,
|
|
2169
|
+
affected: historyAffected
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
if (ctx.cacheRoot && ctx.index.cacheDirs.includes(srcMangled)) {
|
|
2173
|
+
steps.push({
|
|
2174
|
+
kind: "move-cache-dir",
|
|
2175
|
+
id: "mcp-logs",
|
|
2176
|
+
from: join8(ctx.cacheRoot, srcMangled),
|
|
2177
|
+
to: join8(ctx.cacheRoot, dstMangled)
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
if (!req.stateOnly && ctx.srcExists) {
|
|
2181
|
+
steps.push({ kind: "move-directory", id: "working-tree", from: req.src, to: req.dst });
|
|
2182
|
+
}
|
|
2183
|
+
if (steps.length === 0) warnings.push({ kind: "nothing-to-do" });
|
|
2184
|
+
const proseLeftBehind = req.rewriteProse ? 0 : countProse(ctx.index, req);
|
|
2185
|
+
return { request: req, steps, blockers, warnings, proseLeftBehind };
|
|
2186
|
+
}
|
|
2187
|
+
function locateInConfig(index, req) {
|
|
2188
|
+
return {
|
|
2189
|
+
renameKey: index.configProjectKeys.some((k) => pathEquals(k, req.src, req.policy)),
|
|
2190
|
+
dstKeyExists: index.configProjectKeys.some((k) => pathEquals(k, req.dst, req.policy)),
|
|
2191
|
+
githubRepos: Object.entries(index.githubRepoPaths).filter(
|
|
2192
|
+
([, paths]) => paths.some((p) => reparent(p, req.src, req.dst, req.policy, req.platform) !== void 0)
|
|
2193
|
+
).map(([repo]) => repo)
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
function countProse(index, req) {
|
|
2197
|
+
let n = 0;
|
|
2198
|
+
for (const project of index.projects) {
|
|
2199
|
+
for (const t of project.transcripts) {
|
|
2200
|
+
for (const [path3, lines] of t.prose) {
|
|
2201
|
+
if (reparent(path3, req.src, req.dst, req.policy, req.platform) !== void 0) n += lines;
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
return n;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// src/core/migration.ts
|
|
2209
|
+
function newBackupId2() {
|
|
2210
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[-:T]/g, "");
|
|
2211
|
+
return `${stamp}-${ulid2().slice(-6)}`;
|
|
2212
|
+
}
|
|
2213
|
+
var Migration = class _Migration {
|
|
2214
|
+
constructor(plan, toolDir) {
|
|
2215
|
+
this.plan = plan;
|
|
2216
|
+
this.toolDir = toolDir;
|
|
2217
|
+
}
|
|
2218
|
+
plan;
|
|
2219
|
+
toolDir;
|
|
2220
|
+
static prepare(request, context) {
|
|
2221
|
+
return new _Migration(buildPlan(request, context), context.toolDir);
|
|
2222
|
+
}
|
|
2223
|
+
get blocked() {
|
|
2224
|
+
return this.plan.blockers.length > 0;
|
|
2225
|
+
}
|
|
2226
|
+
get empty() {
|
|
2227
|
+
return this.plan.steps.length === 0;
|
|
2228
|
+
}
|
|
2229
|
+
async run(opts) {
|
|
2230
|
+
const { plan } = this;
|
|
2231
|
+
if (this.blocked) return { status: "blocked", plan };
|
|
2232
|
+
const root = backupRoot(this.toolDir);
|
|
2233
|
+
mkdirSync5(root, { recursive: true });
|
|
2234
|
+
const backupId = newBackupId2();
|
|
2235
|
+
const backup = opts.backup ? createBackup({ root, backupId, version: "runtime", plan }) : null;
|
|
2236
|
+
const journal = Journal.resume(
|
|
2237
|
+
backup ? backup.journalPath : join9(root, `${backupId}.journal.jsonl`)
|
|
2238
|
+
);
|
|
2239
|
+
journal.append({ kind: "begin", backupId, plan });
|
|
2240
|
+
const startedAt2 = Date.now();
|
|
2241
|
+
const result = await apply({ plan, backup, journal, onEvent: opts.onEvent });
|
|
2242
|
+
if (!result.failed) {
|
|
2243
|
+
return {
|
|
2244
|
+
status: "applied",
|
|
2245
|
+
plan,
|
|
2246
|
+
backupId: backup?.id ?? null,
|
|
2247
|
+
durationMs: Date.now() - startedAt2
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
if (!backup) {
|
|
2251
|
+
return { status: "dirty", plan, backupId: null, error: result.failed.error };
|
|
2252
|
+
}
|
|
2253
|
+
const reverted = rollback({ backupDir: backup.dir });
|
|
2254
|
+
return reverted.ok ? {
|
|
2255
|
+
status: "rolled-back",
|
|
2256
|
+
plan,
|
|
2257
|
+
backupId: backup.id,
|
|
2258
|
+
error: result.failed.error,
|
|
2259
|
+
rollback: reverted
|
|
2260
|
+
} : { status: "dirty", plan, backupId: backup.id, error: result.failed.error };
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
|
|
2264
|
+
// src/commands/mv.ts
|
|
2265
|
+
async function runMove(ctx, reporter, opts) {
|
|
2266
|
+
const norm = (value, realpath = true) => normalizePath(value, { platform: ctx.platform, home: opts.home, realpath });
|
|
2267
|
+
const src = pickKnown(ctx, [norm(opts.src), norm(opts.src, false)]);
|
|
2268
|
+
const dst = norm(opts.dst, false);
|
|
2269
|
+
if (src === dst) throw new CliError("src\uC640 dst\uAC00 \uAC19\uC2B5\uB2C8\uB2E4.", "usage");
|
|
2270
|
+
const request = {
|
|
2271
|
+
src,
|
|
2272
|
+
dst,
|
|
2273
|
+
stateOnly: opts.stateOnly || !ctx.exists(src) && ctx.exists(dst),
|
|
2274
|
+
rewriteProse: opts.rewriteProse,
|
|
2275
|
+
policy: ctx.policy,
|
|
2276
|
+
platform: ctx.platform
|
|
2277
|
+
};
|
|
2278
|
+
const locks = await checkLocks(ctx.index.sessions, {
|
|
2279
|
+
src,
|
|
2280
|
+
policy: ctx.policy,
|
|
2281
|
+
platform: ctx.platform,
|
|
2282
|
+
probe: ctx.probe,
|
|
2283
|
+
allowAncestors: opts.allowAncestors
|
|
2284
|
+
});
|
|
2285
|
+
const migration = Migration.prepare(request, {
|
|
2286
|
+
index: ctx.index,
|
|
2287
|
+
configPath: ctx.env.configPath,
|
|
2288
|
+
historyPath: ctx.env.historyPath,
|
|
2289
|
+
projectsDir: ctx.env.projectsDir,
|
|
2290
|
+
plansDir: ctx.env.plansDir,
|
|
2291
|
+
cacheRoot: ctx.env.cacheRoot,
|
|
2292
|
+
toolDir: ctx.env.toolDir,
|
|
2293
|
+
fileExists: ctx.exists,
|
|
2294
|
+
srcExists: ctx.exists(src),
|
|
2295
|
+
dstExists: ctx.exists(dst),
|
|
2296
|
+
locks: opts.force ? { findings: locks.findings, blocking: [] } : locks,
|
|
2297
|
+
skipLiveTranscripts: opts.skipLive
|
|
2298
|
+
});
|
|
2299
|
+
const plan = migration.plan;
|
|
2300
|
+
const informational = locks.findings.filter((finding) => !locks.blocking.includes(finding));
|
|
2301
|
+
if (informational.length > 0) reporter.locks({ findings: informational, blocking: [] });
|
|
2302
|
+
if (plan.warnings.some((w) => w.kind === "nothing-to-do")) {
|
|
2303
|
+
reporter.plan(plan, { backupId: null, dryRun: true });
|
|
2304
|
+
reporter.note("\uC62E\uAE38 \uC0C1\uD0DC\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.");
|
|
2305
|
+
return "nothingToDo";
|
|
2306
|
+
}
|
|
2307
|
+
if (opts.dryRun || plan.blockers.length > 0) {
|
|
2308
|
+
reporter.plan(plan, { backupId: null, dryRun: true });
|
|
2309
|
+
for (const blocker of plan.blockers) reporter.warn(describeBlocker(blocker));
|
|
2310
|
+
if (plan.blockers.length > 0) return blockerExit(plan);
|
|
2311
|
+
return "planned";
|
|
2312
|
+
}
|
|
2313
|
+
const backupId = `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[-:T]/g, "")}-${ulid3().slice(-6)}`;
|
|
2314
|
+
reporter.plan(plan, { backupId: opts.noBackup ? null : backupId, dryRun: false });
|
|
2315
|
+
if (!opts.yes && !await reporter.confirm(`${plan.steps.length}\uAC1C \uD56D\uBAA9\uC744 \uC62E\uAE41\uB2C8\uB2E4. \uC9C4\uD589\uD560\uAE4C\uC694?`)) {
|
|
2316
|
+
reporter.note("\uCDE8\uC18C\uD588\uC2B5\uB2C8\uB2E4.");
|
|
2317
|
+
return "ok";
|
|
2318
|
+
}
|
|
2319
|
+
const root = backupRoot(ctx.env.toolDir);
|
|
2320
|
+
mkdirSync6(root, { recursive: true });
|
|
2321
|
+
const backup = opts.noBackup ? null : createBackup({ root, backupId, version: opts.version, plan });
|
|
2322
|
+
const journal = Journal.resume(
|
|
2323
|
+
backup ? backup.journalPath : join10(root, `${backupId}.journal.jsonl`)
|
|
2324
|
+
);
|
|
2325
|
+
journal.append({ kind: "begin", backupId, plan });
|
|
2326
|
+
const startedAt2 = Date.now();
|
|
2327
|
+
const result = await apply({
|
|
2328
|
+
plan,
|
|
2329
|
+
backup,
|
|
2330
|
+
journal,
|
|
2331
|
+
onEvent: (event) => reporter.progress(event)
|
|
2332
|
+
});
|
|
2333
|
+
if (!result.failed) {
|
|
2334
|
+
reporter.applied(plan, {
|
|
2335
|
+
backupId: backup?.id ?? null,
|
|
2336
|
+
durationMs: Date.now() - startedAt2
|
|
2337
|
+
});
|
|
2338
|
+
return "ok";
|
|
2339
|
+
}
|
|
2340
|
+
if (!backup) {
|
|
2341
|
+
reporter.failed(plan, { error: result.failed.error, rolledBack: false, backupId: null });
|
|
2342
|
+
return "failedDirty";
|
|
2343
|
+
}
|
|
2344
|
+
const reverted = rollback({ backupDir: backup.dir });
|
|
2345
|
+
reporter.failed(plan, {
|
|
2346
|
+
error: result.failed.error,
|
|
2347
|
+
rolledBack: reverted.ok,
|
|
2348
|
+
backupId: backup.id
|
|
2349
|
+
});
|
|
2350
|
+
for (const residue of reverted.residue) reporter.warn(`${residue.path}: ${residue.why}`);
|
|
2351
|
+
return reverted.ok ? "failedRolledBack" : "failedDirty";
|
|
2352
|
+
}
|
|
2353
|
+
function pickKnown(ctx, candidates) {
|
|
2354
|
+
for (const candidate of candidates) {
|
|
2355
|
+
const known = ctx.index.projects.some(
|
|
2356
|
+
(p) => p.primaryCwd && pathEquals(p.primaryCwd, candidate, ctx.policy) || [...p.cwdCensus.keys()].some((c) => pathEquals(c, candidate, ctx.policy))
|
|
2357
|
+
) || ctx.index.configProjectKeys.some((k) => pathEquals(k, candidate, ctx.policy));
|
|
2358
|
+
if (known) return candidate;
|
|
2359
|
+
}
|
|
2360
|
+
return candidates[0];
|
|
2361
|
+
}
|
|
2362
|
+
function blockerExit(plan) {
|
|
2363
|
+
if (plan.blockers.some((b) => b.kind === "locked")) return "locked";
|
|
2364
|
+
if (plan.blockers.some((b) => b.kind === "src-missing" || b.kind === "dst-inside-src")) {
|
|
2365
|
+
return "precondition";
|
|
2366
|
+
}
|
|
2367
|
+
return "conflict";
|
|
2368
|
+
}
|
|
2369
|
+
function describeBlocker(blocker) {
|
|
2370
|
+
switch (blocker.kind) {
|
|
2371
|
+
case "locked":
|
|
2372
|
+
return `claude \uC138\uC158\uC774 \uC774 \uACBD\uB85C\uB97C \uC0AC\uC6A9\uD558\uACE0 \uC788\uC2B5\uB2C8\uB2E4 (pid ${blocker.sessions.map((s) => s.pid).join(", ")}). \uC138\uC158\uC744 \uC885\uB8CC\uD55C \uB4A4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.`;
|
|
2373
|
+
case "dst-project-dir-exists":
|
|
2374
|
+
return `\uB300\uC0C1 \uACBD\uB85C\uC758 project \uB514\uB809\uD130\uB9AC\uAC00 \uC774\uBBF8 \uC788\uC2B5\uB2C8\uB2E4: ${blocker.dirName}`;
|
|
2375
|
+
case "dst-config-key-exists":
|
|
2376
|
+
return `claude.json\uC5D0 \uB300\uC0C1 \uACBD\uB85C \uD0A4\uAC00 \uC774\uBBF8 \uC788\uC2B5\uB2C8\uB2E4: ${blocker.key}`;
|
|
2377
|
+
case "src-missing":
|
|
2378
|
+
return "\uC6D0\uBCF8 \uB514\uB809\uD130\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uC774\uBBF8 \uC62E\uACBC\uB2E4\uBA74 --state-only\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.";
|
|
2379
|
+
case "dst-inside-src":
|
|
2380
|
+
return "\uB300\uC0C1 \uACBD\uB85C\uAC00 \uC6D0\uBCF8 \uC548\uC5D0 \uC788\uC2B5\uB2C8\uB2E4.";
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
// src/commands/transfer.ts
|
|
2385
|
+
import {
|
|
2386
|
+
cpSync,
|
|
2387
|
+
existsSync as existsSync7,
|
|
2388
|
+
mkdirSync as mkdirSync7,
|
|
2389
|
+
mkdtempSync,
|
|
2390
|
+
readFileSync as readFileSync6,
|
|
2391
|
+
rmSync as rmSync4,
|
|
2392
|
+
writeFileSync as writeFileSync2
|
|
2393
|
+
} from "fs";
|
|
2394
|
+
import { tmpdir } from "os";
|
|
2395
|
+
import { basename as basename2, join as join11 } from "path";
|
|
2396
|
+
import * as tar from "tar";
|
|
2397
|
+
var MANIFEST = "claude-mv-bundle.json";
|
|
2398
|
+
function runExport(ctx, reporter, opts) {
|
|
2399
|
+
const project = findProject(ctx, opts.target, opts.home);
|
|
2400
|
+
if (!project) throw new CliError(`\uB4F1\uB85D\uB41C \uD504\uB85C\uC81D\uD2B8\uAC00 \uC544\uB2D9\uB2C8\uB2E4: ${opts.target}`, "usage");
|
|
2401
|
+
const bundle = {
|
|
2402
|
+
version: 1,
|
|
2403
|
+
tool: "claude-mv",
|
|
2404
|
+
originalPath: project.primaryCwd ?? project.dirPath,
|
|
2405
|
+
dirName: project.dirName,
|
|
2406
|
+
exportedAt: Date.now(),
|
|
2407
|
+
platform: ctx.platform
|
|
2408
|
+
};
|
|
2409
|
+
const staging = mkdtempSync(join11(tmpdir(), "claude-mv-export-"));
|
|
2410
|
+
writeFileSync2(join11(staging, MANIFEST), JSON.stringify(bundle, null, 2));
|
|
2411
|
+
cpSync(project.dirPath, join11(staging, "projects", project.dirName), { recursive: true });
|
|
2412
|
+
const out = opts.out ?? `${basename2(bundle.originalPath)}-claude-state.tgz`;
|
|
2413
|
+
tar.create({ gzip: true, file: out, sync: true, cwd: staging }, [MANIFEST, "projects"]);
|
|
2414
|
+
rmSync4(staging, { recursive: true, force: true });
|
|
2415
|
+
reporter.note(`${out}\uC5D0 \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4.`);
|
|
2416
|
+
return "ok";
|
|
2417
|
+
}
|
|
2418
|
+
function readBundle(file) {
|
|
2419
|
+
const staging = mkdtempSync(join11(tmpdir(), "claude-mv-peek-"));
|
|
2420
|
+
tar.extract({ file, sync: true, cwd: staging }, [MANIFEST]);
|
|
2421
|
+
const manifestPath = join11(staging, MANIFEST);
|
|
2422
|
+
if (!existsSync7(manifestPath)) {
|
|
2423
|
+
throw new CliError("claude-mv\uB85C \uB9CC\uB4E0 \uBC88\uB4E4\uC774 \uC544\uB2D9\uB2C8\uB2E4.", "usage");
|
|
2424
|
+
}
|
|
2425
|
+
return JSON.parse(readFileSync6(manifestPath, "utf8"));
|
|
2426
|
+
}
|
|
2427
|
+
function runImport(ctx, reporter, opts) {
|
|
2428
|
+
const bundle = readBundle(opts.file);
|
|
2429
|
+
const staging = mkdtempSync(join11(tmpdir(), "claude-mv-import-"));
|
|
2430
|
+
tar.extract({ file: opts.file, sync: true, cwd: staging });
|
|
2431
|
+
const source = join11(staging, "projects", bundle.dirName);
|
|
2432
|
+
if (!existsSync7(source)) throw new CliError("\uBC88\uB4E4\uC5D0 project \uB514\uB809\uD130\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", "usage");
|
|
2433
|
+
const destination = join11(ctx.env.projectsDir, bundle.dirName);
|
|
2434
|
+
if (existsSync7(destination)) {
|
|
2435
|
+
throw new CliError(
|
|
2436
|
+
`\uC774\uBBF8 \uC874\uC7AC\uD558\uB294 \uC0C1\uD0DC\uB97C \uB36E\uC5B4\uC4F0\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${bundle.dirName}. merge\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.`,
|
|
2437
|
+
"conflict"
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2440
|
+
mkdirSync7(ctx.env.projectsDir, { recursive: true });
|
|
2441
|
+
cpSync(source, destination, { recursive: true });
|
|
2442
|
+
rmSync4(staging, { recursive: true, force: true });
|
|
2443
|
+
reporter.note(`${bundle.originalPath}\uC758 \uC0C1\uD0DC\uB97C \uB4E4\uC5EC\uC654\uC2B5\uB2C8\uB2E4.`);
|
|
2444
|
+
if (!opts.to) return { code: "ok" };
|
|
2445
|
+
const to = normalizePath(opts.to, { platform: ctx.platform, home: opts.home, realpath: false });
|
|
2446
|
+
reporter.note(`\uC774\uC5B4\uC11C ${bundle.originalPath}\uC5D0\uC11C ${to}\uB85C \uC774\uAD00\uD569\uB2C8\uB2E4.`);
|
|
2447
|
+
return { code: "ok", migrateFrom: bundle.originalPath, migrateTo: to };
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// src/render/json/reporter.ts
|
|
2451
|
+
var SCHEMA_VERSION = 1;
|
|
2452
|
+
var JsonReporter = class {
|
|
2453
|
+
constructor(out = process.stdout, err = process.stderr, version = "0.0.0") {
|
|
2454
|
+
this.out = out;
|
|
2455
|
+
this.err = err;
|
|
2456
|
+
this.version = version;
|
|
2457
|
+
}
|
|
2458
|
+
out;
|
|
2459
|
+
err;
|
|
2460
|
+
version;
|
|
2461
|
+
result = null;
|
|
2462
|
+
envelope(kind, body) {
|
|
2463
|
+
return { tool: "claude-mv", version: this.version, schema: SCHEMA_VERSION, kind, ...body };
|
|
2464
|
+
}
|
|
2465
|
+
event(body) {
|
|
2466
|
+
this.err.write(`${JSON.stringify(body)}
|
|
2467
|
+
`);
|
|
2468
|
+
}
|
|
2469
|
+
plan(plan, opts) {
|
|
2470
|
+
this.result = this.envelope(opts.dryRun ? "plan" : "pending", {
|
|
2471
|
+
src: plan.request.src,
|
|
2472
|
+
dst: plan.request.dst,
|
|
2473
|
+
options: {
|
|
2474
|
+
stateOnly: plan.request.stateOnly,
|
|
2475
|
+
rewriteProse: plan.request.rewriteProse
|
|
2476
|
+
},
|
|
2477
|
+
steps: plan.steps,
|
|
2478
|
+
blockers: plan.blockers,
|
|
2479
|
+
warnings: plan.warnings,
|
|
2480
|
+
proseLeftBehind: plan.proseLeftBehind,
|
|
2481
|
+
backup: opts.backupId ? { id: opts.backupId } : null
|
|
2482
|
+
});
|
|
2483
|
+
}
|
|
2484
|
+
async confirm() {
|
|
2485
|
+
return false;
|
|
2486
|
+
}
|
|
2487
|
+
progress(event) {
|
|
2488
|
+
this.event({
|
|
2489
|
+
kind: "progress",
|
|
2490
|
+
event: event.kind,
|
|
2491
|
+
step: "step" in event ? event.step.id : void 0,
|
|
2492
|
+
phase: "phase" in event ? event.phase : void 0,
|
|
2493
|
+
detail: "detail" in event ? event.detail : void 0
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
applied(plan, opts) {
|
|
2497
|
+
this.result = this.envelope("result", {
|
|
2498
|
+
src: plan.request.src,
|
|
2499
|
+
dst: plan.request.dst,
|
|
2500
|
+
steps: plan.steps.map((s) => ({ ...s, status: "done" })),
|
|
2501
|
+
backup: opts.backupId ? { id: opts.backupId } : null,
|
|
2502
|
+
durationMs: opts.durationMs,
|
|
2503
|
+
ok: true
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2506
|
+
failed(plan, opts) {
|
|
2507
|
+
this.result = this.envelope("result", {
|
|
2508
|
+
src: plan.request.src,
|
|
2509
|
+
dst: plan.request.dst,
|
|
2510
|
+
ok: false,
|
|
2511
|
+
error: opts.error.message,
|
|
2512
|
+
rollback: { attempted: true, ok: opts.rolledBack },
|
|
2513
|
+
backup: opts.backupId ? { id: opts.backupId } : null
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
locks(report) {
|
|
2517
|
+
this.event({ kind: "locks", findings: report.findings });
|
|
2518
|
+
}
|
|
2519
|
+
doctor(report) {
|
|
2520
|
+
this.result = this.envelope("doctor", {
|
|
2521
|
+
orphans: report.orphans.map((entry) => ({
|
|
2522
|
+
encoded: entry.project.dirName,
|
|
2523
|
+
path: entry.project.primaryCwd,
|
|
2524
|
+
pathSource: entry.project.primaryCwd ? "transcript" : "unknown",
|
|
2525
|
+
health: entry.project.health,
|
|
2526
|
+
sizeBytes: entry.project.bytes,
|
|
2527
|
+
lastActivity: entry.project.lastActivityMs,
|
|
2528
|
+
transcripts: entry.project.transcripts.length,
|
|
2529
|
+
alsoIn: entry.alsoIn,
|
|
2530
|
+
suggestions: entry.suggestions
|
|
2531
|
+
})),
|
|
2532
|
+
healthy: report.healthy.map((p) => ({ path: p.primaryCwd, encoded: p.dirName })),
|
|
2533
|
+
collisions: report.collisions
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
list(projects) {
|
|
2537
|
+
this.result = this.envelope("list", {
|
|
2538
|
+
projects: projects.map((p) => ({
|
|
2539
|
+
path: p.primaryCwd,
|
|
2540
|
+
encoded: p.dirName,
|
|
2541
|
+
health: p.health,
|
|
2542
|
+
sizeBytes: p.bytes,
|
|
2543
|
+
transcripts: p.transcripts.length,
|
|
2544
|
+
lastActivity: p.lastActivityMs
|
|
2545
|
+
}))
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2548
|
+
info(project) {
|
|
2549
|
+
this.result = this.envelope("info", {
|
|
2550
|
+
path: project.primaryCwd,
|
|
2551
|
+
encoded: project.dirName,
|
|
2552
|
+
dir: project.dirPath,
|
|
2553
|
+
health: project.health,
|
|
2554
|
+
sizeBytes: project.bytes,
|
|
2555
|
+
transcripts: project.transcripts.map((t) => ({
|
|
2556
|
+
kind: t.kind,
|
|
2557
|
+
sessionId: t.sessionId,
|
|
2558
|
+
agentId: t.agentId,
|
|
2559
|
+
lines: t.lines,
|
|
2560
|
+
bytes: t.bytes,
|
|
2561
|
+
malformed: t.malformed.length
|
|
2562
|
+
})),
|
|
2563
|
+
cwdCensus: Object.fromEntries(project.cwdCensus)
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
backups(items) {
|
|
2567
|
+
this.result = this.envelope("backups", { backups: items });
|
|
2568
|
+
}
|
|
2569
|
+
note(message) {
|
|
2570
|
+
this.event({ kind: "note", message });
|
|
2571
|
+
}
|
|
2572
|
+
warn(message) {
|
|
2573
|
+
this.event({ kind: "warn", message });
|
|
2574
|
+
}
|
|
2575
|
+
async close() {
|
|
2576
|
+
if (this.result) this.out.write(`${JSON.stringify(this.result, null, 2)}
|
|
2577
|
+
`);
|
|
2578
|
+
}
|
|
2579
|
+
};
|
|
2580
|
+
|
|
2581
|
+
// src/render/reporter.ts
|
|
2582
|
+
function resolveRenderMode(opts) {
|
|
2583
|
+
if (opts.json) return "json";
|
|
2584
|
+
if (!opts.isTTY) return "plain";
|
|
2585
|
+
if (opts.ci) return "plain";
|
|
2586
|
+
if (opts.term === "dumb") return "plain";
|
|
2587
|
+
if (opts.quiet) return "plain";
|
|
2588
|
+
return "ink";
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
// src/index.ts
|
|
2592
|
+
async function main(argv) {
|
|
2593
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
2594
|
+
process.stdout.write(`${package_default.version}
|
|
2595
|
+
`);
|
|
2596
|
+
return ExitCode.ok;
|
|
2597
|
+
}
|
|
2598
|
+
const { command, rest } = dispatch(argv);
|
|
2599
|
+
if (command === "__complete") return await runComplete(rest);
|
|
2600
|
+
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
2601
|
+
process.stdout.write(helpText());
|
|
2602
|
+
return ExitCode.ok;
|
|
2603
|
+
}
|
|
2604
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
2605
|
+
process.stdout.write(helpText(command));
|
|
2606
|
+
return ExitCode.ok;
|
|
2607
|
+
}
|
|
2608
|
+
const spec = findCommand(command);
|
|
2609
|
+
if (!spec) throw new CliError(`\uC54C \uC218 \uC5C6\uB294 \uBA85\uB839\uC785\uB2C8\uB2E4: ${command}`, "usage");
|
|
2610
|
+
const parsed = parseArgs(spec, rest);
|
|
2611
|
+
if (command === "completion") {
|
|
2612
|
+
const [shell] = requirePositionals(spec, parsed);
|
|
2613
|
+
if (!["zsh", "bash", "fish"].includes(shell)) {
|
|
2614
|
+
throw new CliError("zsh, bash, fish \uC911 \uD558\uB098\uC5EC\uC57C \uD569\uB2C8\uB2E4.", "usage");
|
|
2615
|
+
}
|
|
2616
|
+
process.stdout.write(completionScript(shell));
|
|
2617
|
+
return ExitCode.ok;
|
|
2618
|
+
}
|
|
2619
|
+
const cliEnv = readCliEnv();
|
|
2620
|
+
const ctx = await createContext({ claudeHome: str(parsed, "claude-home") });
|
|
2621
|
+
const mode = resolveRenderMode({
|
|
2622
|
+
json: bool(parsed, "json"),
|
|
2623
|
+
quiet: bool(parsed, "quiet"),
|
|
2624
|
+
isTTY: cliEnv.isStdoutTTY,
|
|
2625
|
+
ci: cliEnv.ci,
|
|
2626
|
+
term: cliEnv.term
|
|
2627
|
+
});
|
|
2628
|
+
if (mode === "json" && isMutating(command) && !bool(parsed, "yes")) {
|
|
2629
|
+
throw new CliError(
|
|
2630
|
+
"--json\uC73C\uB85C \uC2E4\uD589\uD560 \uB54C\uB294 --yes\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. \uD655\uC778\uC744 \uBB3C\uC744 \uC218 \uC5C6\uAE30 \uB54C\uBB38\uC785\uB2C8\uB2E4.",
|
|
2631
|
+
"usage"
|
|
2632
|
+
);
|
|
2633
|
+
}
|
|
2634
|
+
const reporter = mode === "json" ? new JsonReporter(process.stdout, process.stderr, package_default.version) : mode === "ink" ? (await import("./reporter-4S4IPLQW.js")).createInkReporter() : new PlainReporter();
|
|
2635
|
+
try {
|
|
2636
|
+
return ExitCode[await run(command, ctx, reporter, parsed, mode, cliEnv)];
|
|
2637
|
+
} finally {
|
|
2638
|
+
await reporter.close();
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
function isMutating(command) {
|
|
2642
|
+
return ["mv", "rm", "merge", "import", "rollback"].includes(command);
|
|
2643
|
+
}
|
|
2644
|
+
async function run(command, ctx, reporter, parsed, mode, cliEnv) {
|
|
2645
|
+
const home = homedir2();
|
|
2646
|
+
const spec = findCommand(command);
|
|
2647
|
+
switch (command) {
|
|
2648
|
+
case "mv": {
|
|
2649
|
+
const [src, dst] = requirePositionals(spec, parsed);
|
|
2650
|
+
return await runMove(ctx, reporter, {
|
|
2651
|
+
src,
|
|
2652
|
+
dst,
|
|
2653
|
+
stateOnly: bool(parsed, "state-only"),
|
|
2654
|
+
rewriteProse: bool(parsed, "rewrite-prose"),
|
|
2655
|
+
dryRun: bool(parsed, "dry-run"),
|
|
2656
|
+
yes: bool(parsed, "yes"),
|
|
2657
|
+
force: bool(parsed, "force"),
|
|
2658
|
+
noBackup: bool(parsed, "no-backup"),
|
|
2659
|
+
allowAncestors: bool(parsed, "allow-ancestors"),
|
|
2660
|
+
skipLive: bool(parsed, "skip-live"),
|
|
2661
|
+
home,
|
|
2662
|
+
version: package_default.version
|
|
2663
|
+
});
|
|
2664
|
+
}
|
|
2665
|
+
case "ls":
|
|
2666
|
+
return runList(ctx, reporter, {
|
|
2667
|
+
orphansOnly: bool(parsed, "orphans"),
|
|
2668
|
+
sort: str(parsed, "sort") ?? "activity"
|
|
2669
|
+
});
|
|
2670
|
+
case "info":
|
|
2671
|
+
return runInfo(ctx, reporter, {
|
|
2672
|
+
target: await needTarget(ctx, parsed, mode, "info", cliEnv),
|
|
2673
|
+
home
|
|
2674
|
+
});
|
|
2675
|
+
case "doctor":
|
|
2676
|
+
return runDoctor(ctx, reporter);
|
|
2677
|
+
case "rm":
|
|
2678
|
+
return await runRemove(ctx, reporter, {
|
|
2679
|
+
target: await needTarget(ctx, parsed, mode, "rm", cliEnv),
|
|
2680
|
+
home,
|
|
2681
|
+
version: package_default.version,
|
|
2682
|
+
yes: bool(parsed, "yes"),
|
|
2683
|
+
force: bool(parsed, "force")
|
|
2684
|
+
});
|
|
2685
|
+
case "merge": {
|
|
2686
|
+
const [from, to] = requirePositionals(spec, parsed);
|
|
2687
|
+
return runMerge(ctx, reporter, { from, to, home, yes: bool(parsed, "yes") });
|
|
2688
|
+
}
|
|
2689
|
+
case "backup":
|
|
2690
|
+
return runBackupOnly(ctx, reporter, {
|
|
2691
|
+
target: await needTarget(ctx, parsed, mode, "backup", cliEnv),
|
|
2692
|
+
home,
|
|
2693
|
+
version: package_default.version
|
|
2694
|
+
});
|
|
2695
|
+
case "rollback": {
|
|
2696
|
+
const [id] = parsed.positionals;
|
|
2697
|
+
if (!id) return runBackups(ctx, reporter);
|
|
2698
|
+
return runRollback(ctx, reporter, { id });
|
|
2699
|
+
}
|
|
2700
|
+
case "export":
|
|
2701
|
+
return runExport(ctx, reporter, {
|
|
2702
|
+
target: await needTarget(ctx, parsed, mode, "export", cliEnv),
|
|
2703
|
+
out: str(parsed, "out"),
|
|
2704
|
+
home
|
|
2705
|
+
});
|
|
2706
|
+
case "import": {
|
|
2707
|
+
const [file] = requirePositionals(spec, parsed);
|
|
2708
|
+
const result = runImport(ctx, reporter, { file, to: str(parsed, "to"), home });
|
|
2709
|
+
if (!result.migrateFrom || !result.migrateTo) return result.code;
|
|
2710
|
+
const fresh = await createContext({ claudeHome: str(parsed, "claude-home") });
|
|
2711
|
+
return await runMove(fresh, reporter, {
|
|
2712
|
+
src: result.migrateFrom,
|
|
2713
|
+
dst: result.migrateTo,
|
|
2714
|
+
stateOnly: true,
|
|
2715
|
+
rewriteProse: false,
|
|
2716
|
+
dryRun: false,
|
|
2717
|
+
yes: true,
|
|
2718
|
+
force: false,
|
|
2719
|
+
noBackup: false,
|
|
2720
|
+
allowAncestors: false,
|
|
2721
|
+
skipLive: false,
|
|
2722
|
+
home,
|
|
2723
|
+
version: package_default.version
|
|
2724
|
+
});
|
|
2725
|
+
}
|
|
2726
|
+
default:
|
|
2727
|
+
throw new CliError(`\uC544\uC9C1 \uAD6C\uD604\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4: ${command}`, "internal");
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
async function needTarget(ctx, parsed, mode, command, cliEnv) {
|
|
2731
|
+
const [given] = parsed.positionals;
|
|
2732
|
+
if (given) return given;
|
|
2733
|
+
const picker = resolvePickerMode({
|
|
2734
|
+
override: cliEnv.picker,
|
|
2735
|
+
isTTY: cliEnv.isStdinTTY && mode !== "json",
|
|
2736
|
+
hasFzf: await hasFzf()
|
|
2737
|
+
});
|
|
2738
|
+
if (picker === "none") {
|
|
2739
|
+
throw new CliError(`claude-mv ${command} <\uD504\uB85C\uC81D\uD2B8>\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.`, "usage");
|
|
2740
|
+
}
|
|
2741
|
+
const candidates = projectCandidates(ctx);
|
|
2742
|
+
const picked = picker === "fzf" ? await pickWithFzf(candidates, {
|
|
2743
|
+
prompt: `${command}>`,
|
|
2744
|
+
preview: "claude-mv info {1} 2>/dev/null"
|
|
2745
|
+
}) : null;
|
|
2746
|
+
if (!picked) throw new CliError("\uB300\uC0C1\uC744 \uC120\uD0DD\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.", "usage");
|
|
2747
|
+
return picked;
|
|
2748
|
+
}
|
|
2749
|
+
function projectCandidates(ctx) {
|
|
2750
|
+
return ctx.index.projects.map((p) => ({
|
|
2751
|
+
value: p.primaryCwd ?? p.dirName,
|
|
2752
|
+
description: p.health === "healthy" ? void 0 : p.health
|
|
2753
|
+
}));
|
|
2754
|
+
}
|
|
2755
|
+
async function runComplete(words) {
|
|
2756
|
+
try {
|
|
2757
|
+
const ctx = await createContext();
|
|
2758
|
+
const root = backupRoot(ctx.env.toolDir);
|
|
2759
|
+
process.stdout.write(
|
|
2760
|
+
`${formatCandidates(
|
|
2761
|
+
complete(words, {
|
|
2762
|
+
projects: () => projectCandidates(ctx),
|
|
2763
|
+
backups: () => listBackups(root).map((b) => ({ value: b.backupId, description: b.dst })),
|
|
2764
|
+
directories: (prefix) => directoryCandidates(prefix),
|
|
2765
|
+
bundles: (prefix) => fileCandidates(prefix, ".tgz")
|
|
2766
|
+
})
|
|
2767
|
+
)}
|
|
2768
|
+
`
|
|
2769
|
+
);
|
|
2770
|
+
} catch {
|
|
2771
|
+
}
|
|
2772
|
+
return ExitCode.ok;
|
|
2773
|
+
}
|
|
2774
|
+
function directoryCandidates(prefix) {
|
|
2775
|
+
const base = prefix.endsWith("/") ? prefix : join12(prefix, "..");
|
|
2776
|
+
try {
|
|
2777
|
+
return readdirSync5(base, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => ({ value: join12(base, e.name) }));
|
|
2778
|
+
} catch {
|
|
2779
|
+
return [];
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
function fileCandidates(prefix, suffix) {
|
|
2783
|
+
const base = prefix.includes("/") ? join12(prefix, "..") : ".";
|
|
2784
|
+
try {
|
|
2785
|
+
return readdirSync5(base).filter((name) => name.endsWith(suffix)).map((name) => ({ value: join12(base, name) })).filter((c) => existsSync8(c.value));
|
|
2786
|
+
} catch {
|
|
2787
|
+
return [];
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
main(process.argv.slice(2)).then((code) => process.exit(code)).catch((error) => {
|
|
2791
|
+
if (error instanceof CliError) {
|
|
2792
|
+
process.stderr.write(`${error.message}
|
|
2793
|
+
`);
|
|
2794
|
+
if (error.hint) process.stderr.write(`${error.hint}
|
|
2795
|
+
`);
|
|
2796
|
+
process.exit(ExitCode[error.code]);
|
|
2797
|
+
}
|
|
2798
|
+
process.stderr.write(`${error.stack ?? String(error)}
|
|
2799
|
+
`);
|
|
2800
|
+
process.exit(ExitCode.internal);
|
|
2801
|
+
});
|