Files
codewhale/npm/deepseek-tui/scripts/install.js
T
Claude 0e5afe0b01 feat(v0.8.8): linux ARM64 prebuilts + install docs overhaul
Triggered by a Telegram report from a Chinese user trying to deploy
DeepSeek TUI on a HarmonyOS ARM64 thin-and-light: `npm i -g deepseek-tui`
exited with `Unsupported architecture: arm64 on platform linux` because
v0.8.7 only published x64 Linux artifacts. They worked around it with
`cargo install`, but the README never documented that path for ARM users.

This PR closes that gap on three layers:

- **Release workflow** — add `aarch64-unknown-linux-gnu` to the build
  matrix using GitHub's `ubuntu-24.04-arm` runner. v0.8.8 will publish
  `deepseek-linux-arm64` and `deepseek-tui-linux-arm64` alongside the
  existing x64/macOS/Windows assets, plus add the row to the Release
  body's manual-download table.

- **npm wrapper** — uncomment the linux/arm64 row in `ASSET_MATRIX`,
  rewrite the `Unsupported architecture/platform` error to print the
  full `cargo install deepseek-tui-cli deepseek-tui --locked` recipe
  and link to docs/INSTALL.md, and add `DEEPSEEK_TUI_OPTIONAL_INSTALL=1`
  so CI matrices that include unsupported platforms can keep running
  without a binary.

- **Docs** — new docs/INSTALL.md covering every supported platform,
  prebuilt vs. cargo install vs. manual download, cross-compiling x64
  -> ARM64 with `cross` or `gcc-aarch64-linux-gnu`, China mirror setup,
  and a troubleshooting section for the common arm64, MISSING_COMPANION_BINARY,
  and self-update arch-mapping (#503) errors. README and README.zh-CN
  now have an explicit Linux ARM64 quickstart pointing at `cargo install`
  for v0.8.7 today and `npm i -g` for v0.8.8+; the v0.8.7 known-issue
  block is updated to mention both #503 and the missing arm64 prebuilt.

https://claude.ai/code/session_01Fg1FKMtDxVnC4pp6bNBRCS
2026-05-03 04:42:53 +00:00

222 lines
6.2 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, getChecksums) {
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)) {
return targetPath;
}
}
}
const checksums = await getChecksums();
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 });
let checksumsPromise;
const getChecksums = () => {
if (!checksumsPromise) {
checksumsPromise = loadChecksums(version, repo);
}
return checksumsPromise;
};
await Promise.all([
ensureBinary(paths.deepseek.target, paths.deepseek.asset, version, repo, getChecksums),
ensureBinary(paths.tui.target, paths.tui.asset, version, repo, getChecksums),
]);
}
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);
if (process.env.DEEPSEEK_TUI_OPTIONAL_INSTALL === "1") {
console.error(
"DEEPSEEK_TUI_OPTIONAL_INSTALL=1 set; continuing without a usable binary.",
);
process.exit(0);
}
process.exit(1);
});
}