@cc-claw/code 0.6.122 → 0.6.124
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 +63 -5
- package/install.test.js +105 -0
- package/package.json +1 -1
package/install.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const {
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const { createWriteStream, mkdirSync, chmodSync, existsSync, renameSync, unlinkSync, writeFileSync, readdirSync } = fs;
|
|
5
|
+
const { join, basename, dirname } = require("path");
|
|
5
6
|
const { execSync } = require("child_process");
|
|
6
7
|
const crypto = require("crypto");
|
|
7
8
|
|
|
@@ -432,6 +433,57 @@ function overwriteNpmGlobalWrapper(binDirPath) {
|
|
|
432
433
|
}
|
|
433
434
|
}
|
|
434
435
|
|
|
436
|
+
// 清理历史残留的 .old.* 备份文件(旧进程已退出则删除,仍占用则静默忽略)
|
|
437
|
+
function cleanOldBinaries(binDir) {
|
|
438
|
+
try {
|
|
439
|
+
const files = fs.readdirSync(binDir);
|
|
440
|
+
for (const file of files) {
|
|
441
|
+
if (file.includes(".old.")) {
|
|
442
|
+
try {
|
|
443
|
+
fs.unlinkSync(join(binDir, file));
|
|
444
|
+
} catch {
|
|
445
|
+
// 进程可能仍持有文件句柄,忽略
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
} catch {
|
|
450
|
+
// 忽略目录读取错误
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// 安全替换二进制文件,支持 Windows 进程运行中热替换(文件锁退避)
|
|
455
|
+
function replaceBinary(extractedPath, finalPath) {
|
|
456
|
+
if (!fs.existsSync(finalPath)) {
|
|
457
|
+
fs.renameSync(extractedPath, finalPath);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// 1. 尝试常规删除(未运行或 POSIX 平台)
|
|
462
|
+
try {
|
|
463
|
+
fs.unlinkSync(finalPath);
|
|
464
|
+
fs.renameSync(extractedPath, finalPath);
|
|
465
|
+
return;
|
|
466
|
+
} catch (err) {
|
|
467
|
+
// 若非文件占用错误(EBUSY / EPERM / EACCES),直接抛出
|
|
468
|
+
if (err.code !== "EBUSY" && err.code !== "EPERM" && err.code !== "EACCES") {
|
|
469
|
+
throw err;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// 2. Windows 文件锁退避:运行中 exe 无法 unlink,但允许在同目录下重命名
|
|
474
|
+
const oldPath = `${finalPath}.old.${Date.now()}`;
|
|
475
|
+
try {
|
|
476
|
+
fs.renameSync(finalPath, oldPath);
|
|
477
|
+
fs.renameSync(extractedPath, finalPath);
|
|
478
|
+
console.log(` (Note: Replaced running binary, old binary renamed to ${basename(oldPath)})`);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
throw new Error(`无法替换正在运行的二进制文件 ${basename(finalPath)}: ${err.message}`);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// 3. 尝试清理已释放的历史旧二进制备份
|
|
484
|
+
cleanOldBinaries(dirname(finalPath));
|
|
485
|
+
}
|
|
486
|
+
|
|
435
487
|
async function main() {
|
|
436
488
|
const key = getPlatformKey();
|
|
437
489
|
const platform = PLATFORMS[key];
|
|
@@ -465,8 +517,7 @@ async function main() {
|
|
|
465
517
|
const finalPath = join(binDir, finalName);
|
|
466
518
|
|
|
467
519
|
if (existsSync(extractedPath)) {
|
|
468
|
-
|
|
469
|
-
renameSync(extractedPath, finalPath);
|
|
520
|
+
replaceBinary(extractedPath, finalPath);
|
|
470
521
|
}
|
|
471
522
|
|
|
472
523
|
if (platform.os !== "win32") {
|
|
@@ -542,4 +593,11 @@ if (require.main === module) {
|
|
|
542
593
|
});
|
|
543
594
|
}
|
|
544
595
|
|
|
545
|
-
module.exports = {
|
|
596
|
+
module.exports = {
|
|
597
|
+
migrateFromClaudeCode,
|
|
598
|
+
maybeMigrateFromClaudeCode,
|
|
599
|
+
wouldCopyKeys,
|
|
600
|
+
verifyChecksum,
|
|
601
|
+
replaceBinary,
|
|
602
|
+
cleanOldBinaries,
|
|
603
|
+
};
|
package/install.test.js
CHANGED
|
@@ -237,5 +237,110 @@ test("maybeMigrateFromClaudeCode still backfills without consent", async (home)
|
|
|
237
237
|
if (cfg.config.providers[0].models.fable !== "claude-fable-5") throw new Error("expected fable backfilled");
|
|
238
238
|
});
|
|
239
239
|
|
|
240
|
+
// ─────────────────────────────────────────────
|
|
241
|
+
// Windows 进程文件锁热替换与旧备份清理
|
|
242
|
+
// ─────────────────────────────────────────────
|
|
243
|
+
const { replaceBinary, cleanOldBinaries } = require("./install");
|
|
244
|
+
|
|
245
|
+
test("replaceBinary moves extracted file to finalPath when finalPath does not exist", (home) => {
|
|
246
|
+
const binDir = join(home, "bin");
|
|
247
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
248
|
+
const extracted = join(binDir, "cc-code-new.exe");
|
|
249
|
+
const finalPath = join(binDir, "cc-code.exe");
|
|
250
|
+
fs.writeFileSync(extracted, "new-binary-content");
|
|
251
|
+
|
|
252
|
+
replaceBinary(extracted, finalPath);
|
|
253
|
+
|
|
254
|
+
if (!fs.existsSync(finalPath)) throw new Error("finalPath must exist");
|
|
255
|
+
if (fs.existsSync(extracted)) throw new Error("extractedPath must have been moved");
|
|
256
|
+
if (fs.readFileSync(finalPath, "utf-8") !== "new-binary-content") throw new Error("content mismatch");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("replaceBinary overwrites finalPath normally when not locked", (home) => {
|
|
260
|
+
const binDir = join(home, "bin");
|
|
261
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
262
|
+
const extracted = join(binDir, "cc-code-new.exe");
|
|
263
|
+
const finalPath = join(binDir, "cc-code.exe");
|
|
264
|
+
fs.writeFileSync(finalPath, "old-binary-content");
|
|
265
|
+
fs.writeFileSync(extracted, "new-binary-content");
|
|
266
|
+
|
|
267
|
+
replaceBinary(extracted, finalPath);
|
|
268
|
+
|
|
269
|
+
if (!fs.existsSync(finalPath)) throw new Error("finalPath must exist");
|
|
270
|
+
if (fs.readFileSync(finalPath, "utf-8") !== "new-binary-content") throw new Error("content mismatch");
|
|
271
|
+
// 正常未锁定删除时,不应产生 .old 备份文件
|
|
272
|
+
const files = fs.readdirSync(binDir);
|
|
273
|
+
if (files.some((f) => f.includes(".old."))) throw new Error("should not create .old file when not locked");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("replaceBinary falls back to renaming old file when finalPath is locked (EBUSY/EPERM)", (home) => {
|
|
277
|
+
const binDir = join(home, "bin");
|
|
278
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
279
|
+
const extracted = join(binDir, "cc-code-new.exe");
|
|
280
|
+
const finalPath = join(binDir, "cc-code.exe");
|
|
281
|
+
fs.writeFileSync(finalPath, "running-binary-content");
|
|
282
|
+
fs.writeFileSync(extracted, "new-binary-content");
|
|
283
|
+
|
|
284
|
+
// 模拟 Windows 运行中 exe 无法 unlinkSync(抛 EPERM/EBUSY,包括重命名后的 .old 文件仍然被进程句柄占用)
|
|
285
|
+
const originalUnlink = fs.unlinkSync;
|
|
286
|
+
try {
|
|
287
|
+
fs.unlinkSync = (target) => {
|
|
288
|
+
if (target === finalPath || target.includes("cc-code.exe.old.")) {
|
|
289
|
+
const err = new Error("resource busy or locked");
|
|
290
|
+
err.code = "EBUSY";
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
return originalUnlink(target);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
replaceBinary(extracted, finalPath);
|
|
297
|
+
} finally {
|
|
298
|
+
fs.unlinkSync = originalUnlink;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 验证:新文件成功放至 finalPath
|
|
302
|
+
if (!fs.existsSync(finalPath)) throw new Error("finalPath must exist");
|
|
303
|
+
if (fs.readFileSync(finalPath, "utf-8") !== "new-binary-content") throw new Error("new content mismatch");
|
|
304
|
+
|
|
305
|
+
// 验证:旧文件被成功重命名为 .old.* 备份
|
|
306
|
+
const files = fs.readdirSync(binDir);
|
|
307
|
+
const oldFiles = files.filter((f) => f.startsWith("cc-code.exe.old."));
|
|
308
|
+
if (oldFiles.length !== 1) throw new Error(`expected 1 old backup file, found ${oldFiles.length}`);
|
|
309
|
+
if (fs.readFileSync(join(binDir, oldFiles[0]), "utf-8") !== "running-binary-content") {
|
|
310
|
+
throw new Error("old backup content mismatch");
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("cleanOldBinaries unlinks released .old files and ignores still-locked ones", (home) => {
|
|
315
|
+
const binDir = join(home, "bin");
|
|
316
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
317
|
+
const activeOld = join(binDir, "cc-code.exe.old.1001");
|
|
318
|
+
const inactiveOld = join(binDir, "cc-code.exe.old.1002");
|
|
319
|
+
const normalFile = join(binDir, "cc-code.exe");
|
|
320
|
+
fs.writeFileSync(activeOld, "active");
|
|
321
|
+
fs.writeFileSync(inactiveOld, "inactive");
|
|
322
|
+
fs.writeFileSync(normalFile, "normal");
|
|
323
|
+
|
|
324
|
+
const originalUnlink = fs.unlinkSync;
|
|
325
|
+
try {
|
|
326
|
+
fs.unlinkSync = (target) => {
|
|
327
|
+
if (target === activeOld) {
|
|
328
|
+
const err = new Error("file is locked");
|
|
329
|
+
err.code = "EBUSY";
|
|
330
|
+
throw err;
|
|
331
|
+
}
|
|
332
|
+
return originalUnlink(target);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
cleanOldBinaries(binDir);
|
|
336
|
+
} finally {
|
|
337
|
+
fs.unlinkSync = originalUnlink;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!fs.existsSync(normalFile)) throw new Error("normalFile must not be deleted");
|
|
341
|
+
if (!fs.existsSync(activeOld)) throw new Error("activeOld should be kept because it is locked");
|
|
342
|
+
if (fs.existsSync(inactiveOld)) throw new Error("inactiveOld should be unlinked");
|
|
343
|
+
});
|
|
344
|
+
|
|
240
345
|
// 由 runTests() 顺序执行(支持 async 用例)
|
|
241
346
|
runTests();
|