i18-fe-automator-beta 2.1.8 → 2.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs/index.js +1217 -7
- package/dist/esm/index.mjs +1217 -7
- package/package.json +2 -1
package/dist/commonjs/index.js
CHANGED
|
@@ -17,6 +17,8 @@ var xlsx = require('node-xlsx');
|
|
|
17
17
|
var crypto = require('crypto');
|
|
18
18
|
var querystring = require('querystring');
|
|
19
19
|
var uuid = require('uuid');
|
|
20
|
+
var os = require('os');
|
|
21
|
+
var AdmZip = require('adm-zip');
|
|
20
22
|
var eslint = require('eslint');
|
|
21
23
|
var espree = require('espree');
|
|
22
24
|
var node_module = require('node:module');
|
|
@@ -26,7 +28,6 @@ require('shelljs');
|
|
|
26
28
|
var process$1 = require('child_process');
|
|
27
29
|
var _ = require('lodash');
|
|
28
30
|
var request$1 = require('request-promise');
|
|
29
|
-
require('os');
|
|
30
31
|
require('console');
|
|
31
32
|
|
|
32
33
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
@@ -1803,7 +1804,7 @@ async function addSassPermission(cliOpts = {}) {
|
|
|
1803
1804
|
|
|
1804
1805
|
// 禅道基础地址/缓存键(联调不符只改这里)
|
|
1805
1806
|
const ZENTAO_BASE = "https://zentao.hongxinshop.com/zentao";
|
|
1806
|
-
const CACHE_KEY = "zentao"; // .cache/zentao: { account, password, zentaosid, products: [{id, name}] }
|
|
1807
|
+
const CACHE_KEY$1 = "zentao"; // .cache/zentao: { account, password, zentaosid, products: [{id, name}] }
|
|
1807
1808
|
|
|
1808
1809
|
// 需求列表展示与pull并发的上限
|
|
1809
1810
|
const PLAN_STORY_TABLE_LIMIT = 50;
|
|
@@ -1832,12 +1833,12 @@ function ensureCacheDir() {
|
|
|
1832
1833
|
// 读取禅道凭据/会话缓存(不存在/损坏返回 {})
|
|
1833
1834
|
function readZentaoCache() {
|
|
1834
1835
|
const cacheFilePath =
|
|
1835
|
-
getRunCliPath({ root: true }) + "/.cache/" + CACHE_KEY;
|
|
1836
|
+
getRunCliPath({ root: true }) + "/.cache/" + CACHE_KEY$1;
|
|
1836
1837
|
if (!isExistPath(cacheFilePath)) {
|
|
1837
1838
|
return {};
|
|
1838
1839
|
}
|
|
1839
1840
|
try {
|
|
1840
|
-
return JSON.parse(readCache(CACHE_KEY).toString("utf8")) || {};
|
|
1841
|
+
return JSON.parse(readCache(CACHE_KEY$1).toString("utf8")) || {};
|
|
1841
1842
|
} catch (error) {
|
|
1842
1843
|
return {};
|
|
1843
1844
|
}
|
|
@@ -1847,7 +1848,7 @@ function readZentaoCache() {
|
|
|
1847
1848
|
function writeZentaoCache(obj) {
|
|
1848
1849
|
ensureCacheDir();
|
|
1849
1850
|
writeCache(
|
|
1850
|
-
CACHE_KEY,
|
|
1851
|
+
CACHE_KEY$1,
|
|
1851
1852
|
JSON.stringify({ ...readZentaoCache(), ...obj })
|
|
1852
1853
|
);
|
|
1853
1854
|
}
|
|
@@ -2116,7 +2117,7 @@ function askZentaoCredentials(cliOpts) {
|
|
|
2116
2117
|
password: cliOpts.password,
|
|
2117
2118
|
},
|
|
2118
2119
|
useCache: Boolean(cliOpts.cache),
|
|
2119
|
-
cacheKey: CACHE_KEY,
|
|
2120
|
+
cacheKey: CACHE_KEY$1,
|
|
2120
2121
|
}).then((secret) => {
|
|
2121
2122
|
// resolveSecretQuestions 会整文件覆盖回写缓存(只剩凭据字段, 发生在登录之前),
|
|
2122
2123
|
// 合并写补回 zentaosid 等字段, 防止登录失败时已缓存的有效会话被抹掉
|
|
@@ -4181,6 +4182,1209 @@ function registerStoryCommand(program, { markHandled, addHelpExample }) {
|
|
|
4181
4182
|
);
|
|
4182
4183
|
}
|
|
4183
4184
|
|
|
4185
|
+
// 坚果云 WebDAV 公共模块(其他功能可直接复用)
|
|
4186
|
+
// 应用密码需网页端 账户信息->安全选项->添加应用密码 生成, 非登录密码
|
|
4187
|
+
const JIANGUOYUN_DAV = "https://dav.jianguoyun.com/dav";
|
|
4188
|
+
const CACHE_KEY = "jianguoyun"; // .cache/jianguoyun: { account, appPassword, remoteDir }
|
|
4189
|
+
|
|
4190
|
+
// 凭据交互问题(默认值会被缓存覆盖, 输入后回写缓存)
|
|
4191
|
+
const JGY_QUESTIONS = [
|
|
4192
|
+
{
|
|
4193
|
+
message: "请输入坚果云账号(邮箱)",
|
|
4194
|
+
name: "account",
|
|
4195
|
+
validate: (v) => (v && v.trim() ? true : "账号不能为空"),
|
|
4196
|
+
},
|
|
4197
|
+
{
|
|
4198
|
+
message: "请输入坚果云应用密码(网页端: 账户信息->安全选项->添加应用密码)",
|
|
4199
|
+
name: "appPassword",
|
|
4200
|
+
type: "password",
|
|
4201
|
+
validate: (v) => (v && v.trim() ? true : "应用密码不能为空"),
|
|
4202
|
+
},
|
|
4203
|
+
{
|
|
4204
|
+
message: "请输入远程目录(坚果云里的文件夹名)",
|
|
4205
|
+
name: "remoteDir",
|
|
4206
|
+
default: "claude-migrate",
|
|
4207
|
+
},
|
|
4208
|
+
];
|
|
4209
|
+
|
|
4210
|
+
/**
|
|
4211
|
+
* 解析坚果云凭据: CLI参数 > --cache缓存 > 交互(展示缓存默认值), 解析后回写缓存
|
|
4212
|
+
* @param cliValues CLI 参数值 { account, appPassword, remoteDir }
|
|
4213
|
+
* @param useCache 是否静默走缓存(缓存缺失的字段回退交互)
|
|
4214
|
+
*/
|
|
4215
|
+
function resolveJianguoyunSecret(cliValues = {}, useCache = false) {
|
|
4216
|
+
return resolveSecretQuestions({
|
|
4217
|
+
questions: JGY_QUESTIONS,
|
|
4218
|
+
cliValues,
|
|
4219
|
+
useCache,
|
|
4220
|
+
cacheKey: CACHE_KEY,
|
|
4221
|
+
});
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
function basicAuth(account, appPassword) {
|
|
4225
|
+
return "Basic " + Buffer.from(`${account}:${appPassword}`).toString("base64");
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
// 规范化远程目录: 去首尾空白与斜杠
|
|
4229
|
+
function normDir(remoteDir) {
|
|
4230
|
+
return String(remoteDir || "").trim().replace(/^\/+|\/+$/g, "");
|
|
4231
|
+
}
|
|
4232
|
+
|
|
4233
|
+
function remoteUrl(secret, fileName = "") {
|
|
4234
|
+
const parts = [JIANGUOYUN_DAV, normDir(secret.remoteDir)];
|
|
4235
|
+
if (fileName) parts.push(...String(fileName).split("/").map(encodeURIComponent));
|
|
4236
|
+
return parts.filter(Boolean).join("/");
|
|
4237
|
+
}
|
|
4238
|
+
|
|
4239
|
+
// 从 WebDAV 错误响应体提取可读信息(坚果云错误是 XML, 含 <s:message>)
|
|
4240
|
+
async function extractErrorMessage(res) {
|
|
4241
|
+
if (res.status === 401) return "账号或应用密码错误(应用密码需网页端生成, 非登录密码)";
|
|
4242
|
+
let text = "";
|
|
4243
|
+
try {
|
|
4244
|
+
text = await res.text();
|
|
4245
|
+
} catch {
|
|
4246
|
+
/* 忽略 */
|
|
4247
|
+
}
|
|
4248
|
+
const msg = text.match(/<(?:[\w-]+:)?message>([\s\S]*?)<\/(?:[\w-]+:)?message>/i)?.[1]?.trim();
|
|
4249
|
+
return msg || `HTTP ${res.status}`;
|
|
4250
|
+
}
|
|
4251
|
+
|
|
4252
|
+
// 确保远程目录存在(逐级 MKCOL, 已存在返回 405 忽略)
|
|
4253
|
+
async function ensureRemoteDir(secret) {
|
|
4254
|
+
const dir = normDir(secret.remoteDir);
|
|
4255
|
+
if (!dir) return;
|
|
4256
|
+
const auth = basicAuth(secret.account, secret.appPassword);
|
|
4257
|
+
let current = JIANGUOYUN_DAV;
|
|
4258
|
+
for (const seg of dir.split("/")) {
|
|
4259
|
+
current += "/" + encodeURIComponent(seg);
|
|
4260
|
+
const res = await fetch(current, { method: "MKCOL", headers: { Authorization: auth } });
|
|
4261
|
+
// 201=创建成功 405=已存在; 其他视为失败
|
|
4262
|
+
if (res.status !== 201 && res.status !== 405) {
|
|
4263
|
+
throw new Error(`创建远程目录失败: ${await extractErrorMessage(res)}`);
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
|
|
4268
|
+
/**
|
|
4269
|
+
* 上传文件到坚果云
|
|
4270
|
+
* @param secret resolveJianguoyunSecret 的结果
|
|
4271
|
+
* @param localFile 本地文件路径
|
|
4272
|
+
* @param fileName 远程文件名(默认取 localFile 的 basename)
|
|
4273
|
+
*/
|
|
4274
|
+
async function jianguoyunUpload(secret, localFile, fileName) {
|
|
4275
|
+
await ensureRemoteDir(secret);
|
|
4276
|
+
const name = fileName || localFile.split(/[\\/]/).pop();
|
|
4277
|
+
const res = await fetch(remoteUrl(secret, name), {
|
|
4278
|
+
method: "PUT",
|
|
4279
|
+
headers: { Authorization: basicAuth(secret.account, secret.appPassword) },
|
|
4280
|
+
body: fs.readFileSync(localFile),
|
|
4281
|
+
});
|
|
4282
|
+
if (!res.ok) {
|
|
4283
|
+
throw new Error(`上传坚果云失败: ${await extractErrorMessage(res)}`);
|
|
4284
|
+
}
|
|
4285
|
+
return `${normDir(secret.remoteDir)}/${name}`;
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4288
|
+
// 解析 PROPFIND 响应, 返回目录下文件列表(跳过目录本身)
|
|
4289
|
+
function parsePropfind(xml) {
|
|
4290
|
+
const files = [];
|
|
4291
|
+
const respRe = /<(?:[\w-]+:)?response>([\s\S]*?)<\/(?:[\w-]+:)?response>/g;
|
|
4292
|
+
let m;
|
|
4293
|
+
while ((m = respRe.exec(xml))) {
|
|
4294
|
+
const block = m[1];
|
|
4295
|
+
const href =
|
|
4296
|
+
block.match(/<(?:[\w-]+:)?href>([\s\S]*?)<\/(?:[\w-]+:)?href>/)?.[1] || "";
|
|
4297
|
+
if (!href || href.endsWith("/")) continue; // 目录本身
|
|
4298
|
+
const name = decodeURIComponent(href.split("/").filter(Boolean).pop());
|
|
4299
|
+
const size = Number(
|
|
4300
|
+
block.match(/<(?:[\w-]+:)?getcontentlength>([\s\S]*?)</)?.[1] || 0
|
|
4301
|
+
);
|
|
4302
|
+
const mtime =
|
|
4303
|
+
block.match(/<(?:[\w-]+:)?getlastmodified>([\s\S]*?)</)?.[1] || "";
|
|
4304
|
+
files.push({ name, size, mtime });
|
|
4305
|
+
}
|
|
4306
|
+
return files;
|
|
4307
|
+
}
|
|
4308
|
+
|
|
4309
|
+
/**
|
|
4310
|
+
* 列出坚果云远程目录下的文件(目录不存在返回 [])
|
|
4311
|
+
* @returns [{ name, size, mtime }]
|
|
4312
|
+
*/
|
|
4313
|
+
async function jianguoyunList(secret) {
|
|
4314
|
+
const dir = normDir(secret.remoteDir);
|
|
4315
|
+
if (!dir) return [];
|
|
4316
|
+
const res = await fetch(remoteUrl(secret), {
|
|
4317
|
+
method: "PROPFIND",
|
|
4318
|
+
headers: {
|
|
4319
|
+
Authorization: basicAuth(secret.account, secret.appPassword),
|
|
4320
|
+
Depth: "1",
|
|
4321
|
+
},
|
|
4322
|
+
});
|
|
4323
|
+
if (res.status === 404) return []; // 目录不存在
|
|
4324
|
+
if (!res.ok) {
|
|
4325
|
+
throw new Error(`读取坚果云目录失败: ${await extractErrorMessage(res)}`);
|
|
4326
|
+
}
|
|
4327
|
+
return parsePropfind(await res.text());
|
|
4328
|
+
}
|
|
4329
|
+
|
|
4330
|
+
/**
|
|
4331
|
+
* 从坚果云下载文件
|
|
4332
|
+
* @param localPath 下载保存的本地路径
|
|
4333
|
+
*/
|
|
4334
|
+
async function jianguoyunDownload(secret, fileName, localPath) {
|
|
4335
|
+
const res = await fetch(remoteUrl(secret, fileName), {
|
|
4336
|
+
headers: { Authorization: basicAuth(secret.account, secret.appPassword) },
|
|
4337
|
+
});
|
|
4338
|
+
if (!res.ok) {
|
|
4339
|
+
throw new Error(`下载坚果云文件失败: ${await extractErrorMessage(res)}`);
|
|
4340
|
+
}
|
|
4341
|
+
fs.writeFileSync(localPath, Buffer.from(await res.arrayBuffer()));
|
|
4342
|
+
}
|
|
4343
|
+
|
|
4344
|
+
// claude 子命令 — Claude Code 数据跨平台迁移(list/export/import)
|
|
4345
|
+
// 参照 claude-migrate.sh 重写为 Node 版: zip 归档 + 跨系统路径重映射(POSIX <-> Windows)
|
|
4346
|
+
const MIGRATE_VERSION = "1.0.0";
|
|
4347
|
+
const ALL_CATEGORIES =
|
|
4348
|
+
"settings,history,plans,projects,plugins,skills,memory";
|
|
4349
|
+
const CATEGORY_LIST = ALL_CATEGORIES.split(",");
|
|
4350
|
+
|
|
4351
|
+
// 类别 -> 顶层文件(相对 ~/.claude)
|
|
4352
|
+
const CATEGORY_FILES = {
|
|
4353
|
+
settings: ["settings.json", "settings.local.json", "keybindings.json"],
|
|
4354
|
+
history: ["history.jsonl"],
|
|
4355
|
+
};
|
|
4356
|
+
// 类别 -> 整目录拷贝(相对 ~/.claude)
|
|
4357
|
+
const CATEGORY_DIRS = {
|
|
4358
|
+
plans: "plans",
|
|
4359
|
+
projects: "projects",
|
|
4360
|
+
skills: "skills",
|
|
4361
|
+
};
|
|
4362
|
+
// export 交互勾选时默认不勾选的类别(history/projects 体积大且含大量本机路径, 按需勾选)
|
|
4363
|
+
const UNCHECKED_BY_DEFAULT = ["history", "projects"];
|
|
4364
|
+
// plugins 只迁 2 个清单(插件本体可重装, 不迁)
|
|
4365
|
+
const PLUGIN_FILES = ["installed_plugins.json", "known_marketplaces.json"];
|
|
4366
|
+
// memory = memory/ 目录 + 全局记忆文件
|
|
4367
|
+
const MEMORY_DIR = "memory";
|
|
4368
|
+
const MEMORY_FILES = ["CLAUDE.md", "AGENTS.md"];
|
|
4369
|
+
|
|
4370
|
+
// ─── 工具函数 ─────────────────────────────────────────────────────────
|
|
4371
|
+
|
|
4372
|
+
function log(msg) {
|
|
4373
|
+
console.log(`${chalk.green("=>")} ${msg}`);
|
|
4374
|
+
}
|
|
4375
|
+
function warn(msg) {
|
|
4376
|
+
console.log(chalk.yellow(`警告: ${msg}`));
|
|
4377
|
+
}
|
|
4378
|
+
function verbose(msg, enabled) {
|
|
4379
|
+
if (enabled) console.log(` ${chalk.blue("+")} ${msg}`);
|
|
4380
|
+
}
|
|
4381
|
+
function die(msg) {
|
|
4382
|
+
console.error(chalk.red(`错误: ${msg}`));
|
|
4383
|
+
process.exit(1);
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
4387
|
+
|
|
4388
|
+
// 时间戳 YYYY-MM-DD-HHmmss(年月日时分秒)
|
|
4389
|
+
function getTimestamp() {
|
|
4390
|
+
const d = new Date();
|
|
4391
|
+
return (
|
|
4392
|
+
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
|
|
4393
|
+
`-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`
|
|
4394
|
+
);
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
// 递归收集目录下所有文件(绝对路径)
|
|
4398
|
+
function walkFiles(dir) {
|
|
4399
|
+
const out = [];
|
|
4400
|
+
(function rec(d) {
|
|
4401
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
4402
|
+
if (e.isDirectory()) rec(path.join(d, e.name));
|
|
4403
|
+
else out.push(path.join(d, e.name));
|
|
4404
|
+
}
|
|
4405
|
+
})(dir);
|
|
4406
|
+
return out;
|
|
4407
|
+
}
|
|
4408
|
+
|
|
4409
|
+
function countFiles(dir) {
|
|
4410
|
+
if (!fs.existsSync(dir)) return 0;
|
|
4411
|
+
return walkFiles(dir).length;
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4414
|
+
// ─── 路径编码与重映射 ──────────────────────────────────────────────────
|
|
4415
|
+
|
|
4416
|
+
/**
|
|
4417
|
+
* 跨平台路径编码(Claude Code projects 目录名规则):
|
|
4418
|
+
* / \ : 均替换为 -(统一按正斜杠处理, POSIX 首斜杠替换后自然带前导横杠)
|
|
4419
|
+
* /Users/harry -> -Users-harry
|
|
4420
|
+
* D:\Users\harry -> D--Users-harry
|
|
4421
|
+
*/
|
|
4422
|
+
function encodePath(p) {
|
|
4423
|
+
const norm = String(p).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
4424
|
+
return norm.replace(/[:/]/g, "-");
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4427
|
+
// projects 子目录名: 把编码后的源 home 前缀替换为目标 home 前缀
|
|
4428
|
+
function remapDirname(name, srcHome, dstHome) {
|
|
4429
|
+
const src = encodePath(srcHome);
|
|
4430
|
+
const dst = encodePath(dstHome);
|
|
4431
|
+
if (src === dst || !name.startsWith(src)) return name;
|
|
4432
|
+
return dst + name.slice(src.length);
|
|
4433
|
+
}
|
|
4434
|
+
|
|
4435
|
+
/**
|
|
4436
|
+
* 文件内容路径重映射, 覆盖 3 种变体(长变体优先, 避免单分隔符替换破坏双转义):
|
|
4437
|
+
* 1. JSON 双反斜杠转义: D:\\Users\\harry
|
|
4438
|
+
* 2. 反斜杠分隔: D:\Users\harry 或 \Users\harry
|
|
4439
|
+
* 3. 正斜杠分隔: /Users/harry 或 D:/Users/harry
|
|
4440
|
+
*/
|
|
4441
|
+
function remapContent(content, srcHome, dstHome) {
|
|
4442
|
+
if (!srcHome || !dstHome || srcHome === dstHome) return content;
|
|
4443
|
+
const srcPosix = srcHome.replace(/\\/g, "/");
|
|
4444
|
+
const dstPosix = dstHome.replace(/\\/g, "/");
|
|
4445
|
+
const srcWin = srcPosix.replace(/\//g, "\\");
|
|
4446
|
+
const dstWin = dstPosix.replace(/\//g, "\\");
|
|
4447
|
+
const pairs = [
|
|
4448
|
+
[srcWin.replace(/\\/g, "\\\\"), dstWin.replace(/\\/g, "\\\\")],
|
|
4449
|
+
[srcWin, dstWin],
|
|
4450
|
+
[srcPosix, dstPosix],
|
|
4451
|
+
];
|
|
4452
|
+
for (const [from, to] of pairs) {
|
|
4453
|
+
if (from && from !== to) content = content.split(from).join(to);
|
|
4454
|
+
}
|
|
4455
|
+
return content;
|
|
4456
|
+
}
|
|
4457
|
+
|
|
4458
|
+
function remapFileContents(file, srcHome, dstHome) {
|
|
4459
|
+
if (srcHome === dstHome) return;
|
|
4460
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
4461
|
+
const newContent = remapContent(content, srcHome, dstHome);
|
|
4462
|
+
if (newContent !== content) {
|
|
4463
|
+
fs.writeFileSync(file, newContent, "utf-8");
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
|
|
4467
|
+
// ─── claude 目录解析 ───────────────────────────────────────────────────
|
|
4468
|
+
|
|
4469
|
+
function detectClaudeDir() {
|
|
4470
|
+
return (
|
|
4471
|
+
process.env.CLAUDE_DIR || path.join(os.homedir(), ".claude")
|
|
4472
|
+
);
|
|
4473
|
+
}
|
|
4474
|
+
|
|
4475
|
+
// 读操作(list/export)用: --claude-dir > 环境变量 > 默认检测; 不存在则交互输入
|
|
4476
|
+
async function resolveClaudeDirForRead(options) {
|
|
4477
|
+
if (options.claudeDir) {
|
|
4478
|
+
const p = path.resolve(options.claudeDir);
|
|
4479
|
+
if (!fs.existsSync(p)) die(`claude 目录不存在: ${p}`);
|
|
4480
|
+
return p;
|
|
4481
|
+
}
|
|
4482
|
+
const detected = detectClaudeDir();
|
|
4483
|
+
if (fs.existsSync(detected)) return detected;
|
|
4484
|
+
warn(`未检测到 claude 目录: ${detected}`);
|
|
4485
|
+
const { custom } = await inquirer.prompt([
|
|
4486
|
+
{
|
|
4487
|
+
type: "input",
|
|
4488
|
+
name: "custom",
|
|
4489
|
+
message: "请输入 claude 数据目录路径",
|
|
4490
|
+
validate: (v) => {
|
|
4491
|
+
if (!v.trim()) return "路径不能为空";
|
|
4492
|
+
if (!fs.existsSync(v.trim())) return "目录不存在, 请重新输入";
|
|
4493
|
+
return true;
|
|
4494
|
+
},
|
|
4495
|
+
},
|
|
4496
|
+
]);
|
|
4497
|
+
return path.resolve(custom.trim());
|
|
4498
|
+
}
|
|
4499
|
+
|
|
4500
|
+
// import 用: --claude-dir 传了直接用; 否则展示检测路径让用户确认或自定义
|
|
4501
|
+
async function resolveClaudeDirForImport(options) {
|
|
4502
|
+
if (options.claudeDir) return path.resolve(options.claudeDir);
|
|
4503
|
+
const detected = detectClaudeDir();
|
|
4504
|
+
const exists = fs.existsSync(detected);
|
|
4505
|
+
const { choice } = await inquirer.prompt([
|
|
4506
|
+
{
|
|
4507
|
+
type: "list",
|
|
4508
|
+
name: "choice",
|
|
4509
|
+
message: "请确认目标 claude 数据目录",
|
|
4510
|
+
choices: [
|
|
4511
|
+
{
|
|
4512
|
+
name: `使用检测到的路径: ${detected}${exists ? "" : chalk.yellow(" (目录不存在, 导入时会创建)")}`,
|
|
4513
|
+
value: detected,
|
|
4514
|
+
},
|
|
4515
|
+
{ name: "手动输入自定义路径", value: null },
|
|
4516
|
+
],
|
|
4517
|
+
},
|
|
4518
|
+
]);
|
|
4519
|
+
if (choice) return choice;
|
|
4520
|
+
const { custom } = await inquirer.prompt([
|
|
4521
|
+
{
|
|
4522
|
+
type: "input",
|
|
4523
|
+
name: "custom",
|
|
4524
|
+
message: "请输入 claude 数据目录路径",
|
|
4525
|
+
validate: (v) => (v.trim() ? true : "路径不能为空"),
|
|
4526
|
+
},
|
|
4527
|
+
]);
|
|
4528
|
+
return path.resolve(custom.trim());
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4531
|
+
// ─── 类别收集(export 暂存) ─────────────────────────────────────────────
|
|
4532
|
+
|
|
4533
|
+
function collectCategory(claudeDir, dataDir, cat, isVerbose) {
|
|
4534
|
+
if (CATEGORY_FILES[cat]) {
|
|
4535
|
+
for (const f of CATEGORY_FILES[cat]) {
|
|
4536
|
+
const src = path.join(claudeDir, f);
|
|
4537
|
+
if (fs.existsSync(src)) {
|
|
4538
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
4539
|
+
fs.copyFileSync(src, path.join(dataDir, f));
|
|
4540
|
+
verbose(f, isVerbose);
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4543
|
+
} else if (CATEGORY_DIRS[cat]) {
|
|
4544
|
+
const dir = CATEGORY_DIRS[cat];
|
|
4545
|
+
const src = path.join(claudeDir, dir);
|
|
4546
|
+
if (fs.existsSync(src)) {
|
|
4547
|
+
const dst = path.join(dataDir, cat);
|
|
4548
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
4549
|
+
fs.cpSync(src, dst, { recursive: true });
|
|
4550
|
+
verbose(`${cat}/ (${countFiles(src)} 个文件)`, isVerbose);
|
|
4551
|
+
}
|
|
4552
|
+
} else if (cat === "plugins") {
|
|
4553
|
+
const src = path.join(claudeDir, "plugins");
|
|
4554
|
+
if (fs.existsSync(src)) {
|
|
4555
|
+
fs.mkdirSync(path.join(dataDir, "plugins"), { recursive: true });
|
|
4556
|
+
for (const f of PLUGIN_FILES) {
|
|
4557
|
+
if (fs.existsSync(path.join(src, f))) {
|
|
4558
|
+
fs.copyFileSync(path.join(src, f), path.join(dataDir, "plugins", f));
|
|
4559
|
+
verbose(`plugins/${f}`, isVerbose);
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
} else if (cat === "memory") {
|
|
4564
|
+
const memSrc = path.join(claudeDir, MEMORY_DIR);
|
|
4565
|
+
if (fs.existsSync(memSrc)) {
|
|
4566
|
+
fs.mkdirSync(path.join(dataDir, MEMORY_DIR), { recursive: true });
|
|
4567
|
+
fs.cpSync(memSrc, path.join(dataDir, MEMORY_DIR), { recursive: true });
|
|
4568
|
+
verbose(`memory/ (${countFiles(memSrc)} 个文件)`, isVerbose);
|
|
4569
|
+
}
|
|
4570
|
+
for (const f of MEMORY_FILES) {
|
|
4571
|
+
const src = path.join(claudeDir, f);
|
|
4572
|
+
if (fs.existsSync(src)) {
|
|
4573
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
4574
|
+
fs.copyFileSync(src, path.join(dataDir, f));
|
|
4575
|
+
verbose(f, isVerbose);
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4580
|
+
|
|
4581
|
+
// ─── 本地类别统计(list) ────────────────────────────────────────────────
|
|
4582
|
+
|
|
4583
|
+
function scanCategoryLocal(claudeDir, cat) {
|
|
4584
|
+
const exists = (p) => fs.existsSync(path.join(claudeDir, p));
|
|
4585
|
+
switch (cat) {
|
|
4586
|
+
case "settings": {
|
|
4587
|
+
const n = CATEGORY_FILES.settings.filter(exists).length;
|
|
4588
|
+
return n ? `${n} 个文件` : "未找到";
|
|
4589
|
+
}
|
|
4590
|
+
case "history": {
|
|
4591
|
+
const p = path.join(claudeDir, "history.jsonl");
|
|
4592
|
+
if (!fs.existsSync(p)) return "未找到";
|
|
4593
|
+
const lines = fs
|
|
4594
|
+
.readFileSync(p, "utf-8")
|
|
4595
|
+
.split("\n")
|
|
4596
|
+
.filter((l) => l.trim()).length;
|
|
4597
|
+
return `${lines} 条`;
|
|
4598
|
+
}
|
|
4599
|
+
case "plans": {
|
|
4600
|
+
const p = path.join(claudeDir, CATEGORY_DIRS[cat]);
|
|
4601
|
+
if (!fs.existsSync(p)) return "未找到";
|
|
4602
|
+
return `${countFiles(p)} 个文件`;
|
|
4603
|
+
}
|
|
4604
|
+
case "projects":
|
|
4605
|
+
case "skills": {
|
|
4606
|
+
const p = path.join(claudeDir, CATEGORY_DIRS[cat]);
|
|
4607
|
+
if (!fs.existsSync(p)) return "未找到";
|
|
4608
|
+
const dirs = fs
|
|
4609
|
+
.readdirSync(p, { withFileTypes: true })
|
|
4610
|
+
.filter((e) => e.isDirectory());
|
|
4611
|
+
const label = cat === "skills" ? "个 skill" : "个项目";
|
|
4612
|
+
return `${dirs.length} ${label}, ${countFiles(p)} 个文件`;
|
|
4613
|
+
}
|
|
4614
|
+
case "plugins": {
|
|
4615
|
+
const p = path.join(claudeDir, "plugins");
|
|
4616
|
+
if (!fs.existsSync(p)) return "未找到";
|
|
4617
|
+
const n = PLUGIN_FILES.filter((f) =>
|
|
4618
|
+
fs.existsSync(path.join(p, f))
|
|
4619
|
+
).length;
|
|
4620
|
+
return n ? `${n} 个文件` : "未找到";
|
|
4621
|
+
}
|
|
4622
|
+
case "memory": {
|
|
4623
|
+
let n = MEMORY_FILES.filter(exists).length;
|
|
4624
|
+
const memDir = path.join(claudeDir, MEMORY_DIR);
|
|
4625
|
+
if (fs.existsSync(memDir)) n += countFiles(memDir);
|
|
4626
|
+
return n ? `${n} 个文件` : "未找到";
|
|
4627
|
+
}
|
|
4628
|
+
default:
|
|
4629
|
+
return "未找到";
|
|
4630
|
+
}
|
|
4631
|
+
}
|
|
4632
|
+
|
|
4633
|
+
function printCategoryTable(claudeDir, scanFn) {
|
|
4634
|
+
console.log("");
|
|
4635
|
+
for (const cat of CATEGORY_LIST) {
|
|
4636
|
+
const info = scanFn(claudeDir, cat);
|
|
4637
|
+
const isFound = info !== "未找到" && !info.startsWith("0 ");
|
|
4638
|
+
const colored = isFound
|
|
4639
|
+
? chalk.green(cat.padEnd(10))
|
|
4640
|
+
: chalk.yellow(cat.padEnd(10));
|
|
4641
|
+
console.log(` ${colored} ${info}`);
|
|
4642
|
+
}
|
|
4643
|
+
}
|
|
4644
|
+
|
|
4645
|
+
function printDirDetails(dir, title, unit) {
|
|
4646
|
+
if (!fs.existsSync(dir)) return;
|
|
4647
|
+
const dirs = fs
|
|
4648
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
4649
|
+
.filter((e) => e.isDirectory());
|
|
4650
|
+
if (!dirs.length) return;
|
|
4651
|
+
console.log("");
|
|
4652
|
+
console.log(chalk.bold(`${title}:`));
|
|
4653
|
+
for (const d of dirs) {
|
|
4654
|
+
const n = countFiles(path.join(dir, d.name));
|
|
4655
|
+
console.log(` ${d.name} (${n} ${unit})`);
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
4658
|
+
|
|
4659
|
+
function cmdListLocal(options) {
|
|
4660
|
+
return resolveClaudeDirForRead(options).then((claudeDir) => {
|
|
4661
|
+
console.log(chalk.bold(`本地 Claude 数据:`), claudeDir);
|
|
4662
|
+
printCategoryTable(claudeDir, scanCategoryLocal);
|
|
4663
|
+
printDirDetails(path.join(claudeDir, "projects"), "Projects", "个文件");
|
|
4664
|
+
printDirDetails(path.join(claudeDir, "skills"), "Skills", "个文件");
|
|
4665
|
+
});
|
|
4666
|
+
}
|
|
4667
|
+
|
|
4668
|
+
// ─── 归档类别统计(list 归档) ───────────────────────────────────────────
|
|
4669
|
+
|
|
4670
|
+
// adm-zip 兼容: 旧版 isDirectory() 是方法, 新版是 boolean 属性
|
|
4671
|
+
function entryIsDirectory(e) {
|
|
4672
|
+
return typeof e.isDirectory === "function"
|
|
4673
|
+
? e.isDirectory()
|
|
4674
|
+
: e.isDirectory === true;
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4677
|
+
// 解析归档: 返回 { zip, root, manifest, relMap }(relMap: 相对 root 的路径 -> entry)
|
|
4678
|
+
function readArchive(archive) {
|
|
4679
|
+
if (!fs.existsSync(archive)) die(`归档不存在: ${archive}`);
|
|
4680
|
+
let zip;
|
|
4681
|
+
try {
|
|
4682
|
+
zip = new AdmZip(archive);
|
|
4683
|
+
} catch (error) {
|
|
4684
|
+
die(`读取归档失败: ${error.message}`);
|
|
4685
|
+
}
|
|
4686
|
+
const entries = zip.getEntries();
|
|
4687
|
+
const manifestEntry = entries.find(
|
|
4688
|
+
(e) => !entryIsDirectory(e) && e.entryName.split("/").pop() === "manifest.json"
|
|
4689
|
+
);
|
|
4690
|
+
if (!manifestEntry) die("归档中未找到 manifest.json");
|
|
4691
|
+
const root = manifestEntry.entryName.includes("/")
|
|
4692
|
+
? manifestEntry.entryName.slice(
|
|
4693
|
+
0,
|
|
4694
|
+
manifestEntry.entryName.lastIndexOf("/")
|
|
4695
|
+
)
|
|
4696
|
+
: "";
|
|
4697
|
+
let manifest;
|
|
4698
|
+
try {
|
|
4699
|
+
manifest = JSON.parse(zip.readAsText(manifestEntry.entryName));
|
|
4700
|
+
} catch (error) {
|
|
4701
|
+
die("manifest.json 解析失败");
|
|
4702
|
+
}
|
|
4703
|
+
const relMap = new Map();
|
|
4704
|
+
for (const e of entries) {
|
|
4705
|
+
if (entryIsDirectory(e)) continue;
|
|
4706
|
+
const rel = root ? e.entryName.slice(root.length + 1) : e.entryName;
|
|
4707
|
+
relMap.set(rel, e);
|
|
4708
|
+
}
|
|
4709
|
+
return { zip, root, manifest, relMap };
|
|
4710
|
+
}
|
|
4711
|
+
|
|
4712
|
+
function scanCategoryArchive(relMap, zip, cat) {
|
|
4713
|
+
const has = (rel) => relMap.has(rel);
|
|
4714
|
+
switch (cat) {
|
|
4715
|
+
case "settings": {
|
|
4716
|
+
const n = CATEGORY_FILES.settings.filter((f) => has(`data/${f}`)).length;
|
|
4717
|
+
return n ? `${n} 个文件` : "未找到";
|
|
4718
|
+
}
|
|
4719
|
+
case "history": {
|
|
4720
|
+
const e = relMap.get("data/history.jsonl");
|
|
4721
|
+
if (!e) return "未找到";
|
|
4722
|
+
const lines = zip
|
|
4723
|
+
.readAsText(e.entryName)
|
|
4724
|
+
.split("\n")
|
|
4725
|
+
.filter((l) => l.trim()).length;
|
|
4726
|
+
return `${lines} 条`;
|
|
4727
|
+
}
|
|
4728
|
+
case "plans":
|
|
4729
|
+
case "skills": {
|
|
4730
|
+
const prefix = `data/${cat}/`;
|
|
4731
|
+
const n = [...relMap.keys()].filter((rel) =>
|
|
4732
|
+
rel.startsWith(prefix)
|
|
4733
|
+
).length;
|
|
4734
|
+
return n ? `${n} 个文件` : "未找到";
|
|
4735
|
+
}
|
|
4736
|
+
case "projects": {
|
|
4737
|
+
const prefix = "data/projects/";
|
|
4738
|
+
const rels = [...relMap.keys()].filter((rel) =>
|
|
4739
|
+
rel.startsWith(prefix)
|
|
4740
|
+
);
|
|
4741
|
+
if (!rels.length) return "未找到";
|
|
4742
|
+
const dirs = new Set(rels.map((rel) => rel.slice(prefix.length).split("/")[0]));
|
|
4743
|
+
return `${dirs.size} 个项目, ${rels.length} 个文件`;
|
|
4744
|
+
}
|
|
4745
|
+
case "plugins": {
|
|
4746
|
+
const n = PLUGIN_FILES.filter((f) => has(`data/plugins/${f}`)).length;
|
|
4747
|
+
return n ? `${n} 个文件` : "未找到";
|
|
4748
|
+
}
|
|
4749
|
+
case "memory": {
|
|
4750
|
+
let n = MEMORY_FILES.filter((f) => has(`data/${f}`)).length;
|
|
4751
|
+
n += [...relMap.keys()].filter((rel) =>
|
|
4752
|
+
rel.startsWith(`data/${MEMORY_DIR}/`)
|
|
4753
|
+
).length;
|
|
4754
|
+
return n ? `${n} 个文件` : "未找到";
|
|
4755
|
+
}
|
|
4756
|
+
default:
|
|
4757
|
+
return "未找到";
|
|
4758
|
+
}
|
|
4759
|
+
}
|
|
4760
|
+
|
|
4761
|
+
function cmdListArchive(archive) {
|
|
4762
|
+
const { zip, manifest, relMap } = readArchive(archive);
|
|
4763
|
+
const platformLabel =
|
|
4764
|
+
manifest.platform ? ` [${manifest.platform}]` : "";
|
|
4765
|
+
console.log(chalk.bold(`归档:`), archive);
|
|
4766
|
+
console.log(chalk.bold(`来源:`), `${manifest.hostname || "unknown"} (${manifest.source_home})${platformLabel}`);
|
|
4767
|
+
console.log(chalk.bold(`日期:`), manifest.date || "unknown");
|
|
4768
|
+
printCategoryTable(relMap, (map, cat) =>
|
|
4769
|
+
scanCategoryArchive(map, zip, cat)
|
|
4770
|
+
);
|
|
4771
|
+
// projects / skills 一级列表
|
|
4772
|
+
for (const [cat, title] of [
|
|
4773
|
+
["projects", "Projects"],
|
|
4774
|
+
["skills", "Skills"],
|
|
4775
|
+
]) {
|
|
4776
|
+
const prefix = `data/${cat}/`;
|
|
4777
|
+
const names = new Set(
|
|
4778
|
+
[...relMap.keys()]
|
|
4779
|
+
.filter((rel) => rel.startsWith(prefix))
|
|
4780
|
+
.map((rel) => rel.slice(prefix.length).split("/")[0])
|
|
4781
|
+
);
|
|
4782
|
+
if (!names.size) continue;
|
|
4783
|
+
console.log("");
|
|
4784
|
+
console.log(chalk.bold(`${title}:`));
|
|
4785
|
+
for (const name of names) {
|
|
4786
|
+
const n = [...relMap.keys()].filter(
|
|
4787
|
+
(rel) => rel.startsWith(prefix + name + "/")
|
|
4788
|
+
).length;
|
|
4789
|
+
console.log(` ${name} (${n} 个文件)`);
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
}
|
|
4793
|
+
|
|
4794
|
+
// ─── export ───────────────────────────────────────────────────────────
|
|
4795
|
+
|
|
4796
|
+
function parseCategories(str) {
|
|
4797
|
+
const cats = String(str || ALL_CATEGORIES)
|
|
4798
|
+
.split(",")
|
|
4799
|
+
.map((s) => s.trim())
|
|
4800
|
+
.filter(Boolean);
|
|
4801
|
+
for (const c of cats) {
|
|
4802
|
+
if (!CATEGORY_LIST.includes(c)) {
|
|
4803
|
+
die(`未知类别: ${c} (可选: ${ALL_CATEGORIES})`);
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
4806
|
+
return cats.length ? cats : CATEGORY_LIST;
|
|
4807
|
+
}
|
|
4808
|
+
|
|
4809
|
+
// 类别中文说明(勾选列表展示用)
|
|
4810
|
+
const CATEGORY_LABELS = {
|
|
4811
|
+
settings: "配置(settings.json等)",
|
|
4812
|
+
history: "输入历史(history.jsonl, 体积大)",
|
|
4813
|
+
plans: "计划(plans/)",
|
|
4814
|
+
projects: "项目会话记录(projects/, 体积大)",
|
|
4815
|
+
plugins: "插件清单(installed_plugins等)",
|
|
4816
|
+
skills: "技能(skills/)",
|
|
4817
|
+
memory: "全局记忆(memory/ + CLAUDE.md)",
|
|
4818
|
+
};
|
|
4819
|
+
|
|
4820
|
+
/**
|
|
4821
|
+
* export 交互勾选导出类别(「全选」与子项父子联动, 复用 CheckboxAllPrompt)
|
|
4822
|
+
* history/projects 默认不勾选(体积大且含大量本机路径), 其余默认勾选
|
|
4823
|
+
*/
|
|
4824
|
+
async function selectExportCategories(claudeDir) {
|
|
4825
|
+
// prompt 注册幂等, 与 story.js/upload.js 各自使用前注册
|
|
4826
|
+
inquirer.registerPrompt("checkbox-all", CheckboxAllPrompt);
|
|
4827
|
+
const choices = [
|
|
4828
|
+
{
|
|
4829
|
+
name: "全选",
|
|
4830
|
+
value: SELECT_ALL,
|
|
4831
|
+
short: "全选",
|
|
4832
|
+
// 默认未全勾(history/projects), 与子项联动状态保持一致
|
|
4833
|
+
checked: false,
|
|
4834
|
+
},
|
|
4835
|
+
...CATEGORY_LIST.map((cat) => ({
|
|
4836
|
+
name: `${cat} — ${CATEGORY_LABELS[cat]} [${scanCategoryLocal(claudeDir, cat)}]`,
|
|
4837
|
+
value: cat,
|
|
4838
|
+
short: cat,
|
|
4839
|
+
checked: !UNCHECKED_BY_DEFAULT.includes(cat),
|
|
4840
|
+
})),
|
|
4841
|
+
];
|
|
4842
|
+
const { picked } = await inquirer.prompt([
|
|
4843
|
+
{
|
|
4844
|
+
message:
|
|
4845
|
+
"请勾选要导出的类别(空格选择, 回车确认; 「全选」与单个类别联动)",
|
|
4846
|
+
name: "picked",
|
|
4847
|
+
type: "checkbox-all",
|
|
4848
|
+
// 到列表边界停止, 不循环回第一条
|
|
4849
|
+
loop: false,
|
|
4850
|
+
choices,
|
|
4851
|
+
},
|
|
4852
|
+
]);
|
|
4853
|
+
const cats = picked.filter((v) => v !== SELECT_ALL);
|
|
4854
|
+
if (!cats.length) {
|
|
4855
|
+
warn("未勾选任何类别, 已取消导出");
|
|
4856
|
+
process.exit(0);
|
|
4857
|
+
}
|
|
4858
|
+
return cats;
|
|
4859
|
+
}
|
|
4860
|
+
|
|
4861
|
+
// 校验并归一化位置参数(local|jianguoyun)
|
|
4862
|
+
function normalizeLocation(value, optionName) {
|
|
4863
|
+
if (value === undefined) return null;
|
|
4864
|
+
if (value !== "local" && value !== "jianguoyun") {
|
|
4865
|
+
die(`参数 ${optionName} 的值 "${value}" 不合法, 可选值: local | jianguoyun`);
|
|
4866
|
+
}
|
|
4867
|
+
return value;
|
|
4868
|
+
}
|
|
4869
|
+
|
|
4870
|
+
// 解析坚果云凭据: CLI参数 > --cache缓存 > 交互(展示缓存默认值), 输入后回写缓存
|
|
4871
|
+
function getJianguoyunSecret(options) {
|
|
4872
|
+
return resolveJianguoyunSecret(
|
|
4873
|
+
{
|
|
4874
|
+
account: options.account,
|
|
4875
|
+
appPassword: options.appPassword,
|
|
4876
|
+
remoteDir: options.remoteDir,
|
|
4877
|
+
},
|
|
4878
|
+
Boolean(options.cache)
|
|
4879
|
+
);
|
|
4880
|
+
}
|
|
4881
|
+
|
|
4882
|
+
// export 选保存位置: --dest 直通(一键), 否则交互选 本地/坚果云
|
|
4883
|
+
async function selectExportDest(options) {
|
|
4884
|
+
const dest = normalizeLocation(options.dest, "--dest");
|
|
4885
|
+
if (dest) return dest;
|
|
4886
|
+
const { picked } = await inquirer.prompt([
|
|
4887
|
+
{
|
|
4888
|
+
type: "list",
|
|
4889
|
+
name: "picked",
|
|
4890
|
+
message: "请选择归档保存位置",
|
|
4891
|
+
choices: [
|
|
4892
|
+
{ name: "本地(当前目录下 zip 文件)", value: "local" },
|
|
4893
|
+
{ name: "坚果云(WebDAV, 账号密码缓存后下次免输)", value: "jianguoyun" },
|
|
4894
|
+
],
|
|
4895
|
+
},
|
|
4896
|
+
]);
|
|
4897
|
+
return picked;
|
|
4898
|
+
}
|
|
4899
|
+
|
|
4900
|
+
async function cmdExport(options) {
|
|
4901
|
+
const claudeDir = await resolveClaudeDirForRead(options);
|
|
4902
|
+
// --categories 传了直通用(一键), 否则交互勾选
|
|
4903
|
+
const cats = options.categories
|
|
4904
|
+
? parseCategories(options.categories)
|
|
4905
|
+
: await selectExportCategories(claudeDir);
|
|
4906
|
+
// 保存位置: 本地 / 坚果云
|
|
4907
|
+
const dest = await selectExportDest(options);
|
|
4908
|
+
const isVerbose = Boolean(options.verbose);
|
|
4909
|
+
const timestamp = getTimestamp();
|
|
4910
|
+
const dateShort = timestamp.slice(0, 10);
|
|
4911
|
+
const fileName = options.output
|
|
4912
|
+
? options.output.split(/[\\/]/).pop()
|
|
4913
|
+
: `claude-export-${dateShort}.zip`;
|
|
4914
|
+
const output = dest === "jianguoyun"
|
|
4915
|
+
? path.join(os.tmpdir(), `claude-export-${timestamp}.zip`) // 坚果云: 先写临时再上传后删除
|
|
4916
|
+
: (options.output || fileName);
|
|
4917
|
+
|
|
4918
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), "claude-migrate-"));
|
|
4919
|
+
const stageRoot = path.join(tmpBase, `claude-migrate-${timestamp}`);
|
|
4920
|
+
const dataDir = path.join(stageRoot, "data");
|
|
4921
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
4922
|
+
|
|
4923
|
+
log(`导出 Claude 数据: ${claudeDir}`);
|
|
4924
|
+
for (const cat of cats) {
|
|
4925
|
+
collectCategory(claudeDir, dataDir, cat, isVerbose);
|
|
4926
|
+
}
|
|
4927
|
+
|
|
4928
|
+
const manifest = {
|
|
4929
|
+
version: MIGRATE_VERSION,
|
|
4930
|
+
source_home: os.homedir(),
|
|
4931
|
+
hostname: os.hostname(),
|
|
4932
|
+
platform: process.platform,
|
|
4933
|
+
date: timestamp,
|
|
4934
|
+
categories: cats,
|
|
4935
|
+
};
|
|
4936
|
+
fs.writeFileSync(
|
|
4937
|
+
path.join(stageRoot, "manifest.json"),
|
|
4938
|
+
JSON.stringify(manifest, null, 2) + "\n",
|
|
4939
|
+
"utf-8"
|
|
4940
|
+
);
|
|
4941
|
+
|
|
4942
|
+
const fileCount = countFiles(dataDir);
|
|
4943
|
+
log(`已暂存 ${fileCount} 个文件`);
|
|
4944
|
+
if (!fileCount) {
|
|
4945
|
+
warn("没有可导出的数据");
|
|
4946
|
+
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
4947
|
+
process.exit(1);
|
|
4948
|
+
}
|
|
4949
|
+
|
|
4950
|
+
const zip = new AdmZip();
|
|
4951
|
+
// tmpBase 下仅 stageRoot 一个子目录, 整体打入 zip 使根目录为 claude-migrate-<ts>/
|
|
4952
|
+
zip.addLocalFolder(tmpBase, "");
|
|
4953
|
+
zip.writeZip(output);
|
|
4954
|
+
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
4955
|
+
const size = (fs.statSync(output).size / 1024).toFixed(1);
|
|
4956
|
+
|
|
4957
|
+
if (dest === "jianguoyun") {
|
|
4958
|
+
// 上传坚果云后删除本地临时文件
|
|
4959
|
+
const secret = await getJianguoyunSecret(options);
|
|
4960
|
+
log(`上传坚果云中...`);
|
|
4961
|
+
const remotePath = await jianguoyunUpload(secret, output, fileName);
|
|
4962
|
+
fs.rmSync(output, { force: true });
|
|
4963
|
+
log(`已上传至坚果云: ${remotePath} (${size} KB)`);
|
|
4964
|
+
} else {
|
|
4965
|
+
log(`已创建归档: ${output} (${size} KB)`);
|
|
4966
|
+
}
|
|
4967
|
+
}
|
|
4968
|
+
|
|
4969
|
+
// ─── import ───────────────────────────────────────────────────────────
|
|
4970
|
+
|
|
4971
|
+
// import 选归档来源: --source 直通(一键), 否则交互选 本地/坚果云
|
|
4972
|
+
async function selectImportSource(options) {
|
|
4973
|
+
const source = normalizeLocation(options.source, "--source");
|
|
4974
|
+
if (source) return source;
|
|
4975
|
+
const { picked } = await inquirer.prompt([
|
|
4976
|
+
{
|
|
4977
|
+
type: "list",
|
|
4978
|
+
name: "picked",
|
|
4979
|
+
message: "请选择归档来源",
|
|
4980
|
+
choices: [
|
|
4981
|
+
{ name: "本地(zip 文件路径)", value: "local" },
|
|
4982
|
+
{ name: "坚果云(WebDAV, 列出远程归档选择)", value: "jianguoyun" },
|
|
4983
|
+
],
|
|
4984
|
+
},
|
|
4985
|
+
]);
|
|
4986
|
+
return picked;
|
|
4987
|
+
}
|
|
4988
|
+
|
|
4989
|
+
// 交互输入本地归档路径
|
|
4990
|
+
async function askLocalArchivePath() {
|
|
4991
|
+
const { archive } = await inquirer.prompt([
|
|
4992
|
+
{
|
|
4993
|
+
type: "input",
|
|
4994
|
+
name: "archive",
|
|
4995
|
+
message: "请输入归档文件路径",
|
|
4996
|
+
validate: (v) => {
|
|
4997
|
+
if (!v.trim()) return "路径不能为空";
|
|
4998
|
+
if (!fs.existsSync(v.trim())) return "文件不存在, 请重新输入";
|
|
4999
|
+
return true;
|
|
5000
|
+
},
|
|
5001
|
+
},
|
|
5002
|
+
]);
|
|
5003
|
+
return archive.trim();
|
|
5004
|
+
}
|
|
5005
|
+
|
|
5006
|
+
// 从坚果云列归档选择并下载; 返回下载后的本地路径
|
|
5007
|
+
async function downloadFromJianguoyun(options, savePath) {
|
|
5008
|
+
const secret = await getJianguoyunSecret(options);
|
|
5009
|
+
log("读取坚果云远程目录...");
|
|
5010
|
+
const files = (await jianguoyunList(secret)).filter((f) =>
|
|
5011
|
+
/\.zip$/i.test(f.name)
|
|
5012
|
+
);
|
|
5013
|
+
if (!files.length) {
|
|
5014
|
+
die(`坚果云 ${secret.remoteDir} 目录下没有 zip 归档`);
|
|
5015
|
+
}
|
|
5016
|
+
// 按修改时间倒序, 最新在前
|
|
5017
|
+
files.sort((a, b) => new Date(b.mtime) - new Date(a.mtime));
|
|
5018
|
+
const { picked } = await inquirer.prompt([
|
|
5019
|
+
{
|
|
5020
|
+
type: "list",
|
|
5021
|
+
name: "picked",
|
|
5022
|
+
message: "请选择要导入的归档",
|
|
5023
|
+
choices: files.map((f) => ({
|
|
5024
|
+
name: `${f.name} (${(f.size / 1024).toFixed(1)} KB)`,
|
|
5025
|
+
value: f.name,
|
|
5026
|
+
})),
|
|
5027
|
+
},
|
|
5028
|
+
]);
|
|
5029
|
+
log(`下载中: ${picked}`);
|
|
5030
|
+
await jianguoyunDownload(secret, picked, savePath);
|
|
5031
|
+
return savePath;
|
|
5032
|
+
}
|
|
5033
|
+
|
|
5034
|
+
async function cmdImport(archive, options) {
|
|
5035
|
+
const strategy = options.strategy || "overwrite";
|
|
5036
|
+
if (!["overwrite", "merge", "skip"].includes(strategy)) {
|
|
5037
|
+
die(`无效策略: ${strategy} (可选: overwrite, merge, skip)`);
|
|
5038
|
+
}
|
|
5039
|
+
const isVerbose = Boolean(options.verbose);
|
|
5040
|
+
const dryRun = Boolean(options.dryRun);
|
|
5041
|
+
|
|
5042
|
+
// 归档解析: 传了路径直接用; 未传(或 --source)交互选 本地/坚果云
|
|
5043
|
+
let downloadedFile = null;
|
|
5044
|
+
try {
|
|
5045
|
+
if (!archive || options.source) {
|
|
5046
|
+
const source = await selectImportSource(options);
|
|
5047
|
+
if (source === "jianguoyun") {
|
|
5048
|
+
downloadedFile = path.join(
|
|
5049
|
+
os.tmpdir(),
|
|
5050
|
+
`claude-download-${getTimestamp()}.zip`
|
|
5051
|
+
);
|
|
5052
|
+
// die() 的 process.exit 不走 finally, 注册退出钩子兜底清理下载临时文件
|
|
5053
|
+
process.once("exit", () => fs.rmSync(downloadedFile, { force: true }));
|
|
5054
|
+
archive = await downloadFromJianguoyun(options, downloadedFile);
|
|
5055
|
+
} else if (!archive) {
|
|
5056
|
+
archive = await askLocalArchivePath();
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
if (!fs.existsSync(archive)) die(`归档不存在: ${archive}`);
|
|
5060
|
+
|
|
5061
|
+
await doImport(archive, options, { strategy, isVerbose, dryRun });
|
|
5062
|
+
} finally {
|
|
5063
|
+
// 坚果云下载的临时文件用后即删
|
|
5064
|
+
if (downloadedFile) fs.rmSync(downloadedFile, { force: true });
|
|
5065
|
+
}
|
|
5066
|
+
}
|
|
5067
|
+
|
|
5068
|
+
// 实际导入(archive 已是存在的本地文件)
|
|
5069
|
+
async function doImport(archive, options, { strategy, isVerbose, dryRun }) {
|
|
5070
|
+
const { zip, manifest, root } = readArchive(archive);
|
|
5071
|
+
const srcHome = manifest.source_home || "";
|
|
5072
|
+
const archiveCategories = manifest.categories || CATEGORY_LIST;
|
|
5073
|
+
const cats = options.categories
|
|
5074
|
+
? parseCategories(options.categories)
|
|
5075
|
+
: archiveCategories.filter((c) => CATEGORY_LIST.includes(c));
|
|
5076
|
+
|
|
5077
|
+
// 解压到临时目录
|
|
5078
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "claude-import-"));
|
|
5079
|
+
// die() 的 process.exit 不走 finally, 注册退出钩子兜底清理解压临时目录
|
|
5080
|
+
process.once("exit", () => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
5081
|
+
try {
|
|
5082
|
+
zip.extractAllTo(tmpDir, true);
|
|
5083
|
+
const archiveRoot = root
|
|
5084
|
+
? path.join(tmpDir, ...root.split("/"))
|
|
5085
|
+
: tmpDir;
|
|
5086
|
+
const dataDir = path.join(archiveRoot, "data");
|
|
5087
|
+
if (!fs.existsSync(dataDir)) die("归档中未找到 data 目录");
|
|
5088
|
+
|
|
5089
|
+
// 目标目录确认(检测 + 确认/自定义)
|
|
5090
|
+
const claudeDir = await resolveClaudeDirForImport(options);
|
|
5091
|
+
const dstHome = path.dirname(path.resolve(claudeDir));
|
|
5092
|
+
const needsRemap = srcHome && srcHome !== dstHome;
|
|
5093
|
+
|
|
5094
|
+
console.log("");
|
|
5095
|
+
console.log(chalk.bold("导入信息:"));
|
|
5096
|
+
console.log(` 来源: ${manifest.hostname || "unknown"} (${srcHome || "unknown"})${manifest.platform ? ` [${manifest.platform}]` : ""}`);
|
|
5097
|
+
console.log(` 目标: ${claudeDir}`);
|
|
5098
|
+
console.log(` 日期: ${manifest.date || "unknown"}`);
|
|
5099
|
+
console.log(` 策略: ${strategy}`);
|
|
5100
|
+
console.log(` 类别: ${cats.join(",")}`);
|
|
5101
|
+
if (needsRemap) {
|
|
5102
|
+
console.log(` ${chalk.yellow("路径重映射")}: ${srcHome} → ${dstHome}`);
|
|
5103
|
+
}
|
|
5104
|
+
console.log("");
|
|
5105
|
+
|
|
5106
|
+
// 路径重映射(projects 目录名 + 文件内容)
|
|
5107
|
+
if (needsRemap) {
|
|
5108
|
+
log("重映射路径...");
|
|
5109
|
+
const projDir = path.join(dataDir, "projects");
|
|
5110
|
+
if (fs.existsSync(projDir)) {
|
|
5111
|
+
for (const name of fs.readdirSync(projDir)) {
|
|
5112
|
+
const newName = remapDirname(name, srcHome, dstHome);
|
|
5113
|
+
if (newName !== name) {
|
|
5114
|
+
fs.renameSync(path.join(projDir, name), path.join(projDir, newName));
|
|
5115
|
+
verbose(`目录重命名: ${name} → ${newName}`, isVerbose || dryRun);
|
|
5116
|
+
}
|
|
5117
|
+
}
|
|
5118
|
+
for (const f of walkFiles(projDir)) {
|
|
5119
|
+
if (f.endsWith(".jsonl")) {
|
|
5120
|
+
remapFileContents(f, srcHome, dstHome);
|
|
5121
|
+
verbose(`内容重映射: ${path.relative(dataDir, f)}`, isVerbose);
|
|
5122
|
+
}
|
|
5123
|
+
}
|
|
5124
|
+
}
|
|
5125
|
+
const historyFile = path.join(dataDir, "history.jsonl");
|
|
5126
|
+
if (fs.existsSync(historyFile)) {
|
|
5127
|
+
remapFileContents(historyFile, srcHome, dstHome);
|
|
5128
|
+
verbose("内容重映射: history.jsonl", isVerbose);
|
|
5129
|
+
}
|
|
5130
|
+
for (const f of PLUGIN_FILES) {
|
|
5131
|
+
const p = path.join(dataDir, "plugins", f);
|
|
5132
|
+
if (fs.existsSync(p)) {
|
|
5133
|
+
remapFileContents(p, srcHome, dstHome);
|
|
5134
|
+
verbose(`内容重映射: plugins/${f}`, isVerbose);
|
|
5135
|
+
}
|
|
5136
|
+
}
|
|
5137
|
+
}
|
|
5138
|
+
|
|
5139
|
+
// 预览模式
|
|
5140
|
+
if (dryRun) {
|
|
5141
|
+
log("预览(dry-run) — 将导入以下内容:");
|
|
5142
|
+
for (const cat of cats) {
|
|
5143
|
+
console.log(` ${chalk.bold(cat)}:`);
|
|
5144
|
+
dryRunCategory(cat, dataDir);
|
|
5145
|
+
}
|
|
5146
|
+
log("预览结束(未做任何修改)");
|
|
5147
|
+
return;
|
|
5148
|
+
}
|
|
5149
|
+
|
|
5150
|
+
// 备份现有数据
|
|
5151
|
+
if (options.backup !== false && fs.existsSync(claudeDir)) {
|
|
5152
|
+
const backupDir = `${claudeDir}-backup-${getTimestamp()}`;
|
|
5153
|
+
log(`备份 ${claudeDir} → ${backupDir}`);
|
|
5154
|
+
fs.cpSync(claudeDir, backupDir, { recursive: true });
|
|
5155
|
+
}
|
|
5156
|
+
|
|
5157
|
+
// 应用导入
|
|
5158
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
5159
|
+
for (const cat of cats) {
|
|
5160
|
+
importCategory(cat, dataDir, claudeDir, strategy, isVerbose);
|
|
5161
|
+
}
|
|
5162
|
+
log("导入完成!");
|
|
5163
|
+
} finally {
|
|
5164
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
5165
|
+
}
|
|
5166
|
+
}
|
|
5167
|
+
|
|
5168
|
+
function dryRunCategory(cat, dataDir) {
|
|
5169
|
+
const listFile = (f) =>
|
|
5170
|
+
fs.existsSync(path.join(dataDir, f)) && console.log(` ${f}`);
|
|
5171
|
+
const listDir = (d) => {
|
|
5172
|
+
const p = path.join(dataDir, d);
|
|
5173
|
+
if (!fs.existsSync(p)) return;
|
|
5174
|
+
const entries = fs.readdirSync(p);
|
|
5175
|
+
for (const name of entries.slice(0, 20)) {
|
|
5176
|
+
console.log(` ${d}/${name}${fs.statSync(path.join(p, name)).isDirectory() ? "/" : ""}`);
|
|
5177
|
+
}
|
|
5178
|
+
if (entries.length > 20) console.log(` ... 共 ${entries.length} 项`);
|
|
5179
|
+
};
|
|
5180
|
+
if (CATEGORY_FILES[cat]) {
|
|
5181
|
+
CATEGORY_FILES[cat].forEach(listFile);
|
|
5182
|
+
} else if (cat === "plugins") {
|
|
5183
|
+
PLUGIN_FILES.forEach((f) => listFile(`plugins/${f}`));
|
|
5184
|
+
} else if (cat === "memory") {
|
|
5185
|
+
MEMORY_FILES.forEach(listFile);
|
|
5186
|
+
listDir(MEMORY_DIR);
|
|
5187
|
+
} else {
|
|
5188
|
+
listDir(cat);
|
|
5189
|
+
}
|
|
5190
|
+
}
|
|
5191
|
+
|
|
5192
|
+
function importCategory(cat, dataDir, claudeDir, strategy, isVerbose) {
|
|
5193
|
+
if (CATEGORY_FILES[cat]) {
|
|
5194
|
+
importFiles(dataDir, claudeDir, CATEGORY_FILES[cat], strategy, isVerbose);
|
|
5195
|
+
} else if (CATEGORY_DIRS[cat]) {
|
|
5196
|
+
importDir(
|
|
5197
|
+
path.join(dataDir, cat),
|
|
5198
|
+
path.join(claudeDir, cat),
|
|
5199
|
+
strategy,
|
|
5200
|
+
isVerbose
|
|
5201
|
+
);
|
|
5202
|
+
} else if (cat === "plugins") {
|
|
5203
|
+
fs.mkdirSync(path.join(claudeDir, "plugins"), { recursive: true });
|
|
5204
|
+
importFiles(
|
|
5205
|
+
path.join(dataDir, "plugins"),
|
|
5206
|
+
path.join(claudeDir, "plugins"),
|
|
5207
|
+
PLUGIN_FILES,
|
|
5208
|
+
strategy,
|
|
5209
|
+
isVerbose
|
|
5210
|
+
);
|
|
5211
|
+
} else if (cat === "memory") {
|
|
5212
|
+
importFiles(dataDir, claudeDir, MEMORY_FILES, strategy, isVerbose);
|
|
5213
|
+
importDir(
|
|
5214
|
+
path.join(dataDir, MEMORY_DIR),
|
|
5215
|
+
path.join(claudeDir, MEMORY_DIR),
|
|
5216
|
+
strategy,
|
|
5217
|
+
isVerbose
|
|
5218
|
+
);
|
|
5219
|
+
}
|
|
5220
|
+
verbose(`已导入: ${cat}`, isVerbose);
|
|
5221
|
+
}
|
|
5222
|
+
|
|
5223
|
+
// 单文件按策略导入(json 浅合并 / jsonl 去重合并)
|
|
5224
|
+
function importFiles(srcDir, dstDir, files, strategy, isVerbose) {
|
|
5225
|
+
for (const f of files) {
|
|
5226
|
+
const src = path.join(srcDir, f);
|
|
5227
|
+
const dst = path.join(dstDir, f);
|
|
5228
|
+
if (!fs.existsSync(src)) continue;
|
|
5229
|
+
if (fs.existsSync(dst)) {
|
|
5230
|
+
if (strategy === "skip") {
|
|
5231
|
+
verbose(`跳过(已存在): ${f}`, isVerbose);
|
|
5232
|
+
continue;
|
|
5233
|
+
}
|
|
5234
|
+
if (strategy === "merge" && f.endsWith(".json")) {
|
|
5235
|
+
mergeJson(src, dst);
|
|
5236
|
+
verbose(`已合并: ${f}`, isVerbose);
|
|
5237
|
+
continue;
|
|
5238
|
+
}
|
|
5239
|
+
if (strategy === "merge" && f.endsWith(".jsonl")) {
|
|
5240
|
+
importJsonl(src, dst);
|
|
5241
|
+
verbose(`已合并 JSONL: ${f}`, isVerbose);
|
|
5242
|
+
continue;
|
|
5243
|
+
}
|
|
5244
|
+
}
|
|
5245
|
+
fs.mkdirSync(dstDir, { recursive: true });
|
|
5246
|
+
fs.copyFileSync(src, dst);
|
|
5247
|
+
verbose(`已拷贝: ${f}`, isVerbose);
|
|
5248
|
+
}
|
|
5249
|
+
}
|
|
5250
|
+
|
|
5251
|
+
// 目录按策略导入: overwrite 整目录覆盖; skip/merge 逐文件处理
|
|
5252
|
+
function importDir(src, dst, strategy, isVerbose) {
|
|
5253
|
+
if (!fs.existsSync(src)) return;
|
|
5254
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
5255
|
+
if (strategy === "overwrite") {
|
|
5256
|
+
fs.cpSync(src, dst, { recursive: true });
|
|
5257
|
+
return;
|
|
5258
|
+
}
|
|
5259
|
+
for (const file of walkFiles(src)) {
|
|
5260
|
+
const rel = path.relative(src, file);
|
|
5261
|
+
const dstFile = path.join(dst, rel);
|
|
5262
|
+
if (!fs.existsSync(dstFile)) {
|
|
5263
|
+
fs.mkdirSync(path.dirname(dstFile), { recursive: true });
|
|
5264
|
+
fs.copyFileSync(file, dstFile);
|
|
5265
|
+
verbose(`新增: ${rel}`, isVerbose);
|
|
5266
|
+
} else if (strategy === "skip") {
|
|
5267
|
+
verbose(`跳过(已存在): ${rel}`, isVerbose);
|
|
5268
|
+
} else if (file.endsWith(".jsonl")) {
|
|
5269
|
+
importJsonl(file, dstFile);
|
|
5270
|
+
verbose(`已合并 JSONL: ${rel}`, isVerbose);
|
|
5271
|
+
} else if (file.endsWith(".json")) {
|
|
5272
|
+
mergeJson(file, dstFile);
|
|
5273
|
+
verbose(`已合并 JSON: ${rel}`, isVerbose);
|
|
5274
|
+
} else {
|
|
5275
|
+
fs.copyFileSync(file, dstFile);
|
|
5276
|
+
verbose(`覆盖(非JSON): ${rel}`, isVerbose);
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
5279
|
+
}
|
|
5280
|
+
|
|
5281
|
+
// JSONL 按策略导入; merge 时按 timestamp+sessionId 去重(目标优先)
|
|
5282
|
+
function importJsonl(src, dst, strategy, isVerbose) {
|
|
5283
|
+
if (!fs.existsSync(dst)) {
|
|
5284
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
5285
|
+
fs.copyFileSync(src, dst);
|
|
5286
|
+
return;
|
|
5287
|
+
}
|
|
5288
|
+
const lines = [];
|
|
5289
|
+
const seen = new Set();
|
|
5290
|
+
// 先读目标(本机数据优先), 再读来源
|
|
5291
|
+
for (const p of [dst, src]) {
|
|
5292
|
+
const text = fs.readFileSync(p, "utf-8");
|
|
5293
|
+
for (let line of text.split("\n")) {
|
|
5294
|
+
line = line.replace(/\r$/, "");
|
|
5295
|
+
if (!line.trim()) continue;
|
|
5296
|
+
let key = null;
|
|
5297
|
+
try {
|
|
5298
|
+
const obj = JSON.parse(line);
|
|
5299
|
+
key = `${obj.timestamp || ""}|${obj.sessionId || ""}`;
|
|
5300
|
+
} catch {
|
|
5301
|
+
key = null;
|
|
5302
|
+
}
|
|
5303
|
+
if (key) {
|
|
5304
|
+
if (seen.has(key)) continue;
|
|
5305
|
+
seen.add(key);
|
|
5306
|
+
}
|
|
5307
|
+
lines.push(line);
|
|
5308
|
+
}
|
|
5309
|
+
}
|
|
5310
|
+
fs.writeFileSync(dst, lines.join("\n") + "\n", "utf-8");
|
|
5311
|
+
}
|
|
5312
|
+
|
|
5313
|
+
// JSON 浅合并: 目标(本机)已有的 key 优先, 来源只补缺失
|
|
5314
|
+
function mergeJson(src, dst) {
|
|
5315
|
+
try {
|
|
5316
|
+
const srcObj = JSON.parse(fs.readFileSync(src, "utf-8"));
|
|
5317
|
+
const dstObj = JSON.parse(fs.readFileSync(dst, "utf-8"));
|
|
5318
|
+
if (
|
|
5319
|
+
srcObj &&
|
|
5320
|
+
dstObj &&
|
|
5321
|
+
typeof srcObj === "object" &&
|
|
5322
|
+
typeof dstObj === "object" &&
|
|
5323
|
+
!Array.isArray(srcObj) &&
|
|
5324
|
+
!Array.isArray(dstObj)
|
|
5325
|
+
) {
|
|
5326
|
+
for (const k of Object.keys(srcObj)) {
|
|
5327
|
+
if (!(k in dstObj)) dstObj[k] = srcObj[k];
|
|
5328
|
+
}
|
|
5329
|
+
fs.writeFileSync(dst, JSON.stringify(dstObj, null, 2) + "\n", "utf-8");
|
|
5330
|
+
}
|
|
5331
|
+
// 非对象 JSON: 保持目标不动
|
|
5332
|
+
} catch {
|
|
5333
|
+
// 解析失败: 保持目标不动
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
|
|
5337
|
+
// ─── 命令入口 ─────────────────────────────────────────────────────────
|
|
5338
|
+
|
|
5339
|
+
async function runClaudeCommand(action, archive, options) {
|
|
5340
|
+
switch (action) {
|
|
5341
|
+
case "list":
|
|
5342
|
+
return archive
|
|
5343
|
+
? cmdListArchive(archive)
|
|
5344
|
+
: cmdListLocal(options);
|
|
5345
|
+
case "export":
|
|
5346
|
+
return cmdExport(options);
|
|
5347
|
+
case "import":
|
|
5348
|
+
return cmdImport(archive, options);
|
|
5349
|
+
default:
|
|
5350
|
+
die(`未知操作: ${action} (可选: list, export, import)`);
|
|
5351
|
+
}
|
|
5352
|
+
}
|
|
5353
|
+
|
|
5354
|
+
function registerClaudeCommand(program, { markHandled, addHelpExample }) {
|
|
5355
|
+
program
|
|
5356
|
+
.command("claude <action> [archive]")
|
|
5357
|
+
.description(
|
|
5358
|
+
"Claude Code 数据迁移: list 查看本地/归档数据 | export 导出zip(本地/坚果云) | import 导入(本地/坚果云, 跨系统路径重映射)"
|
|
5359
|
+
)
|
|
5360
|
+
.option("--categories <cats>", `逗号分隔类别(默认全部): ${ALL_CATEGORIES}`)
|
|
5361
|
+
.option("--output <file>", "export 输出路径(默认 ./claude-export-YYYY-MM-DD.zip)")
|
|
5362
|
+
.option("--dest <dest>", "export 保存位置: local(默认交互选) | jianguoyun")
|
|
5363
|
+
.option("--source <source>", "import 归档来源: local(默认交互选) | jianguoyun")
|
|
5364
|
+
.option("--strategy <mode>", "import 策略: overwrite(默认) | merge | skip")
|
|
5365
|
+
.option("--claude-dir <path>", "指定 claude 数据目录(跳过检测与交互)")
|
|
5366
|
+
.option("--account <mail>", "坚果云账号(邮箱)")
|
|
5367
|
+
.option("--app-password <pwd>", "坚果云应用密码(网页端: 账户信息->安全选项->添加)")
|
|
5368
|
+
.option("--remote-dir <dir>", "坚果云远程目录(默认 claude-migrate)")
|
|
5369
|
+
.option("--cache", "坚果云凭据静默走缓存, 缓存缺失时回退交互")
|
|
5370
|
+
.option("--dry-run", "import 预览, 不写盘")
|
|
5371
|
+
.option("--no-backup", "import 跳过自动备份")
|
|
5372
|
+
.option("--verbose", "显示明细")
|
|
5373
|
+
.action((action, archive, options) => {
|
|
5374
|
+
// 同步置位标记, 防止 index.js 落入默认翻译分支
|
|
5375
|
+
markHandled();
|
|
5376
|
+
runClaudeCommand(action, archive, options).catch((error) => {
|
|
5377
|
+
console.error(chalk.red(error.message || String(error)));
|
|
5378
|
+
process.exit(1);
|
|
5379
|
+
});
|
|
5380
|
+
});
|
|
5381
|
+
addHelpExample("fe-it-beta claude list", "查看本地 Claude 数据概况");
|
|
5382
|
+
addHelpExample("fe-it-beta claude export --verbose", "导出数据(交互选类别与保存位置)");
|
|
5383
|
+
addHelpExample("fe-it-beta claude export --dest jianguoyun --cache", "导出直传坚果云, 凭据走缓存");
|
|
5384
|
+
addHelpExample("fe-it-beta claude import claude-export-2026-09-30.zip --dry-run", "预览导入内容");
|
|
5385
|
+
addHelpExample("fe-it-beta claude import --source jianguoyun --strategy merge --cache", "从坚果云选归档合并导入");
|
|
5386
|
+
}
|
|
5387
|
+
|
|
4184
5388
|
/**
|
|
4185
5389
|
* @fileoverview 不要单独中文
|
|
4186
5390
|
* @author ypf
|
|
@@ -5646,6 +6850,12 @@ registerStoryCommand(commander.program, {
|
|
|
5646
6850
|
addHelpExample: (cmd, desc) => helpExamples.push([cmd, desc]),
|
|
5647
6851
|
});
|
|
5648
6852
|
|
|
6853
|
+
let claudeHandled = false;
|
|
6854
|
+
registerClaudeCommand(commander.program, {
|
|
6855
|
+
markHandled: () => (claudeHandled = true),
|
|
6856
|
+
addHelpExample: (cmd, desc) => helpExamples.push([cmd, desc]),
|
|
6857
|
+
});
|
|
6858
|
+
|
|
5649
6859
|
commander.program.addHelpText(
|
|
5650
6860
|
"after",
|
|
5651
6861
|
"\n" +
|
|
@@ -5698,7 +6908,7 @@ const rules = [
|
|
|
5698
6908
|
],
|
|
5699
6909
|
},
|
|
5700
6910
|
];
|
|
5701
|
-
if (storyHandled) ; else if (sass) {
|
|
6911
|
+
if (claudeHandled) ; else if (storyHandled) ; else if (sass) {
|
|
5702
6912
|
sassMain(cliOpts).catch((error) => console.error(error));
|
|
5703
6913
|
} else if (upload) {
|
|
5704
6914
|
upload$1(cliOpts).catch((error) => console.error(error));
|