@cc-claw/code 0.6.178 → 0.6.180

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.test.js DELETED
@@ -1,479 +0,0 @@
1
- const { migrateFromClaudeCode, extractRgTarGz } = require("./install");
2
-
3
- const fs = require("fs");
4
- const { join } = require("path");
5
- const os = require("os");
6
- const { execSync } = require("child_process");
7
- const { dirname } = require("path");
8
-
9
- function makeTempDir() {
10
- return fs.mkdtempSync(join(os.tmpdir(), "cc-code-install-test-"));
11
- }
12
-
13
- function cleanup(dir) {
14
- fs.rmSync(dir, { recursive: true, force: true });
15
- }
16
-
17
- function writeClaudeSettings(home, content) {
18
- const claudeDir = join(home, ".claude");
19
- fs.mkdirSync(claudeDir, { recursive: true });
20
- fs.writeFileSync(join(claudeDir, "settings.json"), JSON.stringify(content, null, 2));
21
- }
22
-
23
- function readCcCodeSettings(home) {
24
- return JSON.parse(fs.readFileSync(join(home, ".cc-code", "settings.json"), "utf-8"));
25
- }
26
-
27
- const __tests = [];
28
- function test(name, fn) {
29
- __tests.push([name, fn]);
30
- }
31
-
32
- async function runTests() {
33
- for (const [name, fn] of __tests) {
34
- const home = makeTempDir();
35
- try {
36
- await fn(home);
37
- console.log(` ✓ ${name}`);
38
- } catch (e) {
39
- console.error(` ✗ ${name}`);
40
- console.error(e.message);
41
- process.exitCode = 1;
42
- } finally {
43
- cleanup(home);
44
- }
45
- }
46
- console.log("");
47
- if (process.exitCode) {
48
- console.log("Some tests failed.");
49
- } else {
50
- console.log("All tests passed.");
51
- }
52
- }
53
-
54
- console.log("npm/install.js tests");
55
-
56
- test("migrateFromClaudeCode returns false when no claude settings exist", (home) => {
57
- const result = migrateFromClaudeCode(home);
58
- if (result !== false) throw new Error("expected false");
59
- });
60
-
61
- test("migrateFromClaudeCode backfills missing aliases when cc-code settings already exist", (home) => {
62
- const ccCodeDir = join(home, ".cc-code");
63
- fs.mkdirSync(ccCodeDir, { recursive: true });
64
- // 已有配置:anthropic provider,缺少 fable 别名,且 opus 是用户手改过的值
65
- const existing = {
66
- config: {
67
- active_alias: "haiku",
68
- providers: [
69
- {
70
- id: "anthropic",
71
- type: "anthropic",
72
- apiKey: "sk-user-own-key",
73
- models: { opus: "claude-opus-user-custom", sonnet: "claude-sonnet-4-6", haiku: "claude-haiku-4-5" },
74
- },
75
- ],
76
- },
77
- };
78
- fs.writeFileSync(join(ccCodeDir, "settings.json"), JSON.stringify(existing, null, 2));
79
- writeClaudeSettings(home, {
80
- env: {
81
- ANTHROPIC_API_KEY: "sk-ant",
82
- ANTHROPIC_DEFAULT_OPUS_MODEL: "claude-opus-4-7",
83
- ANTHROPIC_DEFAULT_FABLE_MODEL: "claude-fable-5",
84
- },
85
- });
86
- const result = migrateFromClaudeCode(home);
87
- if (result !== true) throw new Error("expected true");
88
- const cfg = readCcCodeSettings(home);
89
- const p = cfg.config.providers[0];
90
- // 缺失的 fable 被补上
91
- if (p.models.fable !== "claude-fable-5") throw new Error("expected fable backfilled");
92
- // 用户已有的 opus 值不被覆盖
93
- if (p.models.opus !== "claude-opus-user-custom") throw new Error("must not overwrite existing opus");
94
- // 用户已有值不被 env 覆盖
95
- if (p.apiKey !== "sk-user-own-key") throw new Error("must not overwrite user apiKey");
96
- // active_alias 保持原样
97
- if (cfg.config.active_alias !== "haiku") throw new Error("must not change active_alias");
98
- });
99
-
100
- test("migrateFromClaudeCode backfill leaves unknown provider ids untouched", (home) => {
101
- const ccCodeDir = join(home, ".cc-code");
102
- fs.mkdirSync(ccCodeDir, { recursive: true });
103
- const existing = {
104
- config: {
105
- providers: [{ id: "my-custom-id", type: "anthropic", apiKey: "sk-x", models: {} }],
106
- },
107
- };
108
- fs.writeFileSync(join(ccCodeDir, "settings.json"), JSON.stringify(existing));
109
- writeClaudeSettings(home, { env: { ANTHROPIC_API_KEY: "sk-ant", ANTHROPIC_DEFAULT_FABLE_MODEL: "claude-fable-5" } });
110
- const result = migrateFromClaudeCode(home);
111
- if (result !== true) throw new Error("expected true");
112
- const cfg = readCcCodeSettings(home);
113
- if (cfg.config.providers.length !== 1) throw new Error("must not add providers");
114
- if (cfg.config.providers[0].models.fable !== undefined) throw new Error("must not touch unknown provider id");
115
- });
116
-
117
- test("migrateFromClaudeCode returns true when cc-code exists but no claude settings", (home) => {
118
- const ccCodeDir = join(home, ".cc-code");
119
- fs.mkdirSync(ccCodeDir, { recursive: true });
120
- fs.writeFileSync(join(ccCodeDir, "settings.json"), JSON.stringify({ config: {} }));
121
- const result = migrateFromClaudeCode(home);
122
- if (result !== true) throw new Error("expected true (has cc-code, no quick start)");
123
- });
124
-
125
- test("migrateFromClaudeCode produces camelCase provider config for Anthropic", (home) => {
126
- writeClaudeSettings(home, {
127
- env: {
128
- ANTHROPIC_API_KEY: "sk-ant-xxx",
129
- ANTHROPIC_BASE_URL: "https://api.anthropic.com",
130
- ANTHROPIC_DEFAULT_OPUS_MODEL: "claude-opus-4-7",
131
- ANTHROPIC_DEFAULT_SONNET_MODEL: "claude-sonnet-4-6",
132
- ANTHROPIC_DEFAULT_HAIKU_MODEL: "claude-haiku-4-5",
133
- },
134
- });
135
- const result = migrateFromClaudeCode(home);
136
- if (result !== true) throw new Error("expected true");
137
- const cfg = readCcCodeSettings(home);
138
- if (cfg.config.active_alias !== "opus") throw new Error(`expected active_alias opus, got ${cfg.config.active_alias}`);
139
- if (cfg.config.active_provider_id !== "anthropic") throw new Error("expected active_provider_id anthropic");
140
- const p = cfg.config.providers[0];
141
- if (p.id !== "anthropic") throw new Error("expected provider id anthropic");
142
- if (p.type !== "anthropic") throw new Error("expected provider type anthropic");
143
- if (p.apiKey !== "sk-ant-xxx") throw new Error("expected apiKey");
144
- if (p.baseUrl !== "https://api.anthropic.com") throw new Error("expected baseUrl");
145
- if (p.provider_type !== undefined) throw new Error("provider_type (snake_case) should not exist");
146
- if (p.api_key !== undefined) throw new Error("api_key (snake_case) should not exist");
147
- if (p.base_url !== undefined) throw new Error("base_url (snake_case) should not exist");
148
- if (p.models.opus !== "claude-opus-4-7") throw new Error("expected models.opus");
149
- if (p.models.sonnet !== "claude-sonnet-4-6") throw new Error("expected models.sonnet");
150
- if (p.models.haiku !== "claude-haiku-4-5") throw new Error("expected models.haiku");
151
- });
152
-
153
- test("migrateFromClaudeCode produces camelCase provider config for OpenAI", (home) => {
154
- writeClaudeSettings(home, {
155
- env: {
156
- OPENAI_API_KEY: "sk-openai-xxx",
157
- OPENAI_BASE_URL: "https://api.deepseek.com/v1",
158
- OPENAI_MODEL: "deepseek-chat",
159
- },
160
- });
161
- const result = migrateFromClaudeCode(home);
162
- if (result !== true) throw new Error("expected true");
163
- const cfg = readCcCodeSettings(home);
164
- if (cfg.config.active_alias !== "sonnet") throw new Error(`expected active_alias sonnet, got ${cfg.config.active_alias}`);
165
- if (cfg.config.active_provider_id !== "openai") throw new Error("expected active_provider_id openai");
166
- const p = cfg.config.providers[0];
167
- if (p.id !== "openai") throw new Error("expected provider id openai");
168
- if (p.type !== "openai") throw new Error("expected provider type openai");
169
- if (p.apiKey !== "sk-openai-xxx") throw new Error("expected apiKey");
170
- if (p.baseUrl !== "https://api.deepseek.com/v1") throw new Error("expected baseUrl");
171
- if (p.models.sonnet !== "deepseek-chat") throw new Error("expected models.sonnet");
172
- });
173
-
174
- test("migrateFromClaudeCode supports CODEX_API_KEY fallback", (home) => {
175
- writeClaudeSettings(home, {
176
- env: {
177
- CODEX_API_KEY: "sk-codex-xxx",
178
- },
179
- });
180
- const result = migrateFromClaudeCode(home);
181
- if (result !== true) throw new Error("expected true");
182
- const cfg = readCcCodeSettings(home);
183
- if (cfg.config.providers[0].apiKey !== "sk-codex-xxx") throw new Error("expected CODEX_API_KEY to be used");
184
- });
185
-
186
- // ─────────────────────────────────────────────
187
- // #290:checksum 校验 + 密钥迁移确认制
188
- // ─────────────────────────────────────────────
189
- const { maybeMigrateFromClaudeCode, wouldCopyKeys } = require("./install");
190
-
191
- test("wouldCopyKeys is false when cc-code settings already exist", (home) => {
192
- const ccCodeDir = join(home, ".cc-code");
193
- fs.mkdirSync(ccCodeDir, { recursive: true });
194
- fs.writeFileSync(join(ccCodeDir, "settings.json"), JSON.stringify({ config: {} }));
195
- writeClaudeSettings(home, { env: { ANTHROPIC_API_KEY: "sk-ant" } });
196
- if (wouldCopyKeys(home) !== false) throw new Error("expected false (backfill only)");
197
- });
198
-
199
- test("wouldCopyKeys is true on fresh install with claude keys", (home) => {
200
- writeClaudeSettings(home, { env: { ANTHROPIC_API_KEY: "sk-ant" } });
201
- if (wouldCopyKeys(home) !== true) throw new Error("expected true");
202
- });
203
-
204
- test("wouldCopyKeys is false without claude settings", (home) => {
205
- if (wouldCopyKeys(home) !== false) throw new Error("expected false");
206
- });
207
-
208
- test("maybeMigrateFromClaudeCode skips key copy in non-TTY without prompt", async (home) => {
209
- writeClaudeSettings(home, { env: { ANTHROPIC_API_KEY: "sk-ant-must-not-copy" } });
210
- const result = await maybeMigrateFromClaudeCode(home);
211
- if (result !== false) throw new Error("expected false (skipped)");
212
- if (fs.existsSync(join(home, ".cc-code", "settings.json"))) {
213
- throw new Error("must not create settings.json without consent");
214
- }
215
- });
216
-
217
- test("maybeMigrateFromClaudeCode honors CC_CODE_NO_MIGRATE=1", async (home) => {
218
- writeClaudeSettings(home, { env: { ANTHROPIC_API_KEY: "sk-ant" } });
219
- process.env.CC_CODE_NO_MIGRATE = "1";
220
- try {
221
- const result = await maybeMigrateFromClaudeCode(home);
222
- if (result !== false) throw new Error("expected false (skipped by env)");
223
- } finally {
224
- delete process.env.CC_CODE_NO_MIGRATE;
225
- }
226
- });
227
-
228
- test("maybeMigrateFromClaudeCode still backfills without consent", async (home) => {
229
- const ccCodeDir = join(home, ".cc-code");
230
- fs.mkdirSync(ccCodeDir, { recursive: true });
231
- fs.writeFileSync(
232
- join(ccCodeDir, "settings.json"),
233
- JSON.stringify({ config: { providers: [{ id: "anthropic", type: "anthropic", apiKey: "sk-x", models: {} }] } })
234
- );
235
- writeClaudeSettings(home, { env: { ANTHROPIC_API_KEY: "sk-ant", ANTHROPIC_DEFAULT_FABLE_MODEL: "claude-fable-5" } });
236
- const result = await maybeMigrateFromClaudeCode(home);
237
- if (result !== true) throw new Error("expected true (backfill needs no consent)");
238
- const cfg = readCcCodeSettings(home);
239
- if (cfg.config.providers[0].models.fable !== "claude-fable-5") throw new Error("expected fable backfilled");
240
- });
241
-
242
- // ─────────────────────────────────────────────
243
- // Windows 进程文件锁热替换与旧备份清理
244
- // ─────────────────────────────────────────────
245
- const { replaceBinary, cleanOldBinaries } = require("./install");
246
-
247
- test("replaceBinary moves extracted file to finalPath when finalPath does not exist", (home) => {
248
- const binDir = join(home, "bin");
249
- fs.mkdirSync(binDir, { recursive: true });
250
- const extracted = join(binDir, "cc-code-new.exe");
251
- const finalPath = join(binDir, "cc-code.exe");
252
- fs.writeFileSync(extracted, "new-binary-content");
253
-
254
- replaceBinary(extracted, finalPath);
255
-
256
- if (!fs.existsSync(finalPath)) throw new Error("finalPath must exist");
257
- if (fs.existsSync(extracted)) throw new Error("extractedPath must have been moved");
258
- if (fs.readFileSync(finalPath, "utf-8") !== "new-binary-content") throw new Error("content mismatch");
259
- });
260
-
261
- test("replaceBinary overwrites finalPath normally when not locked", (home) => {
262
- const binDir = join(home, "bin");
263
- fs.mkdirSync(binDir, { recursive: true });
264
- const extracted = join(binDir, "cc-code-new.exe");
265
- const finalPath = join(binDir, "cc-code.exe");
266
- fs.writeFileSync(finalPath, "old-binary-content");
267
- fs.writeFileSync(extracted, "new-binary-content");
268
-
269
- replaceBinary(extracted, finalPath);
270
-
271
- if (!fs.existsSync(finalPath)) throw new Error("finalPath must exist");
272
- if (fs.readFileSync(finalPath, "utf-8") !== "new-binary-content") throw new Error("content mismatch");
273
- // 正常未锁定删除时,不应产生 .old 备份文件
274
- const files = fs.readdirSync(binDir);
275
- if (files.some((f) => f.includes(".old."))) throw new Error("should not create .old file when not locked");
276
- });
277
-
278
- test("replaceBinary falls back to renaming old file when finalPath is locked (EBUSY/EPERM)", (home) => {
279
- const binDir = join(home, "bin");
280
- fs.mkdirSync(binDir, { recursive: true });
281
- const extracted = join(binDir, "cc-code-new.exe");
282
- const finalPath = join(binDir, "cc-code.exe");
283
- fs.writeFileSync(finalPath, "running-binary-content");
284
- fs.writeFileSync(extracted, "new-binary-content");
285
-
286
- // 模拟 Windows 运行中 exe 无法 unlinkSync(抛 EPERM/EBUSY,包括重命名后的 .old 文件仍然被进程句柄占用)
287
- const originalUnlink = fs.unlinkSync;
288
- try {
289
- fs.unlinkSync = (target) => {
290
- if (target === finalPath || target.includes("cc-code.exe.old.")) {
291
- const err = new Error("resource busy or locked");
292
- err.code = "EBUSY";
293
- throw err;
294
- }
295
- return originalUnlink(target);
296
- };
297
-
298
- replaceBinary(extracted, finalPath);
299
- } finally {
300
- fs.unlinkSync = originalUnlink;
301
- }
302
-
303
- // 验证:新文件成功放至 finalPath
304
- if (!fs.existsSync(finalPath)) throw new Error("finalPath must exist");
305
- if (fs.readFileSync(finalPath, "utf-8") !== "new-binary-content") throw new Error("new content mismatch");
306
-
307
- // 验证:旧文件被成功重命名为 .old.* 备份
308
- const files = fs.readdirSync(binDir);
309
- const oldFiles = files.filter((f) => f.startsWith("cc-code.exe.old."));
310
- if (oldFiles.length !== 1) throw new Error(`expected 1 old backup file, found ${oldFiles.length}`);
311
- if (fs.readFileSync(join(binDir, oldFiles[0]), "utf-8") !== "running-binary-content") {
312
- throw new Error("old backup content mismatch");
313
- }
314
- });
315
-
316
- test("cleanOldBinaries unlinks released .old files and ignores still-locked ones", (home) => {
317
- const binDir = join(home, "bin");
318
- fs.mkdirSync(binDir, { recursive: true });
319
- const activeOld = join(binDir, "cc-code.exe.old.1001");
320
- const inactiveOld = join(binDir, "cc-code.exe.old.1002");
321
- const normalFile = join(binDir, "cc-code.exe");
322
- fs.writeFileSync(activeOld, "active");
323
- fs.writeFileSync(inactiveOld, "inactive");
324
- fs.writeFileSync(normalFile, "normal");
325
-
326
- const originalUnlink = fs.unlinkSync;
327
- try {
328
- fs.unlinkSync = (target) => {
329
- if (target === activeOld) {
330
- const err = new Error("file is locked");
331
- err.code = "EBUSY";
332
- throw err;
333
- }
334
- return originalUnlink(target);
335
- };
336
-
337
- cleanOldBinaries(binDir);
338
- } finally {
339
- fs.unlinkSync = originalUnlink;
340
- }
341
-
342
- if (!fs.existsSync(normalFile)) throw new Error("normalFile must not be deleted");
343
- if (!fs.existsSync(activeOld)) throw new Error("activeOld should be kept because it is locked");
344
- if (fs.existsSync(inactiveOld)) throw new Error("inactiveOld should be unlinked");
345
- });
346
-
347
- // --- linkProjectMemories ---
348
- const { linkProjectMemories } = require("./install");
349
-
350
- function writeMemoryDir(dir) {
351
- fs.mkdirSync(dir, { recursive: true });
352
- fs.writeFileSync(join(dir, "MEMORY.md"), "# Memory Index\n");
353
- }
354
-
355
- test("linkProjectMemories links Claude Code project memory into the cc-code side", (home) => {
356
- const claudeMem = join(home, ".claude", "projects", "D--code-demo", "memory");
357
- writeMemoryDir(claudeMem);
358
-
359
- const linked = linkProjectMemories(home);
360
- if (linked !== 1) throw new Error(`expected 1 link, got ${linked}`);
361
-
362
- const ccMem = join(home, ".cc-code", "projects", "D--code-demo", "memory");
363
- if (!fs.existsSync(join(ccMem, "MEMORY.md"))) throw new Error("memory not reachable via cc-code path");
364
-
365
- // 写穿透:经 cc-code 侧写入必须落到 CC 侧同一份
366
- fs.writeFileSync(join(ccMem, "user_role.md"), "shared");
367
- if (!fs.existsSync(join(claudeMem, "user_role.md"))) {
368
- throw new Error("cc-code path is a separate physical directory, not a link");
369
- }
370
- });
371
-
372
- test("linkProjectMemories leaves a cc-code-only project untouched", (home) => {
373
- const ccMem = join(home, ".cc-code", "projects", "D--code-demo", "memory");
374
- writeMemoryDir(ccMem);
375
-
376
- const linked = linkProjectMemories(home);
377
- if (linked !== 0) throw new Error(`expected 0 links, got ${linked}`);
378
- if (fs.existsSync(join(home, ".claude", "projects", "D--code-demo"))) {
379
- throw new Error("must not add anything under ~/.claude");
380
- }
381
- });
382
-
383
- test("linkProjectMemories never overwrites an existing project memory dir", (home) => {
384
- writeMemoryDir(join(home, ".claude", "projects", "D--code-demo", "memory"));
385
- const ccMem = join(home, ".cc-code", "projects", "D--code-demo", "memory");
386
- writeMemoryDir(ccMem);
387
- fs.writeFileSync(join(ccMem, "own.md"), "keep me");
388
-
389
- const linked = linkProjectMemories(home);
390
- if (linked !== 0) throw new Error(`expected 0 links, got ${linked}`);
391
- if (fs.readFileSync(join(ccMem, "own.md"), "utf-8") !== "keep me") {
392
- throw new Error("existing cc-code memory content was touched");
393
- }
394
- if (fs.lstatSync(ccMem).isSymbolicLink()) throw new Error("existing dir must not be replaced by a link");
395
- });
396
-
397
- test("linkProjectMemories is idempotent and no-ops without project memory", (home) => {
398
- if (linkProjectMemories(home) !== 0) throw new Error("expected 0 links on empty home");
399
-
400
- writeMemoryDir(join(home, ".claude", "projects", "D--code-demo", "memory"));
401
- if (linkProjectMemories(home) !== 1) throw new Error("first run should link 1");
402
- if (linkProjectMemories(home) !== 0) throw new Error("second run should link nothing");
403
- });
404
-
405
- // 由 runTests() 顺序执行(支持 async 用例)
406
- // --- extractRgTarGz(#620)---------------------------------------------------
407
- // 上游 ripgrep-prebuilt 的 tar.gz 把 rg 放在根目录,旧实现用
408
- // --strip-components=1 导致解不出,bin/rg 永不落地。
409
-
410
- function makeTarGz(dir, entries) {
411
- // entries: [{ name, content, mode }]
412
- const stage = fs.mkdtempSync(join(os.tmpdir(), "cc-code-rg-stage-"));
413
- for (const e of entries) {
414
- const target = join(stage, e.name);
415
- fs.mkdirSync(dirname(target), { recursive: true });
416
- fs.writeFileSync(target, e.content);
417
- if (e.mode) fs.chmodSync(target, e.mode);
418
- }
419
- const out = join(dir, "rg-fixture.tar.gz");
420
- // 用具体条目名打包(不用 "."),避免引入额外的 "./" 层级,
421
- // 与上游 ripgrep-prebuilt 资产的实际结构保持一致。
422
- const names = [...new Set(entries.map((e) => e.name.split("/")[0]))].join(" ");
423
- execSync(`tar -czf "${out}" -C "${stage}" ${names}`, { stdio: "ignore" });
424
- const buf = fs.readFileSync(out);
425
- fs.rmSync(out, { force: true });
426
- fs.rmSync(stage, { recursive: true, force: true });
427
- return buf;
428
- }
429
-
430
- test("extractRgTarGz extracts rg when it sits at archive root", (home) => {
431
- const dest = join(home, "bin");
432
- fs.mkdirSync(dest, { recursive: true });
433
- const buf = makeTarGz(home, [{ name: "rg", content: "#!/bin/sh\necho rg\n", mode: 0o755 }]);
434
- extractRgTarGz(buf, dest);
435
- const rg = join(dest, "rg");
436
- if (!fs.existsSync(rg)) throw new Error("rg was not extracted from root layout");
437
- if (fs.readFileSync(rg, "utf-8").indexOf("echo rg") === -1) {
438
- throw new Error("extracted rg content mismatch");
439
- }
440
- });
441
-
442
- test("extractRgTarGz extracts rg when nested in a subdirectory", (home) => {
443
- const dest = join(home, "bin");
444
- fs.mkdirSync(dest, { recursive: true });
445
- const buf = makeTarGz(home, [
446
- { name: "ripgrep-x.y.z/rg", content: "#!/bin/sh\necho nested\n", mode: 0o755 },
447
- ]);
448
- extractRgTarGz(buf, dest);
449
- const rg = join(dest, "rg");
450
- if (!fs.existsSync(rg)) throw new Error("rg was not extracted from nested layout");
451
- if (fs.readFileSync(rg, "utf-8").indexOf("echo nested") === -1) {
452
- throw new Error("extracted rg content mismatch");
453
- }
454
- });
455
-
456
- test("extractRgTarGz leaves no temp archive behind on failure", (home) => {
457
- const dest = join(home, "bin");
458
- fs.mkdirSync(dest, { recursive: true });
459
- let threw = false;
460
- try {
461
- extractRgTarGz(Buffer.from("not a gzip"), dest);
462
- } catch {
463
- threw = true;
464
- }
465
- if (!threw) throw new Error("corrupt archive should throw");
466
- const leftover = fs.readdirSync(dest).filter((f) => f.startsWith("rg-download"));
467
- if (leftover.length) throw new Error(`temp file leaked: ${leftover.join(", ")}`);
468
- });
469
-
470
- test("extractRgTarGz leaves no temp archive behind on success", (home) => {
471
- const dest = join(home, "bin");
472
- fs.mkdirSync(dest, { recursive: true });
473
- const buf = makeTarGz(home, [{ name: "rg", content: "#!/bin/sh\n", mode: 0o755 }]);
474
- extractRgTarGz(buf, dest);
475
- const leftover = fs.readdirSync(dest).filter((f) => f.startsWith("rg-download"));
476
- if (leftover.length) throw new Error(`temp file leaked: ${leftover.join(", ")}`);
477
- });
478
-
479
- runTests();