@cc-claw/code 0.6.178 → 0.6.179
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/README.md +4 -10
- package/bin/cc-code +214 -12
- package/package.json +11 -6
- package/install.js +0 -683
- package/install.test.js +0 -479
package/install.js
DELETED
|
@@ -1,683 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const { createWriteStream, mkdirSync, chmodSync, existsSync, renameSync, unlinkSync, writeFileSync, readdirSync } = fs;
|
|
5
|
-
const { join, basename, dirname } = require("path");
|
|
6
|
-
const { execSync } = require("child_process");
|
|
7
|
-
const crypto = require("crypto");
|
|
8
|
-
|
|
9
|
-
const { homedir } = require("os");
|
|
10
|
-
|
|
11
|
-
const VERSION = require("./package.json").version;
|
|
12
|
-
const REPO = "cc-claws/cc-code";
|
|
13
|
-
const BASE_URL = `https://github.com/${REPO}/releases/download/npm-v${VERSION}`;
|
|
14
|
-
const RG_VERSION = "15.0.1";
|
|
15
|
-
const RG_BASE_URL = `https://github.com/microsoft/ripgrep-prebuilt/releases/download/v${RG_VERSION}`;
|
|
16
|
-
|
|
17
|
-
const PLATFORMS = {
|
|
18
|
-
"linux-x64": { os: "linux", arch: "x64", suffix: "linux-x86_64", ext: "tar.gz", rgSuffix: "x86_64-unknown-linux-musl" },
|
|
19
|
-
"linux-arm64": { os: "linux", arch: "arm64", suffix: "linux-aarch64", ext: "tar.gz", rgSuffix: "aarch64-unknown-linux-gnu" },
|
|
20
|
-
"darwin-x64": { os: "darwin", arch: "x64", suffix: "macos-x86_64", ext: "tar.gz", rgSuffix: "x86_64-apple-darwin" },
|
|
21
|
-
"darwin-arm64": { os: "darwin", arch: "arm64", suffix: "macos-aarch64", ext: "tar.gz", rgSuffix: "aarch64-apple-darwin" },
|
|
22
|
-
"win32-x64": { os: "win32", arch: "x64", suffix: "windows-x86_64", ext: "zip", rgSuffix: "x86_64-pc-windows-msvc" },
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
function getPlatformKey() {
|
|
26
|
-
const key = `${process.platform}-${process.arch}`;
|
|
27
|
-
if (!PLATFORMS[key]) {
|
|
28
|
-
throw new Error(`Unsupported platform: ${key}. Supported: ${Object.keys(PLATFORMS).join(", ")}`);
|
|
29
|
-
}
|
|
30
|
-
return key;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function getProxyUrl() {
|
|
34
|
-
// Check common proxy environment variables
|
|
35
|
-
const proxy = process.env.HTTPS_PROXY || process.env.https_proxy
|
|
36
|
-
|| process.env.HTTP_PROXY || process.env.http_proxy
|
|
37
|
-
|| process.env.ALL_PROXY || process.env.all_proxy;
|
|
38
|
-
return proxy || null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function download(url) {
|
|
42
|
-
const proxyUrl = getProxyUrl();
|
|
43
|
-
|
|
44
|
-
if (proxyUrl) {
|
|
45
|
-
return downloadViaProxy(url, proxyUrl);
|
|
46
|
-
}
|
|
47
|
-
return downloadDirect(url);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function downloadDirect(url) {
|
|
51
|
-
const { get } = require("https");
|
|
52
|
-
return new Promise((resolve, reject) => {
|
|
53
|
-
get(url, (res) => {
|
|
54
|
-
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
55
|
-
downloadDirect(res.headers.location).then(resolve, reject);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (res.statusCode !== 200) {
|
|
59
|
-
reject(new Error(`Download failed: HTTP ${res.statusCode} for ${url}`));
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
const chunks = [];
|
|
63
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
64
|
-
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
65
|
-
res.on("error", reject);
|
|
66
|
-
}).on("error", reject);
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function downloadViaProxy(url, proxyUrl) {
|
|
71
|
-
const { URL } = require("url");
|
|
72
|
-
const target = new URL(url);
|
|
73
|
-
const proxy = new URL(proxyUrl);
|
|
74
|
-
|
|
75
|
-
const isHttps = proxy.protocol === "https:" || proxy.protocol === "HTTPS:";
|
|
76
|
-
const proxyModule = isHttps ? require("https") : require("http");
|
|
77
|
-
|
|
78
|
-
const proxyOpts = {
|
|
79
|
-
hostname: proxy.hostname,
|
|
80
|
-
port: proxy.port || (isHttps ? 443 : 80),
|
|
81
|
-
path: url,
|
|
82
|
-
method: "GET",
|
|
83
|
-
headers: { "Host": target.hostname, "User-Agent": "cc-code-installer" },
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
// Support proxy auth
|
|
87
|
-
if (proxy.username) {
|
|
88
|
-
const auth = decodeURIComponent(`${proxy.username}:${proxy.password || ""}`);
|
|
89
|
-
proxyOpts.headers["Proxy-Authorization"] = `Basic ${Buffer.from(auth).toString("base64")}`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
console.log(` Using proxy: ${proxy.hostname}:${proxy.port || (isHttps ? 443 : 80)}`);
|
|
93
|
-
|
|
94
|
-
return new Promise((resolve, reject) => {
|
|
95
|
-
const req = proxyModule.request(proxyOpts, (res) => {
|
|
96
|
-
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
97
|
-
download(res.headers.location).then(resolve, reject);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (res.statusCode !== 200) {
|
|
101
|
-
reject(new Error(`Download failed: HTTP ${res.statusCode} for ${url}`));
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
const chunks = [];
|
|
105
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
106
|
-
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
107
|
-
res.on("error", reject);
|
|
108
|
-
});
|
|
109
|
-
req.on("error", reject);
|
|
110
|
-
req.end();
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function extractTarGz(buffer, dest) {
|
|
115
|
-
const tmpFile = join(dest, "cc-code.tar.gz");
|
|
116
|
-
writeFileSync(tmpFile, buffer);
|
|
117
|
-
execSync(`tar -xzf "${tmpFile}" -C "${dest}"`, { stdio: "ignore" });
|
|
118
|
-
unlinkSync(tmpFile);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function extractZip(buffer, dest) {
|
|
122
|
-
const AdmZip = require("adm-zip");
|
|
123
|
-
const zip = new AdmZip(buffer);
|
|
124
|
-
zip.extractAllTo(dest, true);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// 解压 ripgrep 的 tar.gz 到 dest。
|
|
128
|
-
// 上游 microsoft/ripgrep-prebuilt 的资产把 rg 直接放在根目录(`rg`),
|
|
129
|
-
// 不带 ripgrep-x.y.z-suffix/ 子目录;早先的实现假定存在该子目录并使用
|
|
130
|
-
// --strip-components=1,导致在所有 Linux/macOS 平台上都解不出 rg,
|
|
131
|
-
// 包内 bin/rg 永不落地、只能降级到系统 rg。此处先列目录判断实际布局,
|
|
132
|
-
// 两种结构都兼容,且把临时文件清理放进 finally 避免失败路径泄漏。
|
|
133
|
-
function extractRgTarGz(buffer, dest) {
|
|
134
|
-
const tmpFile = join(dest, "rg-download.tar.gz");
|
|
135
|
-
writeFileSync(tmpFile, buffer);
|
|
136
|
-
try {
|
|
137
|
-
let hasRootRg = false;
|
|
138
|
-
try {
|
|
139
|
-
const listing = execSync(`tar tzf "${tmpFile}"`, {
|
|
140
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
141
|
-
}).toString();
|
|
142
|
-
hasRootRg = listing
|
|
143
|
-
.split("\n")
|
|
144
|
-
.map((line) => line.trim())
|
|
145
|
-
// 兼容 "./rg" 前缀与 "rg" 两种写法
|
|
146
|
-
.some((line) => line === "rg" || line === "./rg");
|
|
147
|
-
} catch {
|
|
148
|
-
hasRootRg = false;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (hasRootRg) {
|
|
152
|
-
execSync(`tar -xzf "${tmpFile}" -C "${dest}"`, { stdio: "ignore" });
|
|
153
|
-
} else {
|
|
154
|
-
execSync(
|
|
155
|
-
`tar -xzf "${tmpFile}" -C "${dest}" --strip-components=1 --wildcards "*/rg"`,
|
|
156
|
-
{ stdio: "ignore" }
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
} finally {
|
|
160
|
-
try {
|
|
161
|
-
unlinkSync(tmpFile);
|
|
162
|
-
} catch {
|
|
163
|
-
// 临时文件清理失败不应影响安装
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// 校验下载的二进制包(#290):release 流程会发布 checksums.txt(sha256sum 格式),
|
|
169
|
-
// 此处下载并校验;缺失或不匹配时直接失败(fail-closed),拒绝安装可疑二进制。
|
|
170
|
-
async function verifyChecksum(buffer, fileName) {
|
|
171
|
-
console.log(" Verifying checksum...");
|
|
172
|
-
let body;
|
|
173
|
-
try {
|
|
174
|
-
body = (await download(`${BASE_URL}/checksums.txt`)).toString("utf-8");
|
|
175
|
-
} catch (e) {
|
|
176
|
-
throw new Error(`无法下载 checksums.txt,拒绝安装(fail-closed):${e.message}`);
|
|
177
|
-
}
|
|
178
|
-
let expected = null;
|
|
179
|
-
for (const line of body.split("\n")) {
|
|
180
|
-
const m = line.trim().match(/^([0-9a-fA-F]{64})\s+\*?(.+)$/);
|
|
181
|
-
if (m && m[2].trim() === fileName) {
|
|
182
|
-
expected = m[1].toLowerCase();
|
|
183
|
-
break;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
if (!expected) {
|
|
187
|
-
throw new Error(`checksums.txt 中未找到 ${fileName},拒绝安装(fail-closed)`);
|
|
188
|
-
}
|
|
189
|
-
const actual = crypto.createHash("sha256").update(buffer).digest("hex");
|
|
190
|
-
if (actual !== expected) {
|
|
191
|
-
throw new Error(`checksum 不匹配:${fileName} 可能被篡改,拒绝安装`);
|
|
192
|
-
}
|
|
193
|
-
console.log(" Checksum OK.");
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// 从 CC env 推导应迁移的 provider 列表(含各档模型别名)
|
|
197
|
-
function detectProvidersFromEnv(env) {
|
|
198
|
-
const providers = [];
|
|
199
|
-
|
|
200
|
-
// 检测 Anthropic
|
|
201
|
-
const anthropicKey = env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN || "";
|
|
202
|
-
const anthropicBaseUrl = env.ANTHROPIC_BASE_URL || "";
|
|
203
|
-
if (anthropicKey || anthropicBaseUrl) {
|
|
204
|
-
const models = {};
|
|
205
|
-
if (env.ANTHROPIC_DEFAULT_OPUS_MODEL) models.opus = env.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
|
206
|
-
if (env.ANTHROPIC_DEFAULT_SONNET_MODEL) models.sonnet = env.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
|
207
|
-
if (env.ANTHROPIC_DEFAULT_HAIKU_MODEL) models.haiku = env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
|
208
|
-
if (env.ANTHROPIC_DEFAULT_FABLE_MODEL) models.fable = env.ANTHROPIC_DEFAULT_FABLE_MODEL;
|
|
209
|
-
const p = {
|
|
210
|
-
id: "anthropic",
|
|
211
|
-
type: "anthropic",
|
|
212
|
-
apiKey: anthropicKey,
|
|
213
|
-
};
|
|
214
|
-
if (anthropicBaseUrl) p.baseUrl = anthropicBaseUrl;
|
|
215
|
-
if (Object.keys(models).length > 0) p.models = models;
|
|
216
|
-
providers.push(p);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// 检测 OpenAI 兼容
|
|
220
|
-
const openaiKey = env.OPENAI_API_KEY || env.CODEX_API_KEY || "";
|
|
221
|
-
const openaiBaseUrl = env.OPENAI_BASE_URL || env.OPENAI_API_BASE || "";
|
|
222
|
-
if (openaiKey || openaiBaseUrl) {
|
|
223
|
-
const models = {};
|
|
224
|
-
if (env.OPENAI_MODEL) models.sonnet = env.OPENAI_MODEL;
|
|
225
|
-
const p = {
|
|
226
|
-
id: "openai",
|
|
227
|
-
type: "openai",
|
|
228
|
-
apiKey: openaiKey,
|
|
229
|
-
};
|
|
230
|
-
if (openaiBaseUrl) p.baseUrl = openaiBaseUrl;
|
|
231
|
-
if (Object.keys(models).length > 0) p.models = models;
|
|
232
|
-
providers.push(p);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return providers;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// 已有 ~/.cc-code/settings.json 时,按 provider id 增量补齐缺失的模型别名。
|
|
239
|
-
// 只补空缺(undefined / null / 空串),绝不覆盖用户已有值,也不新增 provider。
|
|
240
|
-
// 这样新版本新增的别名(如 fable)能自动补进老配置,而用户的改动(key、baseUrl、
|
|
241
|
-
// thinking 等)与已填别名一律保持原样。
|
|
242
|
-
function backfillModels(ccCodeSettingsPath, detected) {
|
|
243
|
-
let existing;
|
|
244
|
-
try {
|
|
245
|
-
existing = JSON.parse(require("fs").readFileSync(ccCodeSettingsPath, "utf-8"));
|
|
246
|
-
} catch {
|
|
247
|
-
return true; // 读不动就保持原样,不破坏用户配置
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const providers = existing && existing.config && existing.config.providers;
|
|
251
|
-
if (!Array.isArray(providers)) {
|
|
252
|
-
return true;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
let changed = false;
|
|
256
|
-
for (const det of detected) {
|
|
257
|
-
if (!det.models) continue;
|
|
258
|
-
const target = providers.find((p) => p && p.id === det.id);
|
|
259
|
-
if (!target) continue; // 找不到同 id provider → 不动(不新增)
|
|
260
|
-
if (!target.models || typeof target.models !== "object") {
|
|
261
|
-
target.models = {};
|
|
262
|
-
}
|
|
263
|
-
for (const alias of Object.keys(det.models)) {
|
|
264
|
-
const cur = target.models[alias];
|
|
265
|
-
if (cur === undefined || cur === null || cur === "") {
|
|
266
|
-
target.models[alias] = det.models[alias];
|
|
267
|
-
changed = true;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (changed) {
|
|
273
|
-
writeFileSync(ccCodeSettingsPath, JSON.stringify(existing, null, 2) + "\n");
|
|
274
|
-
console.log("");
|
|
275
|
-
console.log(" Backfilled new model aliases into ~/.cc-code/settings.json");
|
|
276
|
-
}
|
|
277
|
-
return true;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// 统一项目记忆目录:CC 与 cc-code 各维护一份 projects/<slug>/memory,一旦两侧
|
|
281
|
-
// 都落过盘,记忆就会写散(系统提示对外呈现的 ~/.cc-code 路径,与实际读写的 ~/.claude
|
|
282
|
-
// 目录并不是同一份)。安装时把 CC 侧已有的记忆目录在 cc-code 侧以链接接过来,
|
|
283
|
-
// 使两侧读写同一份物理目录:Windows 用 junction(免管理员权限),Unix 用目录符号链接。
|
|
284
|
-
// 只做 .claude -> .cc-code 单向,不往用户的 CC 目录里添东西;
|
|
285
|
-
// 目标已存在(用户自己的数据或已建好的链接)一律不碰,任何失败只跳过、不影响安装。
|
|
286
|
-
function linkProjectMemories(home = homedir()) {
|
|
287
|
-
const linkType = process.platform === "win32" ? "junction" : "dir";
|
|
288
|
-
const from = join(home, ".claude", "projects");
|
|
289
|
-
const to = join(home, ".cc-code", "projects");
|
|
290
|
-
|
|
291
|
-
let entries;
|
|
292
|
-
try {
|
|
293
|
-
entries = readdirSync(from, { withFileTypes: true });
|
|
294
|
-
} catch {
|
|
295
|
-
return 0; // 没用过 CC,没有可承接的记忆
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
let linked = 0;
|
|
299
|
-
for (const entry of entries) {
|
|
300
|
-
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
301
|
-
const source = join(from, entry.name, "memory");
|
|
302
|
-
const target = join(to, entry.name, "memory");
|
|
303
|
-
if (!existsSync(source) || existsSync(target)) continue;
|
|
304
|
-
try {
|
|
305
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
306
|
-
fs.symlinkSync(source, target, linkType);
|
|
307
|
-
linked++;
|
|
308
|
-
} catch (err) {
|
|
309
|
-
console.log(` (skipped memory link for ${entry.name}: ${err.message})`);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
if (linked > 0) {
|
|
313
|
-
console.log(` Linked ${linked} .claude project memory dir(s) into ~/.cc-code/projects/`);
|
|
314
|
-
}
|
|
315
|
-
return linked;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function migrateFromClaudeCode(home = homedir()) {
|
|
319
|
-
const claudeSettingsPath = join(home, ".claude", "settings.json");
|
|
320
|
-
const ccCodeDir = join(home, ".cc-code");
|
|
321
|
-
const ccCodeSettingsPath = join(ccCodeDir, "settings.json");
|
|
322
|
-
|
|
323
|
-
const hasCcCode = existsSync(ccCodeSettingsPath);
|
|
324
|
-
|
|
325
|
-
// 无 CC 配置:已有 cc-code 视为成功(无需 Quick Start),否则失败
|
|
326
|
-
if (!existsSync(claudeSettingsPath)) {
|
|
327
|
-
return hasCcCode;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
let claudeSettings;
|
|
331
|
-
try {
|
|
332
|
-
claudeSettings = JSON.parse(require("fs").readFileSync(claudeSettingsPath, "utf-8"));
|
|
333
|
-
} catch {
|
|
334
|
-
return hasCcCode;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const env = claudeSettings.env || {};
|
|
338
|
-
const providers = detectProvidersFromEnv(env);
|
|
339
|
-
|
|
340
|
-
if (providers.length === 0) {
|
|
341
|
-
return hasCcCode;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// 已有 cc-code 配置 → 增量补齐缺失别名(不覆盖、不新增 provider)
|
|
345
|
-
if (hasCcCode) {
|
|
346
|
-
return backfillModels(ccCodeSettingsPath, providers);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (!existsSync(ccCodeDir)) {
|
|
350
|
-
mkdirSync(ccCodeDir, { recursive: true });
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// 根据第一个 provider 的可用模型决定默认激活别名
|
|
354
|
-
const firstProvider = providers[0];
|
|
355
|
-
let activeAlias = "opus";
|
|
356
|
-
if (firstProvider.models) {
|
|
357
|
-
if (firstProvider.models.opus) activeAlias = "opus";
|
|
358
|
-
else if (firstProvider.models.sonnet) activeAlias = "sonnet";
|
|
359
|
-
else if (firstProvider.models.haiku) activeAlias = "haiku";
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const ccCodeSettings = {
|
|
363
|
-
config: {
|
|
364
|
-
active_alias: activeAlias,
|
|
365
|
-
active_provider_id: firstProvider.id,
|
|
366
|
-
providers,
|
|
367
|
-
},
|
|
368
|
-
};
|
|
369
|
-
writeFileSync(ccCodeSettingsPath, JSON.stringify(ccCodeSettings, null, 2) + "\n");
|
|
370
|
-
console.log("");
|
|
371
|
-
console.log(" Detected .claude configuration (compatible with cc-code).");
|
|
372
|
-
console.log(" Migrated ~/.claude/settings.json -> ~/.cc-code/settings.json");
|
|
373
|
-
console.log(` Found ${providers.length} provider(s): ${providers.map(p => p.type).join(", ")}`);
|
|
374
|
-
console.log(" NOTE: API keys were copied into ~/.cc-code/settings.json (plain text).");
|
|
375
|
-
console.log(" Review it if you do not want credentials duplicated there.");
|
|
376
|
-
return true;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// 仅当迁移会创建新 settings.json 并写入 API 密钥时返回 true。
|
|
380
|
-
// (已有 ~/.cc-code/settings.json 时只做别名 backfill,不碰密钥,无需确认。)
|
|
381
|
-
function wouldCopyKeys(home = homedir()) {
|
|
382
|
-
if (existsSync(join(home, ".cc-code", "settings.json"))) return false;
|
|
383
|
-
const claudeSettingsPath = join(home, ".claude", "settings.json");
|
|
384
|
-
if (!existsSync(claudeSettingsPath)) return false;
|
|
385
|
-
try {
|
|
386
|
-
const claudeSettings = JSON.parse(require("fs").readFileSync(claudeSettingsPath, "utf-8"));
|
|
387
|
-
return detectProvidersFromEnv(claudeSettings.env || {}).length > 0;
|
|
388
|
-
} catch {
|
|
389
|
-
return false;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function promptYesNo(question) {
|
|
394
|
-
const readline = require("readline");
|
|
395
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
396
|
-
return new Promise((resolve) => {
|
|
397
|
-
rl.question(question, (answer) => {
|
|
398
|
-
rl.close();
|
|
399
|
-
resolve(/^(y|yes)$/i.test(answer.trim()));
|
|
400
|
-
});
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// 密钥迁移需要显式确认(#290):postinstall 不再静默复制 API 密钥。
|
|
405
|
-
// - CC_CODE_NO_MIGRATE=1 或 --no-migrate:跳过
|
|
406
|
-
// - 非 TTY(如 CI/脚本):跳过并提示手动迁移(安全默认值)
|
|
407
|
-
// - TTY:询问用户,默认 No
|
|
408
|
-
async function maybeMigrateFromClaudeCode(home = homedir()) {
|
|
409
|
-
if (!wouldCopyKeys(home)) {
|
|
410
|
-
// 无需复制密钥(仅 backfill 别名或无 Claude 配置):保持原行为
|
|
411
|
-
return migrateFromClaudeCode(home);
|
|
412
|
-
}
|
|
413
|
-
if (process.env.CC_CODE_NO_MIGRATE === "1" || process.argv.includes("--no-migrate")) {
|
|
414
|
-
console.log(" 已跳过 .claude 密钥迁移 (CC_CODE_NO_MIGRATE=1 / --no-migrate)。");
|
|
415
|
-
return false;
|
|
416
|
-
}
|
|
417
|
-
if (!process.stdin.isTTY) {
|
|
418
|
-
console.log("");
|
|
419
|
-
console.log(" 检测到 ~/.claude/settings.json,但当前为非交互环境,不会自动复制 API 密钥。");
|
|
420
|
-
console.log(" 如需迁移,请在终端中重新运行安装,或手动复制配置到 ~/.cc-code/settings.json。");
|
|
421
|
-
return false;
|
|
422
|
-
}
|
|
423
|
-
console.log("");
|
|
424
|
-
const ok = await promptYesNo(" 检测到 .claude 配置,是否将 API 密钥迁移到 ~/.cc-code/settings.json?[y/N] ");
|
|
425
|
-
if (!ok) {
|
|
426
|
-
console.log(" 已跳过密钥迁移。");
|
|
427
|
-
return false;
|
|
428
|
-
}
|
|
429
|
-
return migrateFromClaudeCode(home);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
async function downloadRipgrep(platform, binDir) {
|
|
433
|
-
const rgName = platform.os === "win32" ? "rg.exe" : "rg";
|
|
434
|
-
const rgPath = join(binDir, rgName);
|
|
435
|
-
|
|
436
|
-
// 已存在则跳过
|
|
437
|
-
if (existsSync(rgPath)) {
|
|
438
|
-
console.log(" ripgrep already installed.");
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
try {
|
|
443
|
-
const rgExt = platform.os === "win32" ? "zip" : "tar.gz";
|
|
444
|
-
const rgFileName = `ripgrep-v${RG_VERSION}-${platform.rgSuffix}.${rgExt}`;
|
|
445
|
-
const rgUrl = `${RG_BASE_URL}/${rgFileName}`;
|
|
446
|
-
|
|
447
|
-
console.log(` Downloading ripgrep ${RG_VERSION}...`);
|
|
448
|
-
const rgBuffer = await download(rgUrl);
|
|
449
|
-
|
|
450
|
-
// 解压 rg 二进制到 binDir
|
|
451
|
-
if (rgExt === "tar.gz") {
|
|
452
|
-
extractRgTarGz(rgBuffer, binDir);
|
|
453
|
-
} else {
|
|
454
|
-
const AdmZip = require("adm-zip");
|
|
455
|
-
const zip = new AdmZip(rgBuffer);
|
|
456
|
-
// Windows zip 内含 rg.exe 在根目录或子目录
|
|
457
|
-
const entries = zip.getEntries();
|
|
458
|
-
for (const entry of entries) {
|
|
459
|
-
if (entry.entryName.endsWith("rg.exe")) {
|
|
460
|
-
writeFileSync(rgPath, entry.getData());
|
|
461
|
-
break;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (existsSync(rgPath)) {
|
|
467
|
-
if (platform.os !== "win32") chmodSync(rgPath, 0o755);
|
|
468
|
-
console.log(" ripgrep installed (enhances Grep/Glob performance).");
|
|
469
|
-
} else {
|
|
470
|
-
console.log(" ripgrep extraction skipped (Grep/Glob will use built-in engine).");
|
|
471
|
-
}
|
|
472
|
-
} catch (err) {
|
|
473
|
-
// rg 下载失败不阻塞安装,Rust 内置引擎作为 fallback
|
|
474
|
-
console.log(` ripgrep download skipped: ${err.message} (Grep/Glob will use built-in engine).`);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// #410:把 npm 自动生成的全局入口(<prefix>/cc-code.cmd|.ps1)改写为直起 exe。
|
|
479
|
-
// 全局安装时 __dirname = <prefix>/node_modules/@cc-claw/code,故 prefix 根为
|
|
480
|
-
// __dirname/../../..。仅在能定位到 npm 生成的 wrapper 时才覆盖;任何失败都不
|
|
481
|
-
// 阻塞安装(此时 exe 同目录 bin/cc-code.cmd|.ps1 仍可直起)。
|
|
482
|
-
function overwriteNpmGlobalWrapper(binDirPath) {
|
|
483
|
-
try {
|
|
484
|
-
const prefixRoot = join(__dirname, "..", "..", "..");
|
|
485
|
-
const exePath = join(binDirPath, "cc-code.exe");
|
|
486
|
-
// 全局 cmd 用相对自身的路径引用 exe,避免硬编码绝对路径(prefix 可能含空格)
|
|
487
|
-
const cmdTarget = join(prefixRoot, "cc-code.cmd");
|
|
488
|
-
const ps1Target = join(prefixRoot, "cc-code.ps1");
|
|
489
|
-
|
|
490
|
-
// 防御:仅当 exe 存在且全局 wrapper 也存在(确是 npm 全局安装布局)时才覆盖
|
|
491
|
-
if (!existsSync(exePath)) return;
|
|
492
|
-
if (!existsSync(cmdTarget) && !existsSync(ps1Target)) return;
|
|
493
|
-
|
|
494
|
-
// exe 相对 prefix 根的路径:node_modules/@cc-claw/code/bin/cc-code.exe
|
|
495
|
-
const relExe = join("node_modules", "@cc-claw", "code", "bin", "cc-code.exe");
|
|
496
|
-
|
|
497
|
-
writeFileSync(
|
|
498
|
-
cmdTarget,
|
|
499
|
-
`@echo off\r\n"%~dp0${relExe.replace(/\//g, "\\")}" %*\r\n`
|
|
500
|
-
);
|
|
501
|
-
writeFileSync(
|
|
502
|
-
ps1Target,
|
|
503
|
-
`$basedir = Split-Path $MyInvocation.MyCommand.Definition -Parent\r\n& "$basedir\\${relExe.replace(/\//g, "\\")}" @args\r\nexit $LASTEXITCODE\r\n`
|
|
504
|
-
);
|
|
505
|
-
console.log(" Rewrote npm global wrapper to launch cc-code.exe directly (Ctrl+C fix).");
|
|
506
|
-
} catch (err) {
|
|
507
|
-
console.log(` global wrapper rewrite skipped: ${err.message}`);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// 清理历史残留的 .old.* 备份文件(旧进程已退出则删除,仍占用则静默忽略)
|
|
512
|
-
function cleanOldBinaries(binDir) {
|
|
513
|
-
try {
|
|
514
|
-
const files = fs.readdirSync(binDir);
|
|
515
|
-
for (const file of files) {
|
|
516
|
-
if (file.includes(".old.")) {
|
|
517
|
-
try {
|
|
518
|
-
fs.unlinkSync(join(binDir, file));
|
|
519
|
-
} catch {
|
|
520
|
-
// 进程可能仍持有文件句柄,忽略
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
} catch {
|
|
525
|
-
// 忽略目录读取错误
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// 安全替换二进制文件,支持 Windows 进程运行中热替换(文件锁退避)
|
|
530
|
-
function replaceBinary(extractedPath, finalPath) {
|
|
531
|
-
if (!fs.existsSync(finalPath)) {
|
|
532
|
-
fs.renameSync(extractedPath, finalPath);
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// 1. 尝试常规删除(未运行或 POSIX 平台)
|
|
537
|
-
try {
|
|
538
|
-
fs.unlinkSync(finalPath);
|
|
539
|
-
fs.renameSync(extractedPath, finalPath);
|
|
540
|
-
return;
|
|
541
|
-
} catch (err) {
|
|
542
|
-
// 若非文件占用错误(EBUSY / EPERM / EACCES),直接抛出
|
|
543
|
-
if (err.code !== "EBUSY" && err.code !== "EPERM" && err.code !== "EACCES") {
|
|
544
|
-
throw err;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// 2. Windows 文件锁退避:运行中 exe 无法 unlink,但允许在同目录下重命名
|
|
549
|
-
const oldPath = `${finalPath}.old.${Date.now()}`;
|
|
550
|
-
try {
|
|
551
|
-
fs.renameSync(finalPath, oldPath);
|
|
552
|
-
fs.renameSync(extractedPath, finalPath);
|
|
553
|
-
console.log(` (Note: Replaced running binary, old binary renamed to ${basename(oldPath)})`);
|
|
554
|
-
} catch (err) {
|
|
555
|
-
throw new Error(`无法替换正在运行的二进制文件 ${basename(finalPath)}: ${err.message}`);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// 3. 尝试清理已释放的历史旧二进制备份
|
|
559
|
-
cleanOldBinaries(dirname(finalPath));
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
async function main() {
|
|
563
|
-
const key = getPlatformKey();
|
|
564
|
-
const platform = PLATFORMS[key];
|
|
565
|
-
const fileName = `cc-code-${platform.suffix}.${platform.ext}`;
|
|
566
|
-
const url = `${BASE_URL}/${fileName}`;
|
|
567
|
-
const binDir = join(__dirname, "bin");
|
|
568
|
-
|
|
569
|
-
if (!existsSync(binDir)) {
|
|
570
|
-
mkdirSync(binDir, { recursive: true });
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
console.log(`Downloading cc-code ${VERSION} for ${platform.os}-${platform.arch}...`);
|
|
574
|
-
console.log(` URL: ${url}`);
|
|
575
|
-
|
|
576
|
-
const buffer = await download(url);
|
|
577
|
-
|
|
578
|
-
// #290:校验二进制完整性(fail-closed:缺失/不匹配则拒绝安装)
|
|
579
|
-
await verifyChecksum(buffer, fileName);
|
|
580
|
-
|
|
581
|
-
if (platform.ext === "tar.gz") {
|
|
582
|
-
extractTarGz(buffer, binDir);
|
|
583
|
-
} else {
|
|
584
|
-
extractZip(buffer, binDir);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
const extractedName = platform.os === "win32"
|
|
588
|
-
? `cc-code-${platform.suffix}.exe`
|
|
589
|
-
: `cc-code-${platform.suffix}`;
|
|
590
|
-
const finalName = platform.os === "win32" ? "cc-code.exe" : "cc-code-bin";
|
|
591
|
-
const extractedPath = join(binDir, extractedName);
|
|
592
|
-
const finalPath = join(binDir, finalName);
|
|
593
|
-
|
|
594
|
-
if (existsSync(extractedPath)) {
|
|
595
|
-
replaceBinary(extractedPath, finalPath);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
if (platform.os !== "win32") {
|
|
599
|
-
chmodSync(finalPath, 0o755);
|
|
600
|
-
const wrapperPath = join(__dirname, "bin", "cc-code");
|
|
601
|
-
if (existsSync(wrapperPath)) chmodSync(wrapperPath, 0o755);
|
|
602
|
-
} else {
|
|
603
|
-
// Generate Windows batch wrapper so npm's cc-code.cmd/ps1 can invoke it
|
|
604
|
-
const binDirPath = join(__dirname, "bin");
|
|
605
|
-
writeFileSync(join(binDirPath, "cc-code.cmd"), `@echo off\r\n"%~dp0cc-code.exe" %*\r\n`);
|
|
606
|
-
writeFileSync(join(binDirPath, "cc-code.ps1"), `$basedir = Split-Path $MyInvocation.MyCommand.Definition -Parent\r\n& "$basedir\\cc-code.exe" @args\r\nexit $LASTEXITCODE\r\n`);
|
|
607
|
-
|
|
608
|
-
// #410:npm 全局入口(<prefix>/cc-code.cmd|.ps1)由 npm 依据 package.json
|
|
609
|
-
// 的 bin(指向 node 脚本 bin/cc-code)自动生成,会经 node 以 execFileSync
|
|
610
|
-
// 拉起 exe。Ctrl+C 的 CTRL_C_EVENT 广播给整个控制台进程组时,node 父进程
|
|
611
|
-
// 无 handler 被终止,连带拖死 cc-code.exe(其 SetConsoleCtrlHandler 拦截
|
|
612
|
-
// 因此失效),表现为任意状态按 Ctrl+C 直接退出 TUI。这里把全局入口改写为
|
|
613
|
-
// 直起 exe,去掉 node 中间层,进程组内只剩 cc-code.exe,handler 正常生效。
|
|
614
|
-
overwriteNpmGlobalWrapper(binDirPath);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
console.log(`cc-code ${VERSION} installed successfully.`);
|
|
618
|
-
|
|
619
|
-
// 下载 ripgrep 预编译二进制(增强 Grep/Glob 性能,失败不阻塞安装)
|
|
620
|
-
await downloadRipgrep(platform, binDir);
|
|
621
|
-
|
|
622
|
-
// #290:密钥迁移需显式确认,不再静默复制
|
|
623
|
-
const migrated = await maybeMigrateFromClaudeCode();
|
|
624
|
-
|
|
625
|
-
// 让 CC 与 cc-code 共用同一份项目记忆目录(避免记忆写散到两侧)
|
|
626
|
-
linkProjectMemories();
|
|
627
|
-
|
|
628
|
-
if (!migrated) {
|
|
629
|
-
console.log("");
|
|
630
|
-
console.log("─── Quick Start ───");
|
|
631
|
-
console.log("");
|
|
632
|
-
console.log(" Set your API key (pick one):");
|
|
633
|
-
console.log("");
|
|
634
|
-
console.log(" # DeepSeek");
|
|
635
|
-
console.log(" export OPENAI_API_KEY=sk-xxx");
|
|
636
|
-
console.log(" export OPENAI_BASE_URL=https://api.deepseek.com/v1");
|
|
637
|
-
console.log(" export OPENAI_MODEL=deepseek-chat");
|
|
638
|
-
console.log("");
|
|
639
|
-
console.log(" # Anthropic");
|
|
640
|
-
console.log(" export ANTHROPIC_API_KEY=sk-ant-xxx");
|
|
641
|
-
console.log("");
|
|
642
|
-
console.log(" Or create config file:");
|
|
643
|
-
console.log("");
|
|
644
|
-
console.log(" mkdir -p ~/.cc-code");
|
|
645
|
-
console.log(' cat > ~/.cc-code/settings.json << \'EOF\'');
|
|
646
|
-
console.log(" {");
|
|
647
|
-
console.log(' "config": {');
|
|
648
|
-
console.log(' "providers": [');
|
|
649
|
-
console.log(" {");
|
|
650
|
-
console.log(' "type": "openai",');
|
|
651
|
-
console.log(' "apiKey": "sk-xxx",');
|
|
652
|
-
console.log(' "baseUrl": "https://api.deepseek.com/v1",');
|
|
653
|
-
console.log(' "models": { "sonnet": "deepseek-chat" }');
|
|
654
|
-
console.log(" }");
|
|
655
|
-
console.log(" ]");
|
|
656
|
-
console.log(" }");
|
|
657
|
-
console.log(" }");
|
|
658
|
-
console.log(" EOF");
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
console.log("");
|
|
662
|
-
console.log(" Launch: cc-code");
|
|
663
|
-
console.log(" Docs: https://github.com/cc-claws/cc-code");
|
|
664
|
-
console.log("");
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
if (require.main === module) {
|
|
668
|
-
main().catch((err) => {
|
|
669
|
-
console.error("Failed to install cc-code:", err.message);
|
|
670
|
-
process.exit(1);
|
|
671
|
-
});
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
module.exports = {
|
|
675
|
-
linkProjectMemories,
|
|
676
|
-
migrateFromClaudeCode,
|
|
677
|
-
maybeMigrateFromClaudeCode,
|
|
678
|
-
wouldCopyKeys,
|
|
679
|
-
verifyChecksum,
|
|
680
|
-
replaceBinary,
|
|
681
|
-
cleanOldBinaries,
|
|
682
|
-
extractRgTarGz,
|
|
683
|
-
};
|