@liwenkai/deepcode 0.2.0 → 0.3.0

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 (2) hide show
  1. package/install.mjs +177 -18
  2. package/package.json +1 -1
package/install.mjs CHANGED
@@ -9,13 +9,39 @@ import path from 'node:path';
9
9
  import crypto from 'node:crypto';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { platform, arch } from 'node:os';
12
+ import { execFileSync } from 'node:child_process';
12
13
 
13
14
  // ── 配置 ──
14
15
  const REPO = 'liwenka1/deep-code';
15
- const VERSION = process.env.npm_package_version || '0.1.0';
16
+ // Read the version from package.json rather than trusting `npm_package_version`,
17
+ // which is unset under yarn berry / pnpm in some modes and when this script is
18
+ // run directly. The old `|| '0.1.0'` fallback silently installed v0.1.0 — and
19
+ // v0.1.0's own SHA256SUMS validates it, so a years-old binary installed with a
20
+ // green checkmark. There is no sane default here: fail loudly instead.
21
+ const PKG_DIR = path.dirname(fileURLToPath(import.meta.url));
22
+ const VERSION = readVersion();
23
+
24
+ function readVersion() {
25
+ // The package.json next to this script is the source of truth: it is the
26
+ // version actually being installed. `npm_package_version` is only a fallback
27
+ // for runners that execute the script without writing the file, and must not
28
+ // take precedence — if the two ever disagree, the file is right.
29
+ try {
30
+ const pkg = JSON.parse(fs.readFileSync(path.join(PKG_DIR, 'package.json'), 'utf8'));
31
+ if (pkg.version) return pkg.version;
32
+ if (process.env.npm_package_version) return process.env.npm_package_version;
33
+ throw new Error('package.json has no "version"');
34
+ } catch (error) {
35
+ if (process.env.npm_package_version) return process.env.npm_package_version;
36
+ console.error(`❌ deepcode: cannot determine which version to install (${error.message}).`);
37
+ console.error(' Reinstall with npm, or download a binary from');
38
+ console.error(` https://github.com/${REPO}/releases`);
39
+ process.exit(1);
40
+ }
41
+ }
16
42
  // `import.meta.dirname` only exists on Node 20.11+; derive it from the module
17
43
  // URL so the `engines: node >=18` floor actually works.
18
- const BIN_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), 'bin');
44
+ const BIN_DIR = path.join(PKG_DIR, 'bin');
19
45
  // The real binary sits next to the `deepcode` JS launcher (bin/deepcode), which
20
46
  // spawns it. Distinct names so the download never clobbers the launcher.
21
47
  const BIN_NAME = platform() === 'win32' ? 'deepcode.exe' : 'deepcode-bin';
@@ -32,6 +58,30 @@ const ASSET_MAP = {
32
58
  'win32-arm64': 'deep-code-x86_64-pc-windows-msvc.exe',
33
59
  };
34
60
 
61
+ /// Whether this Linux host's libc is musl rather than glibc.
62
+ ///
63
+ /// Requires POSITIVE evidence of musl, never merely the absence of a glibc
64
+ /// marker: this gate aborts the install, so guessing wrong here would block a
65
+ /// host that would have worked fine. Unknown always means "assume glibc".
66
+ function isMuslHost() {
67
+ try {
68
+ // Present on glibc builds; its absence alone proves nothing.
69
+ if (process.report?.getReport?.()?.header?.glibcVersionRuntime) return false;
70
+ } catch {
71
+ /* fall through to the ldd probe */
72
+ }
73
+ try {
74
+ const out = execFileSync('ldd', ['--version'], {
75
+ encoding: 'utf8',
76
+ stdio: ['ignore', 'pipe', 'pipe'],
77
+ });
78
+ return /musl/i.test(out);
79
+ } catch (error) {
80
+ // musl's ldd exits non-zero but still prints its banner, on stderr.
81
+ return /musl/i.test(`${error?.stdout ?? ''}${error?.stderr ?? ''}`);
82
+ }
83
+ }
84
+
35
85
  // ── 主逻辑 ──
36
86
  async function main() {
37
87
  const platformKey = `${platform()}-${arch()}`;
@@ -45,13 +95,30 @@ async function main() {
45
95
  process.exit(1);
46
96
  }
47
97
 
98
+ // Only `-gnu` Linux binaries are published, so a musl host (Alpine, and most
99
+ // `-slim`/distroless images) would install "successfully" and then fail to
100
+ // exec with a message about a missing dynamic loader. Say so up front.
101
+ if (platform() === 'linux' && isMuslHost()) {
102
+ console.error('❌ deepcode: this Linux host uses musl libc (e.g. Alpine).');
103
+ console.error(' Only glibc builds are published, so the binary cannot run here.');
104
+ console.error(' Use a glibc-based image (debian/ubuntu slim), or build from source:');
105
+ console.error(` cargo install --git https://github.com/${REPO} deep-code-tui`);
106
+ process.exit(1);
107
+ }
108
+
48
109
  const binPath = path.join(BIN_DIR, BIN_NAME);
49
110
  const downloadUrl = `https://github.com/${REPO}/releases/download/v${VERSION}/${assetName}`;
50
111
 
51
- // 已有二进制则跳过(npm install 幂等)
112
+ // 已有二进制则跳过(npm install 幂等)。
113
+ // 校验和照样跑一遍:一个被截断的旧下载同样"存在",早退会让它永久留下且
114
+ // 永不复检,每次 npm install 都报成功。校验失败就当没装过,重新下载。
52
115
  if (fs.existsSync(binPath)) {
53
- console.log(`✅ deepcode binary already installed (${platformKey})`);
54
- return;
116
+ if (await checksumMatches(assetName, binPath)) {
117
+ console.log(`✅ deepcode binary already installed (${platformKey})`);
118
+ return;
119
+ }
120
+ console.log('⚠️ existing deepcode binary failed checksum — re-downloading');
121
+ fs.rmSync(binPath, { force: true });
55
122
  }
56
123
 
57
124
  console.log(`📦 Downloading deepcode v${VERSION} for ${platformKey}...`);
@@ -60,15 +127,22 @@ async function main() {
60
127
  // 确保 bin 目录存在
61
128
  fs.mkdirSync(BIN_DIR, { recursive: true });
62
129
 
63
- // 下载
64
- await downloadFile(downloadUrl, binPath);
65
-
66
- // 校验完整性(SHA256SUMS 来自同一 release)
67
- await verifyChecksum(assetName, binPath);
68
-
69
- // 设置可执行权限(非 Windows
70
- if (platform() !== 'win32') {
71
- fs.chmodSync(binPath, 0o755);
130
+ // 下载到临时文件再改名:中途失败(连接提前关闭、磁盘满)绝不会在目标路径
131
+ // 上留下半个二进制,否则下次 install 的幂等早退会把它当成装好的。
132
+ const tmpPath = `${binPath}.download`;
133
+ fs.rmSync(tmpPath, { force: true });
134
+ try {
135
+ await downloadFile(downloadUrl, tmpPath);
136
+ // 校验完整性(SHA256SUMS 来自同一 release
137
+ await verifyChecksum(assetName, tmpPath);
138
+ // 设置可执行权限(非 Windows)
139
+ if (platform() !== 'win32') {
140
+ fs.chmodSync(tmpPath, 0o755);
141
+ }
142
+ fs.renameSync(tmpPath, binPath);
143
+ } catch (error) {
144
+ fs.rmSync(tmpPath, { force: true });
145
+ throw error;
72
146
  }
73
147
 
74
148
  console.log(`✅ deepcode v${VERSION} installed successfully!`);
@@ -76,15 +150,37 @@ async function main() {
76
150
  }
77
151
 
78
152
  // ── 完整性校验 ──
79
- // 从同一 release 拉取 SHA256SUMS,比对下载二进制的哈希。SHA256SUMS 缺失时
80
- // (旧 release / 手动构建)跳过校验;哈希不匹配则删除文件并报错。
153
+ // 从同一 release 拉取 SHA256SUMS,比对下载二进制的哈希。
154
+
155
+ /// Whether verification is mandatory for the version being installed.
156
+ ///
157
+ /// `SHA256SUMS` has shipped with every release from v0.1.0 on, so from v0.2.0
158
+ /// the back-compat allowance buys nothing and costs a lot: treating a missing
159
+ /// manifest as "skip" handed anyone who can fail that one request a
160
+ /// downgrade-to-no-verification path, and on the already-installed fast path it
161
+ /// printed a green "already installed" for a binary of some other version.
162
+ function checksumRequired() {
163
+ const [major, minor] = String(VERSION)
164
+ .split('.')
165
+ .map((part) => Number.parseInt(part, 10));
166
+ if (!Number.isFinite(major) || !Number.isFinite(minor)) return true;
167
+ return major > 0 || minor >= 2;
168
+ }
169
+
81
170
  async function verifyChecksum(assetName, binPath) {
82
171
  const sumsUrl = `https://github.com/${REPO}/releases/download/v${VERSION}/SHA256SUMS`;
83
172
  const sumsPath = `${binPath}.SHA256SUMS`;
84
173
 
85
174
  try {
86
175
  await downloadFile(sumsUrl, sumsPath);
87
- } catch {
176
+ } catch (error) {
177
+ if (checksumRequired()) {
178
+ throw new Error(
179
+ `cannot verify the download: SHA256SUMS for v${VERSION} is unreachable (${error.message}).\n` +
180
+ ' Refusing to install an unverified binary. Check your network/proxy, or\n' +
181
+ ` download and verify manually from https://github.com/${REPO}/releases`
182
+ );
183
+ }
88
184
  console.warn('⚠️ deepcode: SHA256SUMS not published for this release; skipping checksum verification.');
89
185
  return;
90
186
  }
@@ -97,13 +193,25 @@ async function verifyChecksum(assetName, binPath) {
97
193
  }
98
194
 
99
195
  if (!expected) {
196
+ if (checksumRequired()) {
197
+ throw new Error(
198
+ `SHA256SUMS for v${VERSION} has no entry for ${assetName} — refusing to install an unverified binary.`
199
+ );
200
+ }
100
201
  console.warn(`⚠️ deepcode: no checksum entry for ${assetName}; skipping verification.`);
101
202
  return;
102
203
  }
103
204
 
104
205
  const actual = await sha256OfFile(binPath);
105
206
  if (actual !== expected) {
106
- fs.rmSync(binPath, { force: true });
207
+ // Best-effort: on Windows this file may be locked (EBUSY/EPERM) by a running
208
+ // deepcode. `force: true` does not suppress that, and letting it throw here
209
+ // would replace the precise mismatch error with an unrelated one.
210
+ try {
211
+ fs.rmSync(binPath, { force: true });
212
+ } catch {
213
+ /* fall through to the mismatch error below */
214
+ }
107
215
  throw new Error(
108
216
  `checksum mismatch for ${assetName}\n expected: ${expected}\n actual: ${actual}\n` +
109
217
  ' The download may be corrupted or tampered with — aborting.'
@@ -112,6 +220,30 @@ async function verifyChecksum(assetName, binPath) {
112
220
  console.log('🔒 deepcode: checksum verified.');
113
221
  }
114
222
 
223
+ /// Non-throwing variant for the "already installed" fast path.
224
+ ///
225
+ /// Returns false — i.e. "re-download" — when verification is required but the
226
+ /// manifest cannot be fetched or has no entry. It used to `catch { return true }`
227
+ /// for *every* failure, so a 404 (wrong VERSION, assets not uploaded yet, an
228
+ /// intercepted request) printed "✅ already installed" over whatever binary
229
+ /// happened to be sitting there. For pre-0.2.0 versions the old lenient
230
+ /// behaviour is kept so an unverifiable-but-present binary is not re-downloaded
231
+ /// on every install.
232
+ async function checksumMatches(assetName, binPath) {
233
+ const sumsUrl = `https://github.com/${REPO}/releases/download/v${VERSION}/SHA256SUMS`;
234
+ const sumsPath = `${binPath}.check.SHA256SUMS`;
235
+ try {
236
+ await downloadFile(sumsUrl, sumsPath);
237
+ const expected = parseChecksum(fs.readFileSync(sumsPath, 'utf8'), assetName);
238
+ if (!expected) return !checksumRequired();
239
+ return (await sha256OfFile(binPath)) === expected;
240
+ } catch {
241
+ return !checksumRequired();
242
+ } finally {
243
+ fs.rmSync(sumsPath, { force: true });
244
+ }
245
+ }
246
+
115
247
  // 解析 `sha256sum` 风格的清单(`<hex> name` 或 `<hex> *name`)。
116
248
  function parseChecksum(text, assetName) {
117
249
  for (const line of text.split('\n')) {
@@ -169,10 +301,37 @@ function downloadFile(url, dest) {
169
301
  return;
170
302
  }
171
303
 
304
+ // A premature connection close fires 'error'/'aborted' on the *response*,
305
+ // not on the file — without this the promise resolved on the truncated
306
+ // file's 'finish' and the partial download was treated as a success.
307
+ response.on('error', (err) => {
308
+ file.close();
309
+ fs.rmSync(dest, { force: true });
310
+ reject(err);
311
+ });
312
+ response.on('aborted', () => {
313
+ file.close();
314
+ fs.rmSync(dest, { force: true });
315
+ reject(new Error(`Connection closed before the download finished: ${url}`));
316
+ });
317
+
318
+ // Cross-check the advertised length so a silently short body cannot pass.
319
+ const expectedBytes = Number(response.headers['content-length']) || 0;
320
+
172
321
  response.pipe(file);
173
322
 
174
323
  file.on('finish', () => {
175
324
  file.close();
325
+ if (expectedBytes > 0) {
326
+ const written = fs.statSync(dest).size;
327
+ if (written !== expectedBytes) {
328
+ fs.rmSync(dest, { force: true });
329
+ reject(new Error(
330
+ `Truncated download from ${url}: got ${written} of ${expectedBytes} bytes.`
331
+ ));
332
+ return;
333
+ }
334
+ }
176
335
  resolve();
177
336
  });
178
337
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liwenkai/deepcode",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "AI coding assistant in your terminal — powered by DeepSeek",
5
5
  "bin": {
6
6
  "deepcode": "bin/deepcode"