deepseek-tui 0.8.17 → 0.8.20
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 +9 -5
- package/package.json +3 -3
- package/scripts/install.js +121 -33
- package/test/install.test.js +1 -1
- package/test/postinstall.test.js +91 -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
|
|
|
@@ -75,7 +78,8 @@ build-from-source guide.
|
|
|
75
78
|
must contain `deepseek-artifacts-sha256.txt` and the platform binaries.
|
|
76
79
|
- Set `DEEPSEEK_TUI_FORCE_DOWNLOAD=1` to force download even when the cached binary is already present.
|
|
77
80
|
- Set `DEEPSEEK_TUI_DISABLE_INSTALL=1` to skip install-time download.
|
|
78
|
-
- 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`.
|
|
79
83
|
|
|
80
84
|
## Release integrity
|
|
81
85
|
|
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.20",
|
|
4
|
+
"deepseekBinaryVersion": "0.8.20",
|
|
5
5
|
"description": "Install and run deepseek and deepseek-tui binaries from GitHub release artifacts.",
|
|
6
6
|
"author": "Hmbown",
|
|
7
7
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
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
33
|
"prepack": "node scripts/install.js",
|
|
34
34
|
"test": "node --test test/*.test.js"
|
package/scripts/install.js
CHANGED
|
@@ -35,14 +35,19 @@ const pkg = require("../package.json");
|
|
|
35
35
|
|
|
36
36
|
const DEFAULT_TIMEOUT_MS = 300_000; // 5 minutes per attempt
|
|
37
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
|
|
38
40
|
const MAX_ATTEMPTS = 5;
|
|
41
|
+
const OPTIONAL_MAX_ATTEMPTS = 1; // runtime keeps the full retry budget on first launch
|
|
39
42
|
const BASE_BACKOFF_MS = 1_000;
|
|
40
43
|
|
|
41
44
|
const RETRYABLE_NET_CODES = new Set([
|
|
42
45
|
"ECONNRESET",
|
|
43
46
|
"ECONNREFUSED",
|
|
47
|
+
"EDOWNLOADTIMEOUT",
|
|
44
48
|
"ETIMEDOUT",
|
|
45
49
|
"EAI_AGAIN",
|
|
50
|
+
"ENOTFOUND",
|
|
46
51
|
"ENETUNREACH",
|
|
47
52
|
"EHOSTUNREACH",
|
|
48
53
|
"EPIPE",
|
|
@@ -86,6 +91,38 @@ function resolveRepo() {
|
|
|
86
91
|
return process.env.DEEPSEEK_TUI_GITHUB_REPO || process.env.DEEPSEEK_GITHUB_REPO || "Hmbown/DeepSeek-TUI";
|
|
87
92
|
}
|
|
88
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
|
+
|
|
89
126
|
function binaryPaths() {
|
|
90
127
|
const { deepseek, tui } = detectBinaryNames();
|
|
91
128
|
const releaseDir = releaseBinaryDirectory();
|
|
@@ -174,17 +211,17 @@ function envInt(name, fallback) {
|
|
|
174
211
|
return parsed;
|
|
175
212
|
}
|
|
176
213
|
|
|
177
|
-
function downloadTimeoutMs() {
|
|
214
|
+
function downloadTimeoutMs(context = "runtime") {
|
|
178
215
|
return envInt(
|
|
179
216
|
"DEEPSEEK_TUI_DOWNLOAD_TIMEOUT_MS",
|
|
180
|
-
envInt("DEEPSEEK_DOWNLOAD_TIMEOUT_MS",
|
|
217
|
+
envInt("DEEPSEEK_DOWNLOAD_TIMEOUT_MS", defaultTimeoutMs(context)),
|
|
181
218
|
);
|
|
182
219
|
}
|
|
183
220
|
|
|
184
|
-
function downloadStallMs() {
|
|
221
|
+
function downloadStallMs(context = "runtime") {
|
|
185
222
|
return envInt(
|
|
186
223
|
"DEEPSEEK_TUI_DOWNLOAD_STALL_MS",
|
|
187
|
-
envInt("DEEPSEEK_DOWNLOAD_STALL_MS",
|
|
224
|
+
envInt("DEEPSEEK_DOWNLOAD_STALL_MS", defaultStallMs(context)),
|
|
188
225
|
);
|
|
189
226
|
}
|
|
190
227
|
|
|
@@ -412,13 +449,15 @@ function connectThroughProxy(proxy, targetHost, targetPort, timeoutMs) {
|
|
|
412
449
|
// ────────────────────────────────────────────────────────────────────────────
|
|
413
450
|
|
|
414
451
|
function httpRequest(rawUrl, opts = {}) {
|
|
452
|
+
const context =
|
|
453
|
+
opts.context === undefined || opts.context === null ? "runtime" : opts.context;
|
|
415
454
|
const totalTimeoutMs =
|
|
416
455
|
opts.totalTimeoutMs === undefined || opts.totalTimeoutMs === null
|
|
417
|
-
? downloadTimeoutMs()
|
|
456
|
+
? downloadTimeoutMs(context)
|
|
418
457
|
: opts.totalTimeoutMs;
|
|
419
458
|
const stallMs =
|
|
420
459
|
opts.stallMs === undefined || opts.stallMs === null
|
|
421
|
-
? downloadStallMs()
|
|
460
|
+
? downloadStallMs(context)
|
|
422
461
|
: opts.stallMs;
|
|
423
462
|
|
|
424
463
|
return new Promise((resolve, reject) => {
|
|
@@ -708,7 +747,12 @@ function isRetryable(err) {
|
|
|
708
747
|
if (err.nonRetryable) return false;
|
|
709
748
|
if (err instanceof NonRetryableError) return false;
|
|
710
749
|
if (err instanceof DownloadTimeoutError) return true;
|
|
711
|
-
|
|
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
|
+
) {
|
|
712
756
|
return err.status >= 500;
|
|
713
757
|
}
|
|
714
758
|
if (err.code && RETRYABLE_NET_CODES.has(err.code)) return true;
|
|
@@ -731,27 +775,42 @@ function sleep(ms) {
|
|
|
731
775
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
732
776
|
}
|
|
733
777
|
|
|
734
|
-
async function withRetry(label, fn) {
|
|
778
|
+
async function withRetry(label, fn, context = "runtime") {
|
|
735
779
|
let lastErr;
|
|
736
|
-
|
|
780
|
+
const attemptLimit = maxAttempts(context);
|
|
781
|
+
for (let attempt = 1; attempt <= attemptLimit; attempt++) {
|
|
737
782
|
try {
|
|
738
783
|
return await fn(attempt);
|
|
739
784
|
} catch (err) {
|
|
740
785
|
lastErr = err;
|
|
741
|
-
if (!isRetryable(err) || attempt ===
|
|
786
|
+
if (!isRetryable(err) || attempt === attemptLimit) {
|
|
742
787
|
break;
|
|
743
788
|
}
|
|
744
789
|
const wait = backoffDelay(attempt);
|
|
745
790
|
logInfo(
|
|
746
|
-
`${label} failed (attempt ${attempt}/${
|
|
791
|
+
`${label} failed (attempt ${attempt}/${attemptLimit}): ${err.message}; retrying in ${wait} ms`,
|
|
747
792
|
);
|
|
748
793
|
await sleep(wait);
|
|
749
794
|
}
|
|
750
795
|
}
|
|
751
796
|
const msg = lastErr && lastErr.message ? lastErr.message : String(lastErr);
|
|
752
797
|
const wrapped = new Error(
|
|
753
|
-
`${label} failed after ${
|
|
798
|
+
`${label} failed after ${attemptLimit} attempt(s): ${msg}`,
|
|
754
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
|
+
}
|
|
755
814
|
if (lastErr && lastErr.stack) {
|
|
756
815
|
wrapped.cause = lastErr;
|
|
757
816
|
}
|
|
@@ -762,7 +821,7 @@ async function withRetry(label, fn) {
|
|
|
762
821
|
// Public download primitives (now retry + progress aware)
|
|
763
822
|
// ────────────────────────────────────────────────────────────────────────────
|
|
764
823
|
|
|
765
|
-
async function followRedirects(url, opts) {
|
|
824
|
+
async function followRedirects(url, opts = {}) {
|
|
766
825
|
const maxRedirects = 10;
|
|
767
826
|
let current = url;
|
|
768
827
|
for (let hop = 0; hop < maxRedirects; hop++) {
|
|
@@ -807,17 +866,21 @@ function streamToFile(response, destination, progress) {
|
|
|
807
866
|
async function download(url, destination, options = {}) {
|
|
808
867
|
await mkdir(path.dirname(destination), { recursive: true });
|
|
809
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);
|
|
810
872
|
await withRetry(`download ${assetName}`, async (attempt) => {
|
|
811
873
|
const result = await followRedirects(url, {
|
|
812
|
-
|
|
813
|
-
|
|
874
|
+
context,
|
|
875
|
+
totalTimeoutMs: downloadTimeoutMs(context),
|
|
876
|
+
stallMs: downloadStallMs(context),
|
|
814
877
|
});
|
|
815
878
|
const response = result.response;
|
|
816
879
|
const lenHeader = response.headers["content-length"];
|
|
817
880
|
const total = lenHeader ? Number.parseInt(lenHeader, 10) : 0;
|
|
818
881
|
const progress = createProgressReporter(assetName, Number.isFinite(total) ? total : 0);
|
|
819
882
|
if (attempt > 1) {
|
|
820
|
-
logInfo(`retry attempt ${attempt}/${
|
|
883
|
+
logInfo(`retry attempt ${attempt}/${attemptLimit} for ${assetName}`);
|
|
821
884
|
}
|
|
822
885
|
try {
|
|
823
886
|
await streamToFile(response, destination, progress);
|
|
@@ -831,14 +894,17 @@ async function download(url, destination, options = {}) {
|
|
|
831
894
|
throw err;
|
|
832
895
|
}
|
|
833
896
|
progress.finish();
|
|
834
|
-
});
|
|
897
|
+
}, context);
|
|
835
898
|
}
|
|
836
899
|
|
|
837
|
-
async function downloadText(url) {
|
|
900
|
+
async function downloadText(url, options = {}) {
|
|
901
|
+
const context =
|
|
902
|
+
options.context === undefined || options.context === null ? "runtime" : options.context;
|
|
838
903
|
return withRetry(`fetch ${url}`, async () => {
|
|
839
904
|
const result = await followRedirects(url, {
|
|
840
|
-
|
|
841
|
-
|
|
905
|
+
context,
|
|
906
|
+
totalTimeoutMs: downloadTimeoutMs(context),
|
|
907
|
+
stallMs: downloadStallMs(context),
|
|
842
908
|
});
|
|
843
909
|
const response = result.response;
|
|
844
910
|
response.setEncoding("utf8");
|
|
@@ -861,7 +927,7 @@ async function downloadText(url) {
|
|
|
861
927
|
});
|
|
862
928
|
response.on("error", reject);
|
|
863
929
|
});
|
|
864
|
-
});
|
|
930
|
+
}, context);
|
|
865
931
|
}
|
|
866
932
|
|
|
867
933
|
async function readLocalVersion(file) {
|
|
@@ -913,11 +979,11 @@ async function verifyChecksum(filePath, assetName, checksums) {
|
|
|
913
979
|
}
|
|
914
980
|
}
|
|
915
981
|
|
|
916
|
-
async function loadChecksums(version, repo) {
|
|
917
|
-
return parseChecksumManifest(await downloadText(checksumManifestUrl(version, repo)));
|
|
982
|
+
async function loadChecksums(version, repo, options = {}) {
|
|
983
|
+
return parseChecksumManifest(await downloadText(checksumManifestUrl(version, repo), options));
|
|
918
984
|
}
|
|
919
985
|
|
|
920
|
-
async function ensureBinary(targetPath, assetName, version, repo, getChecksums) {
|
|
986
|
+
async function ensureBinary(targetPath, assetName, version, repo, getChecksums, options = {}) {
|
|
921
987
|
const marker = `${targetPath}.version`;
|
|
922
988
|
const downloadIfNeeded =
|
|
923
989
|
process.env.DEEPSEEK_TUI_FORCE_DOWNLOAD === "1" || process.env.DEEPSEEK_FORCE_DOWNLOAD === "1";
|
|
@@ -933,7 +999,7 @@ async function ensureBinary(targetPath, assetName, version, repo, getChecksums)
|
|
|
933
999
|
const checksums = await getChecksums();
|
|
934
1000
|
const url = releaseAssetUrl(assetName, version, repo);
|
|
935
1001
|
const destination = `${targetPath}.${process.pid}.${Date.now()}.download`;
|
|
936
|
-
await download(url, destination, { assetName });
|
|
1002
|
+
await download(url, destination, { assetName, context: options.context });
|
|
937
1003
|
try {
|
|
938
1004
|
await verifyChecksum(destination, assetName, checksums);
|
|
939
1005
|
preflightGlibc(destination);
|
|
@@ -949,7 +1015,21 @@ async function ensureBinary(targetPath, assetName, version, repo, getChecksums)
|
|
|
949
1015
|
return targetPath;
|
|
950
1016
|
}
|
|
951
1017
|
|
|
952
|
-
|
|
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;
|
|
953
1033
|
if (process.env.DEEPSEEK_TUI_DISABLE_INSTALL === "1" || process.env.DEEPSEEK_DISABLE_INSTALL === "1") {
|
|
954
1034
|
return;
|
|
955
1035
|
}
|
|
@@ -962,19 +1042,19 @@ async function run() {
|
|
|
962
1042
|
let checksumsPromise;
|
|
963
1043
|
const getChecksums = () => {
|
|
964
1044
|
if (!checksumsPromise) {
|
|
965
|
-
checksumsPromise = loadChecksums(version, repo);
|
|
1045
|
+
checksumsPromise = loadChecksums(version, repo, { context });
|
|
966
1046
|
}
|
|
967
1047
|
return checksumsPromise;
|
|
968
1048
|
};
|
|
969
1049
|
|
|
970
1050
|
await Promise.all([
|
|
971
|
-
ensureBinary(paths.deepseek.target, paths.deepseek.asset, version, repo, getChecksums),
|
|
972
|
-
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 }),
|
|
973
1053
|
]);
|
|
974
1054
|
}
|
|
975
1055
|
|
|
976
1056
|
async function getBinaryPath(name) {
|
|
977
|
-
await run();
|
|
1057
|
+
await run({ context: "runtime" });
|
|
978
1058
|
const paths = binaryPaths();
|
|
979
1059
|
if (name === "deepseek") {
|
|
980
1060
|
return paths.deepseek.target;
|
|
@@ -989,18 +1069,26 @@ module.exports = {
|
|
|
989
1069
|
getBinaryPath,
|
|
990
1070
|
installFailureHint,
|
|
991
1071
|
run,
|
|
1072
|
+
_internal: {
|
|
1073
|
+
isOptionalInstall,
|
|
1074
|
+
shouldIgnoreInstallFailure,
|
|
1075
|
+
defaultTimeoutMs,
|
|
1076
|
+
defaultStallMs,
|
|
1077
|
+
maxAttempts,
|
|
1078
|
+
withRetry,
|
|
1079
|
+
},
|
|
992
1080
|
};
|
|
993
1081
|
|
|
994
1082
|
if (require.main === module) {
|
|
995
|
-
run().catch((error) => {
|
|
1083
|
+
run({ context: "install" }).catch((error) => {
|
|
996
1084
|
console.error("deepseek-tui install failed:", error.message);
|
|
997
1085
|
const hint = installFailureHint(error);
|
|
998
1086
|
if (hint) {
|
|
999
1087
|
console.error(hint);
|
|
1000
1088
|
}
|
|
1001
|
-
if (
|
|
1089
|
+
if (shouldIgnoreInstallFailure("install", error)) {
|
|
1002
1090
|
console.error(
|
|
1003
|
-
"
|
|
1091
|
+
"Optional install enabled; continuing without a usable binary. The download will be retried on first run.",
|
|
1004
1092
|
);
|
|
1005
1093
|
process.exit(0);
|
|
1006
1094
|
}
|
package/test/install.test.js
CHANGED
|
@@ -29,7 +29,7 @@ test("install failure hint explains release base override for blocked GitHub dow
|
|
|
29
29
|
try {
|
|
30
30
|
const error = Object.assign(
|
|
31
31
|
new Error(
|
|
32
|
-
"fetch https://github.com/Hmbown/DeepSeek-TUI/releases/download/v0.8.
|
|
32
|
+
"fetch https://github.com/Hmbown/DeepSeek-TUI/releases/download/v0.8.19/deepseek-artifacts-sha256.txt failed after 5 attempts:\ngetaddrinfo ENOTFOUND github.com",
|
|
33
33
|
),
|
|
34
34
|
{ code: "ENOTFOUND" },
|
|
35
35
|
);
|
|
@@ -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
|
+
});
|