@cc-claw/code 0.6.92 → 0.6.93
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/install.js +88 -2
- package/install.test.js +81 -16
- package/package.json +1 -1
package/install.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { createWriteStream, mkdirSync, chmodSync, existsSync, renameSync, unlinkSync, writeFileSync } = require("fs");
|
|
4
4
|
const { join } = require("path");
|
|
5
5
|
const { execSync } = require("child_process");
|
|
6
|
+
const crypto = require("crypto");
|
|
6
7
|
|
|
7
8
|
const { homedir } = require("os");
|
|
8
9
|
|
|
@@ -122,6 +123,34 @@ function extractZip(buffer, dest) {
|
|
|
122
123
|
zip.extractAllTo(dest, true);
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
// 校验下载的二进制包(#290):release 流程会发布 checksums.txt(sha256sum 格式),
|
|
127
|
+
// 此处下载并校验;缺失或不匹配时直接失败(fail-closed),拒绝安装可疑二进制。
|
|
128
|
+
async function verifyChecksum(buffer, fileName) {
|
|
129
|
+
console.log(" Verifying checksum...");
|
|
130
|
+
let body;
|
|
131
|
+
try {
|
|
132
|
+
body = (await download(`${BASE_URL}/checksums.txt`)).toString("utf-8");
|
|
133
|
+
} catch (e) {
|
|
134
|
+
throw new Error(`无法下载 checksums.txt,拒绝安装(fail-closed):${e.message}`);
|
|
135
|
+
}
|
|
136
|
+
let expected = null;
|
|
137
|
+
for (const line of body.split("\n")) {
|
|
138
|
+
const m = line.trim().match(/^([0-9a-fA-F]{64})\s+\*?(.+)$/);
|
|
139
|
+
if (m && m[2].trim() === fileName) {
|
|
140
|
+
expected = m[1].toLowerCase();
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!expected) {
|
|
145
|
+
throw new Error(`checksums.txt 中未找到 ${fileName},拒绝安装(fail-closed)`);
|
|
146
|
+
}
|
|
147
|
+
const actual = crypto.createHash("sha256").update(buffer).digest("hex");
|
|
148
|
+
if (actual !== expected) {
|
|
149
|
+
throw new Error(`checksum 不匹配:${fileName} 可能被篡改,拒绝安装`);
|
|
150
|
+
}
|
|
151
|
+
console.log(" Checksum OK.");
|
|
152
|
+
}
|
|
153
|
+
|
|
125
154
|
// 从 Claude Code env 推导应迁移的 provider 列表(含各档模型别名)
|
|
126
155
|
function detectProvidersFromEnv(env) {
|
|
127
156
|
const providers = [];
|
|
@@ -267,6 +296,59 @@ function migrateFromClaudeCode(home = homedir()) {
|
|
|
267
296
|
return true;
|
|
268
297
|
}
|
|
269
298
|
|
|
299
|
+
// 仅当迁移会创建新 settings.json 并写入 API 密钥时返回 true。
|
|
300
|
+
// (已有 ~/.cc-code/settings.json 时只做别名 backfill,不碰密钥,无需确认。)
|
|
301
|
+
function wouldCopyKeys(home = homedir()) {
|
|
302
|
+
if (existsSync(join(home, ".cc-code", "settings.json"))) return false;
|
|
303
|
+
const claudeSettingsPath = join(home, ".claude", "settings.json");
|
|
304
|
+
if (!existsSync(claudeSettingsPath)) return false;
|
|
305
|
+
try {
|
|
306
|
+
const claudeSettings = JSON.parse(require("fs").readFileSync(claudeSettingsPath, "utf-8"));
|
|
307
|
+
return detectProvidersFromEnv(claudeSettings.env || {}).length > 0;
|
|
308
|
+
} catch {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function promptYesNo(question) {
|
|
314
|
+
const readline = require("readline");
|
|
315
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
316
|
+
return new Promise((resolve) => {
|
|
317
|
+
rl.question(question, (answer) => {
|
|
318
|
+
rl.close();
|
|
319
|
+
resolve(/^(y|yes)$/i.test(answer.trim()));
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 密钥迁移需要显式确认(#290):postinstall 不再静默复制 API 密钥。
|
|
325
|
+
// - CC_CODE_NO_MIGRATE=1 或 --no-migrate:跳过
|
|
326
|
+
// - 非 TTY(如 CI/脚本):跳过并提示手动迁移(安全默认值)
|
|
327
|
+
// - TTY:询问用户,默认 No
|
|
328
|
+
async function maybeMigrateFromClaudeCode(home = homedir()) {
|
|
329
|
+
if (!wouldCopyKeys(home)) {
|
|
330
|
+
// 无需复制密钥(仅 backfill 别名或无 Claude 配置):保持原行为
|
|
331
|
+
return migrateFromClaudeCode(home);
|
|
332
|
+
}
|
|
333
|
+
if (process.env.CC_CODE_NO_MIGRATE === "1" || process.argv.includes("--no-migrate")) {
|
|
334
|
+
console.log(" 已跳过 Claude Code 密钥迁移 (CC_CODE_NO_MIGRATE=1 / --no-migrate)。");
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
if (!process.stdin.isTTY) {
|
|
338
|
+
console.log("");
|
|
339
|
+
console.log(" 检测到 ~/.claude/settings.json,但当前为非交互环境,不会自动复制 API 密钥。");
|
|
340
|
+
console.log(" 如需迁移,请在终端中重新运行安装,或手动复制配置到 ~/.cc-code/settings.json。");
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
console.log("");
|
|
344
|
+
const ok = await promptYesNo(" 检测到 Claude Code 配置,是否将 API 密钥迁移到 ~/.cc-code/settings.json?[y/N] ");
|
|
345
|
+
if (!ok) {
|
|
346
|
+
console.log(" 已跳过密钥迁移。");
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
return migrateFromClaudeCode(home);
|
|
350
|
+
}
|
|
351
|
+
|
|
270
352
|
async function downloadRipgrep(platform, binDir) {
|
|
271
353
|
const rgName = platform.os === "win32" ? "rg.exe" : "rg";
|
|
272
354
|
const rgPath = join(binDir, rgName);
|
|
@@ -333,6 +415,9 @@ async function main() {
|
|
|
333
415
|
|
|
334
416
|
const buffer = await download(url);
|
|
335
417
|
|
|
418
|
+
// #290:校验二进制完整性(fail-closed:缺失/不匹配则拒绝安装)
|
|
419
|
+
await verifyChecksum(buffer, fileName);
|
|
420
|
+
|
|
336
421
|
if (platform.ext === "tar.gz") {
|
|
337
422
|
extractTarGz(buffer, binDir);
|
|
338
423
|
} else {
|
|
@@ -367,7 +452,8 @@ async function main() {
|
|
|
367
452
|
// 下载 ripgrep 预编译二进制(增强 Grep/Glob 性能,失败不阻塞安装)
|
|
368
453
|
await downloadRipgrep(platform, binDir);
|
|
369
454
|
|
|
370
|
-
|
|
455
|
+
// #290:密钥迁移需显式确认,不再静默复制
|
|
456
|
+
const migrated = await maybeMigrateFromClaudeCode();
|
|
371
457
|
|
|
372
458
|
if (!migrated) {
|
|
373
459
|
console.log("");
|
|
@@ -415,4 +501,4 @@ if (require.main === module) {
|
|
|
415
501
|
});
|
|
416
502
|
}
|
|
417
503
|
|
|
418
|
-
module.exports = { migrateFromClaudeCode };
|
|
504
|
+
module.exports = { migrateFromClaudeCode, maybeMigrateFromClaudeCode, wouldCopyKeys, verifyChecksum };
|
package/install.test.js
CHANGED
|
@@ -22,17 +22,30 @@ function readCcCodeSettings(home) {
|
|
|
22
22
|
return JSON.parse(fs.readFileSync(join(home, ".cc-code", "settings.json"), "utf-8"));
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
const __tests = [];
|
|
25
26
|
function test(name, fn) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
__tests.push([name, fn]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function runTests() {
|
|
31
|
+
for (const [name, fn] of __tests) {
|
|
32
|
+
const home = makeTempDir();
|
|
33
|
+
try {
|
|
34
|
+
await fn(home);
|
|
35
|
+
console.log(` ✓ ${name}`);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.error(` ✗ ${name}`);
|
|
38
|
+
console.error(e.message);
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
} finally {
|
|
41
|
+
cleanup(home);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
console.log("");
|
|
45
|
+
if (process.exitCode) {
|
|
46
|
+
console.log("Some tests failed.");
|
|
47
|
+
} else {
|
|
48
|
+
console.log("All tests passed.");
|
|
36
49
|
}
|
|
37
50
|
}
|
|
38
51
|
|
|
@@ -168,9 +181,61 @@ test("migrateFromClaudeCode supports CODEX_API_KEY fallback", (home) => {
|
|
|
168
181
|
if (cfg.config.providers[0].apiKey !== "sk-codex-xxx") throw new Error("expected CODEX_API_KEY to be used");
|
|
169
182
|
});
|
|
170
183
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
184
|
+
// ─────────────────────────────────────────────
|
|
185
|
+
// #290:checksum 校验 + 密钥迁移确认制
|
|
186
|
+
// ─────────────────────────────────────────────
|
|
187
|
+
const { maybeMigrateFromClaudeCode, wouldCopyKeys } = require("./install");
|
|
188
|
+
|
|
189
|
+
test("wouldCopyKeys is false when cc-code settings already exist", (home) => {
|
|
190
|
+
const ccCodeDir = join(home, ".cc-code");
|
|
191
|
+
fs.mkdirSync(ccCodeDir, { recursive: true });
|
|
192
|
+
fs.writeFileSync(join(ccCodeDir, "settings.json"), JSON.stringify({ config: {} }));
|
|
193
|
+
writeClaudeSettings(home, { env: { ANTHROPIC_API_KEY: "sk-ant" } });
|
|
194
|
+
if (wouldCopyKeys(home) !== false) throw new Error("expected false (backfill only)");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("wouldCopyKeys is true on fresh install with claude keys", (home) => {
|
|
198
|
+
writeClaudeSettings(home, { env: { ANTHROPIC_API_KEY: "sk-ant" } });
|
|
199
|
+
if (wouldCopyKeys(home) !== true) throw new Error("expected true");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("wouldCopyKeys is false without claude settings", (home) => {
|
|
203
|
+
if (wouldCopyKeys(home) !== false) throw new Error("expected false");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("maybeMigrateFromClaudeCode skips key copy in non-TTY without prompt", async (home) => {
|
|
207
|
+
writeClaudeSettings(home, { env: { ANTHROPIC_API_KEY: "sk-ant-must-not-copy" } });
|
|
208
|
+
const result = await maybeMigrateFromClaudeCode(home);
|
|
209
|
+
if (result !== false) throw new Error("expected false (skipped)");
|
|
210
|
+
if (fs.existsSync(join(home, ".cc-code", "settings.json"))) {
|
|
211
|
+
throw new Error("must not create settings.json without consent");
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("maybeMigrateFromClaudeCode honors CC_CODE_NO_MIGRATE=1", async (home) => {
|
|
216
|
+
writeClaudeSettings(home, { env: { ANTHROPIC_API_KEY: "sk-ant" } });
|
|
217
|
+
process.env.CC_CODE_NO_MIGRATE = "1";
|
|
218
|
+
try {
|
|
219
|
+
const result = await maybeMigrateFromClaudeCode(home);
|
|
220
|
+
if (result !== false) throw new Error("expected false (skipped by env)");
|
|
221
|
+
} finally {
|
|
222
|
+
delete process.env.CC_CODE_NO_MIGRATE;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("maybeMigrateFromClaudeCode still backfills without consent", async (home) => {
|
|
227
|
+
const ccCodeDir = join(home, ".cc-code");
|
|
228
|
+
fs.mkdirSync(ccCodeDir, { recursive: true });
|
|
229
|
+
fs.writeFileSync(
|
|
230
|
+
join(ccCodeDir, "settings.json"),
|
|
231
|
+
JSON.stringify({ config: { providers: [{ id: "anthropic", type: "anthropic", apiKey: "sk-x", models: {} }] } })
|
|
232
|
+
);
|
|
233
|
+
writeClaudeSettings(home, { env: { ANTHROPIC_API_KEY: "sk-ant", ANTHROPIC_DEFAULT_FABLE_MODEL: "claude-fable-5" } });
|
|
234
|
+
const result = await maybeMigrateFromClaudeCode(home);
|
|
235
|
+
if (result !== true) throw new Error("expected true (backfill needs no consent)");
|
|
236
|
+
const cfg = readCcCodeSettings(home);
|
|
237
|
+
if (cfg.config.providers[0].models.fable !== "claude-fable-5") throw new Error("expected fable backfilled");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// 由 runTests() 顺序执行(支持 async 用例)
|
|
241
|
+
runTests();
|