@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.
- package/install.mjs +177 -18
- 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
|
-
|
|
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(
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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,比对下载二进制的哈希。
|
|
80
|
-
|
|
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
|
-
|
|
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
|
|