Files
codewhale/npm/deepseek-tui/scripts/install.js
T
Hunter Bown b7bd02d814 feat: DeepSeek V4 support with reasoning-effort control (0.4.0)
Adds first-class DeepSeek V4 Pro and Flash support, updates the default model to deepseek-v4-pro, aligns legacy aliases with the current V4 1M context behavior, and fixes thinking-mode request handling.

Key fixes:
- Send DeepSeek's raw Chat Completions `thinking` parameter at the top level instead of SDK-only `extra_body`.
- Preserve assistant `reasoning_content` for all prior thinking-mode tool-call turns so subsequent requests satisfy DeepSeek V4's replay requirement.
- Fix npm wrapper concurrent first-run downloads by using per-process temporary download paths.
- Add `.mailmap` so historical bot-attributed commits aggregate under Hunter Bown where mailmap is honored.

Verified with the full local Rust gate, live DeepSeek V4 smoke, npm wrapper temp-install smoke, and green PR CI across Linux, macOS, and Windows.
2026-04-23 22:53:20 -05:00

209 lines
5.9 KiB
JavaScript

const fs = require("fs");
const https = require("https");
const http = require("http");
const crypto = require("crypto");
const { mkdir, chmod, stat, rename, readFile, unlink, writeFile } = fs.promises;
const { createWriteStream } = fs;
const { pipeline } = require("stream/promises");
const path = require("path");
const {
checksumManifestUrl,
detectBinaryNames,
releaseAssetUrl,
releaseBinaryDirectory,
} = require("./artifacts");
const pkg = require("../package.json");
function resolvePackageVersion() {
const configuredVersion =
process.env.DEEPSEEK_TUI_VERSION ||
process.env.DEEPSEEK_VERSION ||
pkg.deepseekBinaryVersion ||
pkg.version;
return String(configuredVersion).trim();
}
function resolveRepo() {
return process.env.DEEPSEEK_TUI_GITHUB_REPO || process.env.DEEPSEEK_GITHUB_REPO || "Hmbown/DeepSeek-TUI";
}
function binaryPaths() {
const { deepseek, tui } = detectBinaryNames();
const releaseDir = releaseBinaryDirectory();
return {
deepseek: {
asset: deepseek,
target: path.join(releaseDir, process.platform === "win32" ? "deepseek.exe" : "deepseek"),
},
tui: {
asset: tui,
target: path.join(releaseDir, process.platform === "win32" ? "deepseek-tui.exe" : "deepseek-tui"),
},
};
}
async function httpGet(url) {
const client = url.startsWith("https:") ? https : http;
const response = await new Promise((resolve, reject) => {
client.get(url, (res) => {
const status = res.statusCode || 0;
if (status >= 300 && status < 400 && res.headers.location) {
resolve({ redirect: res.headers.location, response: null });
return;
}
if (status !== 200) {
reject(new Error(`Request failed with status ${status}: ${url}`));
return;
}
resolve({ redirect: null, response: res });
}).on("error", reject);
});
return response;
}
async function download(url, destination) {
const resolved = await httpGet(url);
if (resolved.redirect) {
return download(resolved.redirect, destination);
}
await mkdir(path.dirname(destination), { recursive: true });
await pipeline(resolved.response, createWriteStream(destination));
}
async function downloadText(url) {
const resolved = await httpGet(url);
if (resolved.redirect) {
return downloadText(resolved.redirect);
}
const chunks = [];
resolved.response.setEncoding("utf8");
for await (const chunk of resolved.response) {
chunks.push(chunk);
}
return chunks.join("");
}
async function readLocalVersion(file) {
return readFile(file, "utf8").catch(() => "");
}
async function fileExists(file) {
try {
const result = await stat(file);
return result.isFile();
} catch {
return false;
}
}
function parseChecksumManifest(text) {
const checksums = new Map();
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed) {
continue;
}
const match = trimmed.match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/);
if (!match) {
throw new Error(`Invalid checksum manifest line: ${trimmed}`);
}
checksums.set(match[2], match[1].toLowerCase());
}
return checksums;
}
async function sha256File(filePath) {
const content = await readFile(filePath);
return crypto.createHash("sha256").update(content).digest("hex");
}
async function verifyChecksum(filePath, assetName, checksums) {
const expected = checksums.get(assetName);
if (!expected) {
throw new Error(`Checksum manifest is missing ${assetName}`);
}
const actual = await sha256File(filePath);
if (actual !== expected) {
throw new Error(
`Checksum mismatch for ${assetName}: expected ${expected}, got ${actual}`,
);
}
}
async function loadChecksums(version, repo) {
return parseChecksumManifest(await downloadText(checksumManifestUrl(version, repo)));
}
async function ensureBinary(targetPath, assetName, version, repo, checksums) {
const marker = `${targetPath}.version`;
const downloadIfNeeded =
process.env.DEEPSEEK_TUI_FORCE_DOWNLOAD === "1" || process.env.DEEPSEEK_FORCE_DOWNLOAD === "1";
if (!downloadIfNeeded) {
const existing = await fileExists(targetPath);
if (existing) {
const markerVersion = await readLocalVersion(marker);
if (markerVersion === String(version)) {
await verifyChecksum(targetPath, assetName, checksums);
return targetPath;
}
}
}
const url = releaseAssetUrl(assetName, version, repo);
const destination = `${targetPath}.${process.pid}.${Date.now()}.download`;
await download(url, destination);
try {
await verifyChecksum(destination, assetName, checksums);
} catch (error) {
await unlink(destination).catch(() => {});
throw error;
}
if (process.platform !== "win32") {
await chmod(destination, 0o755);
}
await rename(destination, targetPath);
await writeFile(marker, String(version), "utf8");
return targetPath;
}
async function run() {
if (process.env.DEEPSEEK_TUI_DISABLE_INSTALL === "1" || process.env.DEEPSEEK_DISABLE_INSTALL === "1") {
return;
}
const version = resolvePackageVersion();
const repo = resolveRepo();
const paths = binaryPaths();
const releaseDir = releaseBinaryDirectory();
await mkdir(releaseDir, { recursive: true });
const checksums = await loadChecksums(version, repo);
await Promise.all([
ensureBinary(paths.deepseek.target, paths.deepseek.asset, version, repo, checksums),
ensureBinary(paths.tui.target, paths.tui.asset, version, repo, checksums),
]);
}
async function getBinaryPath(name) {
await run();
const paths = binaryPaths();
if (name === "deepseek") {
return paths.deepseek.target;
}
if (name === "deepseek-tui") {
return paths.tui.target;
}
throw new Error(`Unknown binary: ${name}`);
}
module.exports = {
getBinaryPath,
run,
};
if (require.main === module) {
run().catch((error) => {
console.error("deepseek-tui install failed:", error.message);
process.exit(1);
});
}