deepseek-tui 0.8.16 → 0.8.18
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/README.md +12 -7
- package/package.json +6 -4
- package/scripts/install.js +189 -33
- package/scripts/run.js +3 -3
- package/test/install.test.js +71 -0
- package/test/postinstall.test.js +91 -0
- package/test/run.test.js +11 -0
package/README.md
CHANGED
|
@@ -17,8 +17,10 @@ npm install deepseek-tui
|
|
|
17
17
|
npx deepseek-tui --help
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
`postinstall`
|
|
21
|
-
`deepseek` and `deepseek-tui` commands.
|
|
20
|
+
`postinstall` tries to download platform binaries into `bin/downloads/` and
|
|
21
|
+
exposes `deepseek` and `deepseek-tui` commands. If GitHub release assets are
|
|
22
|
+
temporarily unreachable, install continues and the wrapper retries the download
|
|
23
|
+
on first run.
|
|
22
24
|
|
|
23
25
|
## First run
|
|
24
26
|
|
|
@@ -60,8 +62,9 @@ Prebuilt binaries for the GitHub release are downloaded automatically:
|
|
|
60
62
|
- Windows x64
|
|
61
63
|
|
|
62
64
|
Other platform/architecture combinations (musl, riscv64, FreeBSD, …) aren't
|
|
63
|
-
shipped as prebuilts.
|
|
64
|
-
|
|
65
|
+
shipped as prebuilts. Unsupported platforms, checksum failures, and glibc
|
|
66
|
+
compatibility problems still fail with a clear error pointing you at
|
|
67
|
+
`cargo install deepseek-tui-cli deepseek-tui --locked` and the full
|
|
65
68
|
[docs/INSTALL.md](https://github.com/Hmbown/DeepSeek-TUI/blob/main/docs/INSTALL.md)
|
|
66
69
|
build-from-source guide.
|
|
67
70
|
|
|
@@ -70,9 +73,13 @@ build-from-source guide.
|
|
|
70
73
|
- Default binary version comes from `deepseekBinaryVersion` in `package.json`.
|
|
71
74
|
- Set `DEEPSEEK_TUI_VERSION` or `DEEPSEEK_VERSION` to override the release version.
|
|
72
75
|
- Set `DEEPSEEK_TUI_GITHUB_REPO` or `DEEPSEEK_GITHUB_REPO` to override the source repo (defaults to `Hmbown/DeepSeek-TUI`).
|
|
76
|
+
- Set `DEEPSEEK_TUI_RELEASE_BASE_URL` to use an internal or mirrored
|
|
77
|
+
release-asset directory when GitHub Releases is unavailable. The directory
|
|
78
|
+
must contain `deepseek-artifacts-sha256.txt` and the platform binaries.
|
|
73
79
|
- Set `DEEPSEEK_TUI_FORCE_DOWNLOAD=1` to force download even when the cached binary is already present.
|
|
74
80
|
- Set `DEEPSEEK_TUI_DISABLE_INSTALL=1` to skip install-time download.
|
|
75
|
-
- Set `DEEPSEEK_TUI_OPTIONAL_INSTALL=1` to make
|
|
81
|
+
- Set `DEEPSEEK_TUI_OPTIONAL_INSTALL=1` to make install-time retryable download
|
|
82
|
+
failures warn and exit `0` instead of failing `npm install`.
|
|
76
83
|
|
|
77
84
|
## Release integrity
|
|
78
85
|
|
|
@@ -80,5 +87,3 @@ build-from-source guide.
|
|
|
80
87
|
exist for the target GitHub release before publishing.
|
|
81
88
|
- Install-time downloads are verified against the release checksum manifest before
|
|
82
89
|
the wrapper marks them executable.
|
|
83
|
-
- Set `DEEPSEEK_TUI_RELEASE_BASE_URL` to point the installer at a local or
|
|
84
|
-
staged release-asset directory for smoke tests.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepseek-tui",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"deepseekBinaryVersion": "0.8.
|
|
3
|
+
"version": "0.8.18",
|
|
4
|
+
"deepseekBinaryVersion": "0.8.18",
|
|
5
5
|
"description": "Install and run deepseek and deepseek-tui binaries from GitHub release artifacts.",
|
|
6
6
|
"author": "Hmbown",
|
|
7
7
|
"license": "MIT",
|
|
@@ -28,9 +28,10 @@
|
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"release:check": "node scripts/verify-release-assets.js",
|
|
31
|
-
"postinstall": "node scripts/install.js",
|
|
31
|
+
"postinstall": "node scripts/install.js --optional",
|
|
32
32
|
"prepublishOnly": "node scripts/verify-release-assets.js",
|
|
33
|
-
"prepack": "node scripts/install.js"
|
|
33
|
+
"prepack": "node scripts/install.js",
|
|
34
|
+
"test": "node --test test/*.test.js"
|
|
34
35
|
},
|
|
35
36
|
"engines": {
|
|
36
37
|
"node": ">=18"
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
"files": [
|
|
43
44
|
"bin/*.js",
|
|
44
45
|
"scripts/*.js",
|
|
46
|
+
"test/*.js",
|
|
45
47
|
"README.md",
|
|
46
48
|
"package.json"
|
|
47
49
|
]
|
package/scripts/install.js
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
function assertSupportedNode() {
|
|
2
|
+
const version = process.versions && process.versions.node ? process.versions.node : "unknown";
|
|
3
|
+
const major = Number.parseInt(String(version).split(".")[0], 10);
|
|
4
|
+
if (Number.isNaN(major) || major < 18) {
|
|
5
|
+
process.stderr.write(
|
|
6
|
+
"deepseek-tui: Node.js 18 or newer is required for npm installation. " +
|
|
7
|
+
`Current Node.js version is ${version}. ` +
|
|
8
|
+
"Please upgrade Node.js and rerun `npm install -g deepseek-tui`.\n",
|
|
9
|
+
);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
assertSupportedNode();
|
|
15
|
+
|
|
1
16
|
const fs = require("fs");
|
|
2
17
|
const https = require("https");
|
|
3
18
|
const http = require("http");
|
|
@@ -20,14 +35,19 @@ const pkg = require("../package.json");
|
|
|
20
35
|
|
|
21
36
|
const DEFAULT_TIMEOUT_MS = 300_000; // 5 minutes per attempt
|
|
22
37
|
const DEFAULT_STALL_MS = 30_000; // abort if no bytes for 30s
|
|
38
|
+
const OPTIONAL_TIMEOUT_MS = 15_000; // fail fast during optional npm postinstall
|
|
39
|
+
const OPTIONAL_STALL_MS = 5_000; // avoid long hangs when install can recover on first run
|
|
23
40
|
const MAX_ATTEMPTS = 5;
|
|
41
|
+
const OPTIONAL_MAX_ATTEMPTS = 1; // runtime keeps the full retry budget on first launch
|
|
24
42
|
const BASE_BACKOFF_MS = 1_000;
|
|
25
43
|
|
|
26
44
|
const RETRYABLE_NET_CODES = new Set([
|
|
27
45
|
"ECONNRESET",
|
|
28
46
|
"ECONNREFUSED",
|
|
47
|
+
"EDOWNLOADTIMEOUT",
|
|
29
48
|
"ETIMEDOUT",
|
|
30
49
|
"EAI_AGAIN",
|
|
50
|
+
"ENOTFOUND",
|
|
31
51
|
"ENETUNREACH",
|
|
32
52
|
"EHOSTUNREACH",
|
|
33
53
|
"EPIPE",
|
|
@@ -71,6 +91,38 @@ function resolveRepo() {
|
|
|
71
91
|
return process.env.DEEPSEEK_TUI_GITHUB_REPO || process.env.DEEPSEEK_GITHUB_REPO || "Hmbown/DeepSeek-TUI";
|
|
72
92
|
}
|
|
73
93
|
|
|
94
|
+
function isOptionalInstall(argv = process.argv.slice(2), env = process.env) {
|
|
95
|
+
return (
|
|
96
|
+
argv.includes("--optional") ||
|
|
97
|
+
env.DEEPSEEK_TUI_OPTIONAL_INSTALL === "1" ||
|
|
98
|
+
env.DEEPSEEK_OPTIONAL_INSTALL === "1"
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isInstallContext(context) {
|
|
103
|
+
return context === "install";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Optional install only relaxes npm postinstall behavior. Runtime downloads
|
|
107
|
+
// keep the normal retry/timeout budget so first-run recovery stays resilient.
|
|
108
|
+
function defaultTimeoutMs(context = "runtime", env = process.env) {
|
|
109
|
+
return isInstallContext(context) && isOptionalInstall(undefined, env)
|
|
110
|
+
? OPTIONAL_TIMEOUT_MS
|
|
111
|
+
: DEFAULT_TIMEOUT_MS;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function defaultStallMs(context = "runtime", env = process.env) {
|
|
115
|
+
return isInstallContext(context) && isOptionalInstall(undefined, env)
|
|
116
|
+
? OPTIONAL_STALL_MS
|
|
117
|
+
: DEFAULT_STALL_MS;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function maxAttempts(context = "runtime", env = process.env) {
|
|
121
|
+
return isInstallContext(context) && isOptionalInstall(undefined, env)
|
|
122
|
+
? OPTIONAL_MAX_ATTEMPTS
|
|
123
|
+
: MAX_ATTEMPTS;
|
|
124
|
+
}
|
|
125
|
+
|
|
74
126
|
function binaryPaths() {
|
|
75
127
|
const { deepseek, tui } = detectBinaryNames();
|
|
76
128
|
const releaseDir = releaseBinaryDirectory();
|
|
@@ -105,6 +157,48 @@ function logInfo(message) {
|
|
|
105
157
|
process.stderr.write(`deepseek-tui: ${message}\n`);
|
|
106
158
|
}
|
|
107
159
|
|
|
160
|
+
function installFailureHint(error) {
|
|
161
|
+
const message = error && error.message ? String(error.message) : "";
|
|
162
|
+
const code = error && error.code ? String(error.code) : "";
|
|
163
|
+
const releaseBase =
|
|
164
|
+
process.env.DEEPSEEK_TUI_RELEASE_BASE_URL ||
|
|
165
|
+
process.env.DEEPSEEK_RELEASE_BASE_URL;
|
|
166
|
+
const networkMarkers = [
|
|
167
|
+
"github.com",
|
|
168
|
+
"ENOTFOUND",
|
|
169
|
+
"EAI_AGAIN",
|
|
170
|
+
"ETIMEDOUT",
|
|
171
|
+
"ECONNRESET",
|
|
172
|
+
"ENETUNREACH",
|
|
173
|
+
"EHOSTUNREACH",
|
|
174
|
+
"EDOWNLOADTIMEOUT",
|
|
175
|
+
];
|
|
176
|
+
const looksLikeNetworkDownloadFailure = networkMarkers.some(
|
|
177
|
+
(marker) => message.includes(marker) || code === marker,
|
|
178
|
+
);
|
|
179
|
+
if (!looksLikeNetworkDownloadFailure) {
|
|
180
|
+
return "";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (releaseBase) {
|
|
184
|
+
return [
|
|
185
|
+
"deepseek-tui install hint:",
|
|
186
|
+
` DEEPSEEK_TUI_RELEASE_BASE_URL is set to ${releaseBase}`,
|
|
187
|
+
" Verify that this directory contains deepseek-artifacts-sha256.txt",
|
|
188
|
+
" plus the deepseek/deepseek-tui binary assets for your platform.",
|
|
189
|
+
].join("\n");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return [
|
|
193
|
+
"deepseek-tui install hint:",
|
|
194
|
+
" The npm package downloads prebuilt binaries from GitHub Releases.",
|
|
195
|
+
" If GitHub is unavailable on this network, mirror the release assets and set:",
|
|
196
|
+
" DEEPSEEK_TUI_RELEASE_BASE_URL=https://<mirror>/<release-asset-directory>/",
|
|
197
|
+
" The directory must contain deepseek-artifacts-sha256.txt and the platform binaries.",
|
|
198
|
+
" See docs/INSTALL.md#npm-download-is-slow-or-times-out-from-mainland-china.",
|
|
199
|
+
].join("\n");
|
|
200
|
+
}
|
|
201
|
+
|
|
108
202
|
function envInt(name, fallback) {
|
|
109
203
|
const raw = process.env[name];
|
|
110
204
|
if (!raw) {
|
|
@@ -117,17 +211,17 @@ function envInt(name, fallback) {
|
|
|
117
211
|
return parsed;
|
|
118
212
|
}
|
|
119
213
|
|
|
120
|
-
function downloadTimeoutMs() {
|
|
214
|
+
function downloadTimeoutMs(context = "runtime") {
|
|
121
215
|
return envInt(
|
|
122
216
|
"DEEPSEEK_TUI_DOWNLOAD_TIMEOUT_MS",
|
|
123
|
-
envInt("DEEPSEEK_DOWNLOAD_TIMEOUT_MS",
|
|
217
|
+
envInt("DEEPSEEK_DOWNLOAD_TIMEOUT_MS", defaultTimeoutMs(context)),
|
|
124
218
|
);
|
|
125
219
|
}
|
|
126
220
|
|
|
127
|
-
function downloadStallMs() {
|
|
221
|
+
function downloadStallMs(context = "runtime") {
|
|
128
222
|
return envInt(
|
|
129
223
|
"DEEPSEEK_TUI_DOWNLOAD_STALL_MS",
|
|
130
|
-
envInt("DEEPSEEK_DOWNLOAD_STALL_MS",
|
|
224
|
+
envInt("DEEPSEEK_DOWNLOAD_STALL_MS", defaultStallMs(context)),
|
|
131
225
|
);
|
|
132
226
|
}
|
|
133
227
|
|
|
@@ -355,8 +449,16 @@ function connectThroughProxy(proxy, targetHost, targetPort, timeoutMs) {
|
|
|
355
449
|
// ────────────────────────────────────────────────────────────────────────────
|
|
356
450
|
|
|
357
451
|
function httpRequest(rawUrl, opts = {}) {
|
|
358
|
-
const
|
|
359
|
-
|
|
452
|
+
const context =
|
|
453
|
+
opts.context === undefined || opts.context === null ? "runtime" : opts.context;
|
|
454
|
+
const totalTimeoutMs =
|
|
455
|
+
opts.totalTimeoutMs === undefined || opts.totalTimeoutMs === null
|
|
456
|
+
? downloadTimeoutMs(context)
|
|
457
|
+
: opts.totalTimeoutMs;
|
|
458
|
+
const stallMs =
|
|
459
|
+
opts.stallMs === undefined || opts.stallMs === null
|
|
460
|
+
? downloadStallMs(context)
|
|
461
|
+
: opts.stallMs;
|
|
360
462
|
|
|
361
463
|
return new Promise((resolve, reject) => {
|
|
362
464
|
let url;
|
|
@@ -645,7 +747,12 @@ function isRetryable(err) {
|
|
|
645
747
|
if (err.nonRetryable) return false;
|
|
646
748
|
if (err instanceof NonRetryableError) return false;
|
|
647
749
|
if (err instanceof DownloadTimeoutError) return true;
|
|
648
|
-
|
|
750
|
+
// withRetry() rethrows a plain Error while preserving name/status, so wrapped
|
|
751
|
+
// HTTP 5xx failures still classify as retryable during optional postinstall.
|
|
752
|
+
if (
|
|
753
|
+
(err instanceof HttpStatusError || err.name === "HttpStatusError") &&
|
|
754
|
+
typeof err.status === "number"
|
|
755
|
+
) {
|
|
649
756
|
return err.status >= 500;
|
|
650
757
|
}
|
|
651
758
|
if (err.code && RETRYABLE_NET_CODES.has(err.code)) return true;
|
|
@@ -668,27 +775,42 @@ function sleep(ms) {
|
|
|
668
775
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
669
776
|
}
|
|
670
777
|
|
|
671
|
-
async function withRetry(label, fn) {
|
|
778
|
+
async function withRetry(label, fn, context = "runtime") {
|
|
672
779
|
let lastErr;
|
|
673
|
-
|
|
780
|
+
const attemptLimit = maxAttempts(context);
|
|
781
|
+
for (let attempt = 1; attempt <= attemptLimit; attempt++) {
|
|
674
782
|
try {
|
|
675
783
|
return await fn(attempt);
|
|
676
784
|
} catch (err) {
|
|
677
785
|
lastErr = err;
|
|
678
|
-
if (!isRetryable(err) || attempt ===
|
|
786
|
+
if (!isRetryable(err) || attempt === attemptLimit) {
|
|
679
787
|
break;
|
|
680
788
|
}
|
|
681
789
|
const wait = backoffDelay(attempt);
|
|
682
790
|
logInfo(
|
|
683
|
-
`${label} failed (attempt ${attempt}/${
|
|
791
|
+
`${label} failed (attempt ${attempt}/${attemptLimit}): ${err.message}; retrying in ${wait} ms`,
|
|
684
792
|
);
|
|
685
793
|
await sleep(wait);
|
|
686
794
|
}
|
|
687
795
|
}
|
|
688
796
|
const msg = lastErr && lastErr.message ? lastErr.message : String(lastErr);
|
|
689
797
|
const wrapped = new Error(
|
|
690
|
-
`${label} failed after ${
|
|
798
|
+
`${label} failed after ${attemptLimit} attempt(s): ${msg}`,
|
|
691
799
|
);
|
|
800
|
+
// Preserve retry classification metadata because the install entrypoint uses
|
|
801
|
+
// the wrapped error to decide whether optional postinstall may ignore it.
|
|
802
|
+
if (lastErr && lastErr.code) {
|
|
803
|
+
wrapped.code = lastErr.code;
|
|
804
|
+
}
|
|
805
|
+
if (lastErr && lastErr.name) {
|
|
806
|
+
wrapped.name = lastErr.name;
|
|
807
|
+
}
|
|
808
|
+
if (lastErr && typeof lastErr.status === "number") {
|
|
809
|
+
wrapped.status = lastErr.status;
|
|
810
|
+
}
|
|
811
|
+
if (lastErr && lastErr.nonRetryable) {
|
|
812
|
+
wrapped.nonRetryable = true;
|
|
813
|
+
}
|
|
692
814
|
if (lastErr && lastErr.stack) {
|
|
693
815
|
wrapped.cause = lastErr;
|
|
694
816
|
}
|
|
@@ -699,7 +821,7 @@ async function withRetry(label, fn) {
|
|
|
699
821
|
// Public download primitives (now retry + progress aware)
|
|
700
822
|
// ────────────────────────────────────────────────────────────────────────────
|
|
701
823
|
|
|
702
|
-
async function followRedirects(url, opts) {
|
|
824
|
+
async function followRedirects(url, opts = {}) {
|
|
703
825
|
const maxRedirects = 10;
|
|
704
826
|
let current = url;
|
|
705
827
|
for (let hop = 0; hop < maxRedirects; hop++) {
|
|
@@ -744,17 +866,21 @@ function streamToFile(response, destination, progress) {
|
|
|
744
866
|
async function download(url, destination, options = {}) {
|
|
745
867
|
await mkdir(path.dirname(destination), { recursive: true });
|
|
746
868
|
const assetName = options.assetName || path.basename(destination);
|
|
869
|
+
const context =
|
|
870
|
+
options.context === undefined || options.context === null ? "runtime" : options.context;
|
|
871
|
+
const attemptLimit = maxAttempts(context);
|
|
747
872
|
await withRetry(`download ${assetName}`, async (attempt) => {
|
|
748
873
|
const result = await followRedirects(url, {
|
|
749
|
-
|
|
750
|
-
|
|
874
|
+
context,
|
|
875
|
+
totalTimeoutMs: downloadTimeoutMs(context),
|
|
876
|
+
stallMs: downloadStallMs(context),
|
|
751
877
|
});
|
|
752
878
|
const response = result.response;
|
|
753
879
|
const lenHeader = response.headers["content-length"];
|
|
754
880
|
const total = lenHeader ? Number.parseInt(lenHeader, 10) : 0;
|
|
755
881
|
const progress = createProgressReporter(assetName, Number.isFinite(total) ? total : 0);
|
|
756
882
|
if (attempt > 1) {
|
|
757
|
-
logInfo(`retry attempt ${attempt}/${
|
|
883
|
+
logInfo(`retry attempt ${attempt}/${attemptLimit} for ${assetName}`);
|
|
758
884
|
}
|
|
759
885
|
try {
|
|
760
886
|
await streamToFile(response, destination, progress);
|
|
@@ -768,14 +894,17 @@ async function download(url, destination, options = {}) {
|
|
|
768
894
|
throw err;
|
|
769
895
|
}
|
|
770
896
|
progress.finish();
|
|
771
|
-
});
|
|
897
|
+
}, context);
|
|
772
898
|
}
|
|
773
899
|
|
|
774
|
-
async function downloadText(url) {
|
|
900
|
+
async function downloadText(url, options = {}) {
|
|
901
|
+
const context =
|
|
902
|
+
options.context === undefined || options.context === null ? "runtime" : options.context;
|
|
775
903
|
return withRetry(`fetch ${url}`, async () => {
|
|
776
904
|
const result = await followRedirects(url, {
|
|
777
|
-
|
|
778
|
-
|
|
905
|
+
context,
|
|
906
|
+
totalTimeoutMs: downloadTimeoutMs(context),
|
|
907
|
+
stallMs: downloadStallMs(context),
|
|
779
908
|
});
|
|
780
909
|
const response = result.response;
|
|
781
910
|
response.setEncoding("utf8");
|
|
@@ -798,7 +927,7 @@ async function downloadText(url) {
|
|
|
798
927
|
});
|
|
799
928
|
response.on("error", reject);
|
|
800
929
|
});
|
|
801
|
-
});
|
|
930
|
+
}, context);
|
|
802
931
|
}
|
|
803
932
|
|
|
804
933
|
async function readLocalVersion(file) {
|
|
@@ -850,11 +979,11 @@ async function verifyChecksum(filePath, assetName, checksums) {
|
|
|
850
979
|
}
|
|
851
980
|
}
|
|
852
981
|
|
|
853
|
-
async function loadChecksums(version, repo) {
|
|
854
|
-
return parseChecksumManifest(await downloadText(checksumManifestUrl(version, repo)));
|
|
982
|
+
async function loadChecksums(version, repo, options = {}) {
|
|
983
|
+
return parseChecksumManifest(await downloadText(checksumManifestUrl(version, repo), options));
|
|
855
984
|
}
|
|
856
985
|
|
|
857
|
-
async function ensureBinary(targetPath, assetName, version, repo, getChecksums) {
|
|
986
|
+
async function ensureBinary(targetPath, assetName, version, repo, getChecksums, options = {}) {
|
|
858
987
|
const marker = `${targetPath}.version`;
|
|
859
988
|
const downloadIfNeeded =
|
|
860
989
|
process.env.DEEPSEEK_TUI_FORCE_DOWNLOAD === "1" || process.env.DEEPSEEK_FORCE_DOWNLOAD === "1";
|
|
@@ -870,7 +999,7 @@ async function ensureBinary(targetPath, assetName, version, repo, getChecksums)
|
|
|
870
999
|
const checksums = await getChecksums();
|
|
871
1000
|
const url = releaseAssetUrl(assetName, version, repo);
|
|
872
1001
|
const destination = `${targetPath}.${process.pid}.${Date.now()}.download`;
|
|
873
|
-
await download(url, destination, { assetName });
|
|
1002
|
+
await download(url, destination, { assetName, context: options.context });
|
|
874
1003
|
try {
|
|
875
1004
|
await verifyChecksum(destination, assetName, checksums);
|
|
876
1005
|
preflightGlibc(destination);
|
|
@@ -886,7 +1015,21 @@ async function ensureBinary(targetPath, assetName, version, repo, getChecksums)
|
|
|
886
1015
|
return targetPath;
|
|
887
1016
|
}
|
|
888
1017
|
|
|
889
|
-
|
|
1018
|
+
// Optional install may only downgrade retryable download failures to warnings.
|
|
1019
|
+
// Unsupported platforms, checksum mismatches, glibc compatibility errors, and
|
|
1020
|
+
// malformed release metadata must still fail with actionable diagnostics.
|
|
1021
|
+
function shouldIgnoreInstallFailure(
|
|
1022
|
+
context,
|
|
1023
|
+
error,
|
|
1024
|
+
argv = process.argv.slice(2),
|
|
1025
|
+
env = process.env,
|
|
1026
|
+
) {
|
|
1027
|
+
return isInstallContext(context) && isOptionalInstall(argv, env) && isRetryable(error);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
async function run(options = {}) {
|
|
1031
|
+
const context =
|
|
1032
|
+
options.context === undefined || options.context === null ? "runtime" : options.context;
|
|
890
1033
|
if (process.env.DEEPSEEK_TUI_DISABLE_INSTALL === "1" || process.env.DEEPSEEK_DISABLE_INSTALL === "1") {
|
|
891
1034
|
return;
|
|
892
1035
|
}
|
|
@@ -899,19 +1042,19 @@ async function run() {
|
|
|
899
1042
|
let checksumsPromise;
|
|
900
1043
|
const getChecksums = () => {
|
|
901
1044
|
if (!checksumsPromise) {
|
|
902
|
-
checksumsPromise = loadChecksums(version, repo);
|
|
1045
|
+
checksumsPromise = loadChecksums(version, repo, { context });
|
|
903
1046
|
}
|
|
904
1047
|
return checksumsPromise;
|
|
905
1048
|
};
|
|
906
1049
|
|
|
907
1050
|
await Promise.all([
|
|
908
|
-
ensureBinary(paths.deepseek.target, paths.deepseek.asset, version, repo, getChecksums),
|
|
909
|
-
ensureBinary(paths.tui.target, paths.tui.asset, version, repo, getChecksums),
|
|
1051
|
+
ensureBinary(paths.deepseek.target, paths.deepseek.asset, version, repo, getChecksums, { context }),
|
|
1052
|
+
ensureBinary(paths.tui.target, paths.tui.asset, version, repo, getChecksums, { context }),
|
|
910
1053
|
]);
|
|
911
1054
|
}
|
|
912
1055
|
|
|
913
1056
|
async function getBinaryPath(name) {
|
|
914
|
-
await run();
|
|
1057
|
+
await run({ context: "runtime" });
|
|
915
1058
|
const paths = binaryPaths();
|
|
916
1059
|
if (name === "deepseek") {
|
|
917
1060
|
return paths.deepseek.target;
|
|
@@ -924,15 +1067,28 @@ async function getBinaryPath(name) {
|
|
|
924
1067
|
|
|
925
1068
|
module.exports = {
|
|
926
1069
|
getBinaryPath,
|
|
1070
|
+
installFailureHint,
|
|
927
1071
|
run,
|
|
1072
|
+
_internal: {
|
|
1073
|
+
isOptionalInstall,
|
|
1074
|
+
shouldIgnoreInstallFailure,
|
|
1075
|
+
defaultTimeoutMs,
|
|
1076
|
+
defaultStallMs,
|
|
1077
|
+
maxAttempts,
|
|
1078
|
+
withRetry,
|
|
1079
|
+
},
|
|
928
1080
|
};
|
|
929
1081
|
|
|
930
1082
|
if (require.main === module) {
|
|
931
|
-
run().catch((error) => {
|
|
1083
|
+
run({ context: "install" }).catch((error) => {
|
|
932
1084
|
console.error("deepseek-tui install failed:", error.message);
|
|
933
|
-
|
|
1085
|
+
const hint = installFailureHint(error);
|
|
1086
|
+
if (hint) {
|
|
1087
|
+
console.error(hint);
|
|
1088
|
+
}
|
|
1089
|
+
if (shouldIgnoreInstallFailure("install", error)) {
|
|
934
1090
|
console.error(
|
|
935
|
-
"
|
|
1091
|
+
"Optional install enabled; continuing without a usable binary. The download will be retried on first run.",
|
|
936
1092
|
);
|
|
937
1093
|
process.exit(0);
|
|
938
1094
|
}
|
package/scripts/run.js
CHANGED
|
@@ -3,9 +3,8 @@ const { getBinaryPath } = require("./install");
|
|
|
3
3
|
|
|
4
4
|
const pkg = require("../package.json");
|
|
5
5
|
|
|
6
|
-
function isVersionFlag() {
|
|
7
|
-
|
|
8
|
-
return args.includes("--version") || args.includes("-v") || args.includes("-V");
|
|
6
|
+
function isVersionFlag(args = process.argv.slice(2)) {
|
|
7
|
+
return args.includes("--version") || args.includes("-V");
|
|
9
8
|
}
|
|
10
9
|
|
|
11
10
|
function handleVersionFallback(binaryName) {
|
|
@@ -46,6 +45,7 @@ module.exports = {
|
|
|
46
45
|
run,
|
|
47
46
|
runDeepseek,
|
|
48
47
|
runDeepseekTui,
|
|
48
|
+
_internal: { isVersionFlag },
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
if (require.main === module) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const test = require("node:test");
|
|
5
|
+
|
|
6
|
+
const installScript = fs.readFileSync(
|
|
7
|
+
path.join(__dirname, "..", "scripts", "install.js"),
|
|
8
|
+
"utf8",
|
|
9
|
+
);
|
|
10
|
+
const { installFailureHint } = require("../scripts/install");
|
|
11
|
+
|
|
12
|
+
test("install script checks Node support before loading helpers", () => {
|
|
13
|
+
const guardIndex = installScript.indexOf("assertSupportedNode();");
|
|
14
|
+
const firstRequireIndex = installScript.indexOf("require(");
|
|
15
|
+
|
|
16
|
+
assert.notEqual(guardIndex, -1);
|
|
17
|
+
assert.notEqual(firstRequireIndex, -1);
|
|
18
|
+
assert.ok(guardIndex < firstRequireIndex);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("install script remains parseable before the Node support guard runs", () => {
|
|
22
|
+
assert.equal(installScript.includes("??"), false);
|
|
23
|
+
assert.equal(installScript.includes("?."), false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("install failure hint explains release base override for blocked GitHub downloads", () => {
|
|
27
|
+
const previous = process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
|
|
28
|
+
delete process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
|
|
29
|
+
try {
|
|
30
|
+
const error = Object.assign(
|
|
31
|
+
new Error(
|
|
32
|
+
"fetch https://github.com/Hmbown/DeepSeek-TUI/releases/download/v0.8.18/deepseek-artifacts-sha256.txt failed after 5 attempts:\ngetaddrinfo ENOTFOUND github.com",
|
|
33
|
+
),
|
|
34
|
+
{ code: "ENOTFOUND" },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const hint = installFailureHint(error);
|
|
38
|
+
|
|
39
|
+
assert.match(hint, /DEEPSEEK_TUI_RELEASE_BASE_URL/);
|
|
40
|
+
assert.match(hint, /deepseek-artifacts-sha256\.txt/);
|
|
41
|
+
assert.match(hint, /platform binaries/);
|
|
42
|
+
} finally {
|
|
43
|
+
if (previous === undefined) {
|
|
44
|
+
delete process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
|
|
45
|
+
} else {
|
|
46
|
+
process.env.DEEPSEEK_TUI_RELEASE_BASE_URL = previous;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("install failure hint checks configured release base when override is already set", () => {
|
|
52
|
+
const previous = process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
|
|
53
|
+
process.env.DEEPSEEK_TUI_RELEASE_BASE_URL = "https://mirror.example/deepseek/";
|
|
54
|
+
try {
|
|
55
|
+
const error = Object.assign(new Error("download stalled"), {
|
|
56
|
+
code: "EDOWNLOADTIMEOUT",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const hint = installFailureHint(error);
|
|
60
|
+
|
|
61
|
+
assert.match(hint, /is set to https:\/\/mirror\.example\/deepseek\//);
|
|
62
|
+
assert.match(hint, /deepseek-artifacts-sha256\.txt/);
|
|
63
|
+
assert.doesNotMatch(hint, /If GitHub is unavailable/);
|
|
64
|
+
} finally {
|
|
65
|
+
if (previous === undefined) {
|
|
66
|
+
delete process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
|
|
67
|
+
} else {
|
|
68
|
+
process.env.DEEPSEEK_TUI_RELEASE_BASE_URL = previous;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const test = require("node:test");
|
|
3
|
+
|
|
4
|
+
const pkg = require("../package.json");
|
|
5
|
+
const { _internal } = require("../scripts/install");
|
|
6
|
+
|
|
7
|
+
test("postinstall opts into optional install mode", () => {
|
|
8
|
+
assert.equal(pkg.scripts.postinstall, "node scripts/install.js --optional");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("optional install can be enabled by command-line flag or env", () => {
|
|
12
|
+
assert.equal(_internal.isOptionalInstall(["--optional"], {}), true);
|
|
13
|
+
assert.equal(_internal.isOptionalInstall([], {}), false);
|
|
14
|
+
assert.equal(_internal.isOptionalInstall([], { DEEPSEEK_TUI_OPTIONAL_INSTALL: "1" }), true);
|
|
15
|
+
assert.equal(_internal.isOptionalInstall([], { DEEPSEEK_OPTIONAL_INSTALL: "1" }), true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("optional mode only changes install-time defaults", () => {
|
|
19
|
+
assert.equal(_internal.maxAttempts("install", { DEEPSEEK_TUI_OPTIONAL_INSTALL: "1" }), 1);
|
|
20
|
+
assert.equal(_internal.maxAttempts("runtime", { DEEPSEEK_TUI_OPTIONAL_INSTALL: "1" }), 5);
|
|
21
|
+
assert.equal(_internal.defaultTimeoutMs("install", { DEEPSEEK_TUI_OPTIONAL_INSTALL: "1" }), 15_000);
|
|
22
|
+
assert.equal(_internal.defaultTimeoutMs("runtime", { DEEPSEEK_TUI_OPTIONAL_INSTALL: "1" }), 300_000);
|
|
23
|
+
assert.equal(_internal.defaultStallMs("install", { DEEPSEEK_TUI_OPTIONAL_INSTALL: "1" }), 5_000);
|
|
24
|
+
assert.equal(_internal.defaultStallMs("runtime", { DEEPSEEK_TUI_OPTIONAL_INSTALL: "1" }), 30_000);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("optional install only swallows retryable download failures", () => {
|
|
28
|
+
const socketHangUp = new Error("socket hang up");
|
|
29
|
+
assert.equal(
|
|
30
|
+
_internal.shouldIgnoreInstallFailure("install", socketHangUp, ["--optional"], {}),
|
|
31
|
+
true,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const timedOut = new Error("download exceeded total timeout of 15000 ms");
|
|
35
|
+
timedOut.code = "EDOWNLOADTIMEOUT";
|
|
36
|
+
assert.equal(
|
|
37
|
+
_internal.shouldIgnoreInstallFailure("install", timedOut, ["--optional"], {}),
|
|
38
|
+
true,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const unsupported = new Error("Unsupported platform: freebsd");
|
|
42
|
+
assert.equal(
|
|
43
|
+
_internal.shouldIgnoreInstallFailure("install", unsupported, ["--optional"], {}),
|
|
44
|
+
false,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const badChecksum = new Error("Checksum mismatch for deepseek-linux-x64");
|
|
48
|
+
badChecksum.nonRetryable = true;
|
|
49
|
+
assert.equal(
|
|
50
|
+
_internal.shouldIgnoreInstallFailure("install", badChecksum, ["--optional"], {}),
|
|
51
|
+
false,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const glibc = new Error("requires glibc 2.34 or newer");
|
|
55
|
+
glibc.nonRetryable = true;
|
|
56
|
+
assert.equal(
|
|
57
|
+
_internal.shouldIgnoreInstallFailure("install", glibc, ["--optional"], {}),
|
|
58
|
+
false,
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("optional install still swallows wrapped http 5xx failures", async () => {
|
|
63
|
+
const previous = process.env.DEEPSEEK_TUI_OPTIONAL_INSTALL;
|
|
64
|
+
process.env.DEEPSEEK_TUI_OPTIONAL_INSTALL = "1";
|
|
65
|
+
const http5xx = new Error("Request failed with status 502: https://example.invalid");
|
|
66
|
+
http5xx.name = "HttpStatusError";
|
|
67
|
+
http5xx.status = 502;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await assert.rejects(
|
|
71
|
+
_internal.withRetry("fetch https://example.invalid", async () => {
|
|
72
|
+
throw http5xx;
|
|
73
|
+
}, "install"),
|
|
74
|
+
(wrapped) => {
|
|
75
|
+
assert.equal(wrapped.name, "HttpStatusError");
|
|
76
|
+
assert.equal(wrapped.status, 502);
|
|
77
|
+
assert.equal(
|
|
78
|
+
_internal.shouldIgnoreInstallFailure("install", wrapped, ["--optional"], {}),
|
|
79
|
+
true,
|
|
80
|
+
);
|
|
81
|
+
return true;
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
} finally {
|
|
85
|
+
if (previous === undefined) {
|
|
86
|
+
delete process.env.DEEPSEEK_TUI_OPTIONAL_INSTALL;
|
|
87
|
+
} else {
|
|
88
|
+
process.env.DEEPSEEK_TUI_OPTIONAL_INSTALL = previous;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
package/test/run.test.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const test = require("node:test");
|
|
3
|
+
|
|
4
|
+
const { _internal } = require("../scripts/run");
|
|
5
|
+
|
|
6
|
+
test("version fallback handles only version flags", () => {
|
|
7
|
+
assert.equal(_internal.isVersionFlag(["--version"]), true);
|
|
8
|
+
assert.equal(_internal.isVersionFlag(["-V"]), true);
|
|
9
|
+
assert.equal(_internal.isVersionFlag(["-v"]), false);
|
|
10
|
+
assert.equal(_internal.isVersionFlag(["--verbose"]), false);
|
|
11
|
+
});
|