@liwenkai/deepcode 0.4.7 → 0.4.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.
Files changed (3) hide show
  1. package/bin/deepcode +54 -8
  2. package/install.mjs +56 -10
  3. package/package.json +1 -1
package/bin/deepcode CHANGED
@@ -9,15 +9,25 @@
9
9
  // instead of leaving a command pointing at nothing.
10
10
  'use strict';
11
11
 
12
- const { spawnSync } = require('node:child_process');
12
+ const { spawn } = require('node:child_process');
13
13
  const { existsSync } = require('node:fs');
14
- const { join } = require('node:path');
14
+ const { basename, join } = require('node:path');
15
+ const os = require('node:os');
15
16
 
16
17
  const binary = join(
17
18
  __dirname,
18
19
  process.platform === 'win32' ? 'deepcode.exe' : 'deepcode-bin',
19
20
  );
20
21
 
22
+ // The binary prints its own name in `--help`, `--version`, every usage line and
23
+ // every "Try `… --help`" error, and it reads that name from argv[0]. Spawned
24
+ // from here, argv[0] is `deepcode-bin` — the deliberately distinct download
25
+ // name, which is on nobody's PATH — so the first lines a new user reads told
26
+ // them to run a command that does not exist. This launcher is the only thing
27
+ // that knows the name npm actually linked, so it hands it over. `argv[1]` is
28
+ // this script's own path, which IS that name.
29
+ const invokedAs = basename(process.argv[1] || 'deepcode');
30
+
21
31
  if (!existsSync(binary)) {
22
32
  console.error('deepcode: native binary not found.');
23
33
  console.error('Reinstall without --ignore-scripts, or grab a prebuilt binary from');
@@ -25,12 +35,48 @@ if (!existsSync(binary)) {
25
35
  process.exit(1);
26
36
  }
27
37
 
28
- const result = spawnSync(binary, process.argv.slice(2), { stdio: 'inherit' });
38
+ // `spawn`, not `spawnSync`: a supervisor (or `timeout`, or a closed shell) that
39
+ // sends SIGTERM/SIGHUP to THIS launcher would, with spawnSync, kill only Node —
40
+ // the native binary is reparented to init and keeps running, orphaning its job
41
+ // process groups (dev servers, watchers). Async spawn lets us forward those
42
+ // signals so the binary runs its own graceful shutdown. (SIGINT/Ctrl-C already
43
+ // reaches the whole foreground group; forwarding it too is harmless — the binary
44
+ // guards against a double interrupt — and covers a supervisor that signals only
45
+ // this pid.)
46
+ const child = spawn(binary, process.argv.slice(2), {
47
+ stdio: 'inherit',
48
+ env: { ...process.env, DEEP_CODE_PROGRAM_NAME: invokedAs },
49
+ });
29
50
 
30
- if (result.error) {
31
- console.error(`deepcode: failed to launch binary: ${result.error.message}`);
32
- process.exit(1);
51
+ const forward = (signal) => {
52
+ try {
53
+ child.kill(signal);
54
+ } catch {
55
+ /* child already gone — nothing to forward to */
56
+ }
57
+ };
58
+ // SIGINT is deliberately NOT forwarded on Windows: the child receives its own
59
+ // CTRL_C_EVENT from the console directly, and Node's `child.kill('SIGINT')`
60
+ // there is a forced TerminateProcess — it would win the race and abort the
61
+ // binary before its graceful Ctrl-C cancel (drain, JSON report, session tail)
62
+ // could run. On unix SIGINT already reaches the whole foreground group; the
63
+ // forward only additionally covers a supervisor signalling this pid alone.
64
+ const forwarded =
65
+ process.platform === 'win32' ? ['SIGTERM', 'SIGHUP'] : ['SIGINT', 'SIGTERM', 'SIGHUP'];
66
+ for (const signal of forwarded) {
67
+ process.on(signal, () => forward(signal));
33
68
  }
34
69
 
35
- // Mirror the child's exit; a signal-terminated child reports null status.
36
- process.exit(result.status === null ? 1 : result.status);
70
+ child.on('error', (error) => {
71
+ console.error(`deepcode: failed to launch binary: ${error.message}`);
72
+ process.exit(1);
73
+ });
74
+
75
+ child.on('exit', (code, signal) => {
76
+ // Mirror the child's fate. A signal exit becomes the shell convention 128+n so
77
+ // the parent sees the real cause; a normal exit passes its code through.
78
+ if (signal) {
79
+ process.exit(128 + (os.constants.signals[signal] || 0));
80
+ }
81
+ process.exit(code === null ? 1 : code);
82
+ });
package/install.mjs CHANGED
@@ -237,8 +237,24 @@ async function checksumMatches(assetName, binPath) {
237
237
  const expected = parseChecksum(fs.readFileSync(sumsPath, 'utf8'), assetName);
238
238
  if (!expected) return !checksumRequired();
239
239
  return (await sha256OfFile(binPath)) === expected;
240
- } catch {
241
- return !checksumRequired();
240
+ } catch (error) {
241
+ // Distinguish "the server answered no" from "could not reach the server".
242
+ // A 404 / HTTP status means this VERSION's manifest or asset is genuinely
243
+ // absent (a wrong version left on disk), so fall through to re-download as
244
+ // before. But a connection failure/timeout is transient — offline
245
+ // `npm rebuild`, a blocked request — and the binary on disk was already
246
+ // verified at install time (download → verifyChecksum → rename), so deleting
247
+ // it and then failing the offline re-download is worse than keeping it.
248
+ const message = String(error?.message ?? error);
249
+ const serverRejected = message.includes('Binary not found') || message.startsWith('HTTP ');
250
+ if (serverRejected) {
251
+ return !checksumRequired();
252
+ }
253
+ console.warn(
254
+ '⚠️ deepcode: could not reach SHA256SUMS to re-verify the installed binary ' +
255
+ '(offline?); keeping the existing, install-time-verified binary.',
256
+ );
257
+ return true;
242
258
  } finally {
243
259
  fs.rmSync(sumsPath, { force: true });
244
260
  }
@@ -266,7 +282,32 @@ function sha256OfFile(file) {
266
282
  }
267
283
 
268
284
  // ── 辅助函数 ──
269
- function downloadFile(url, dest) {
285
+ // GitHub release assets always 302 to a signed URL, so the redirect branch runs
286
+ // on every install. Two things it must not do: follow forever, and throw
287
+ // synchronously out of the response callback (an `https.get` handler is not
288
+ // inside the promise executor, so a throw there is an uncaught exception, not a
289
+ // rejection — the install dies with an unrelated stack).
290
+ //
291
+ // `MAX_DOWNLOAD_REDIRECTS` bounds the chain: each hop opens its own socket and
292
+ // arms its own 5-minute timeout, so an unbounded chain is an unbounded hang, not
293
+ // merely a slow one.
294
+ const MAX_DOWNLOAD_REDIRECTS = 5;
295
+
296
+ // `fs.rmSync(..., { force: true })`, never `unlinkSync`: the write stream opens
297
+ // the file asynchronously, so on a fast response the path may not exist yet
298
+ // (`ENOENT`), and on Windows a file with a live handle refuses to unlink
299
+ // (`EBUSY`/`EPERM`). Both threw from inside the response callback. `force`
300
+ // swallows the missing-file case; the try/catch covers the locked-file one.
301
+ function discardPartial(file, dest) {
302
+ file.close();
303
+ try {
304
+ fs.rmSync(dest, { force: true });
305
+ } catch {
306
+ /* a locked or already-gone temp file must not mask the real error */
307
+ }
308
+ }
309
+
310
+ function downloadFile(url, dest, redirectsLeft = MAX_DOWNLOAD_REDIRECTS) {
270
311
  return new Promise((resolve, reject) => {
271
312
  const file = fs.createWriteStream(dest, { mode: 0o755 });
272
313
 
@@ -275,18 +316,24 @@ function downloadFile(url, dest) {
275
316
  if ([301, 302, 303, 307, 308].includes(response.statusCode)) {
276
317
  const redirectUrl = response.headers.location;
277
318
  if (!redirectUrl) {
319
+ discardPartial(file, dest);
278
320
  reject(new Error(`Redirect with no location from ${url}`));
279
321
  return;
280
322
  }
281
- file.close();
282
- fs.unlinkSync(dest);
283
- downloadFile(redirectUrl, dest).then(resolve).catch(reject);
323
+ if (redirectsLeft <= 0) {
324
+ discardPartial(file, dest);
325
+ reject(new Error(
326
+ `Too many redirects (more than ${MAX_DOWNLOAD_REDIRECTS}) starting at ${url}.`
327
+ ));
328
+ return;
329
+ }
330
+ discardPartial(file, dest);
331
+ downloadFile(redirectUrl, dest, redirectsLeft - 1).then(resolve).catch(reject);
284
332
  return;
285
333
  }
286
334
 
287
335
  if (response.statusCode === 404) {
288
- file.close();
289
- fs.unlinkSync(dest);
336
+ discardPartial(file, dest);
290
337
  reject(new Error(
291
338
  `Binary not found for ${platform()}-${arch()} (v${VERSION}). ` +
292
339
  `Check https://github.com/${REPO}/releases for available versions.`
@@ -295,8 +342,7 @@ function downloadFile(url, dest) {
295
342
  }
296
343
 
297
344
  if (response.statusCode !== 200) {
298
- file.close();
299
- fs.unlinkSync(dest);
345
+ discardPartial(file, dest);
300
346
  reject(new Error(`HTTP ${response.statusCode} from ${url}`));
301
347
  return;
302
348
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liwenkai/deepcode",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "AI coding assistant in your terminal — powered by DeepSeek",
5
5
  "bin": {
6
6
  "deepcode": "bin/deepcode"