From f2881e7e3d4ce26ae674edc6caba37693e757c51 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Tue, 3 Mar 2026 09:57:56 -0600 Subject: [PATCH] feat: add npm package and update install docs - Add npm/deepseek-tui package that downloads prebuilt binaries from GitHub releases (supports macOS, Linux, Windows) - Published as deepseek-tui@0.3.28 on npmjs.com - Update README to feature npm as primary install method - Add npm badge --- .gitignore | 1 + README.md | 15 ++- npm/deepseek-tui/.gitignore | 1 + npm/deepseek-tui/README.md | 31 ++++++ npm/deepseek-tui/bin/deepseek-tui.js | 8 ++ npm/deepseek-tui/bin/deepseek.js | 8 ++ npm/deepseek-tui/package.json | 42 ++++++++ npm/deepseek-tui/scripts/artifacts.js | 53 ++++++++++ npm/deepseek-tui/scripts/install.js | 142 ++++++++++++++++++++++++++ npm/deepseek-tui/scripts/run.js | 36 +++++++ 10 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 npm/deepseek-tui/.gitignore create mode 100644 npm/deepseek-tui/README.md create mode 100755 npm/deepseek-tui/bin/deepseek-tui.js create mode 100755 npm/deepseek-tui/bin/deepseek.js create mode 100644 npm/deepseek-tui/package.json create mode 100644 npm/deepseek-tui/scripts/artifacts.js create mode 100644 npm/deepseek-tui/scripts/install.js create mode 100644 npm/deepseek-tui/scripts/run.js diff --git a/.gitignore b/.gitignore index 010508bd..88cb49a2 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ AI_HANDOFF.md result.json count_deps.py project_overhaul_prompt.md +prompt_for_release_agent.md .codex/ docs/rlm-paper.txt diff --git a/README.md b/README.md index a35e0860..b12b6bbb 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ A terminal-native TUI and CLI for [DeepSeek](https://platform.deepseek.com) mode [![CI](https://github.com/Hmbown/DeepSeek-TUI/actions/workflows/ci.yml/badge.svg)](https://github.com/Hmbown/DeepSeek-TUI/actions/workflows/ci.yml) [![crates.io](https://img.shields.io/crates/v/deepseek-tui)](https://crates.io/crates/deepseek-tui) +[![npm](https://img.shields.io/npm/v/deepseek-tui)](https://www.npmjs.com/package/deepseek-tui)

DeepSeek CLI @@ -25,17 +26,29 @@ Three modes: ## Install +```bash +# Recommended — no Rust toolchain needed +npm install -g deepseek-tui +``` + +This downloads prebuilt binaries for your platform (macOS, Linux, Windows). After install, both `deepseek` and `deepseek-tui` commands are available. + +

+Other install methods + ```bash # From crates.io (requires Rust 1.85+) cargo install deepseek-tui --locked -# Or from source +# From source git clone https://github.com/Hmbown/DeepSeek-TUI.git cd DeepSeek-TUI cargo install --path crates/tui --locked # TUI (interactive terminal) cargo install --path crates/cli --locked # CLI (dispatcher + server) ``` +
+ ## Setup Create `~/.deepseek/config.toml`: diff --git a/npm/deepseek-tui/.gitignore b/npm/deepseek-tui/.gitignore new file mode 100644 index 00000000..f98d9c9f --- /dev/null +++ b/npm/deepseek-tui/.gitignore @@ -0,0 +1 @@ +bin/downloads/ diff --git a/npm/deepseek-tui/README.md b/npm/deepseek-tui/README.md new file mode 100644 index 00000000..4bcc173c --- /dev/null +++ b/npm/deepseek-tui/README.md @@ -0,0 +1,31 @@ +# deepseek-tui + +This package installs the `deepseek` and `deepseek-tui` binaries from the +`DeepSeek-TUI` GitHub release artifacts and exposes them as Node-compatible +console entry points. + +## Install + +```bash +npm install deepseek-tui +# or +pnpm add deepseek-tui +``` + +This runs `postinstall`, downloads the platform-specific binaries for version +`0.3.28`, and makes `deepseek` and `deepseek-tui` available on your PATH. + +## Supported platforms + +- Linux x64 +- macOS x64 / arm64 +- Windows x64 + +## Notes + +- Binaries come directly from release assets in + `https://github.com/Hmbown/DeepSeek-TUI/releases`. +- Set `DEEPSEEK_VERSION` to install a different release version (defaults to package version). +- Set `DEEPSEEK_GITHUB_REPO` to override the repo source (defaults to `Hmbown/DeepSeek-TUI`). +- Set `DEEPSEEK_TUI_FORCE_DOWNLOAD=1` to force download even when the cached binary is already present. +- Set `DEEPSEEK_TUI_DISABLE_INSTALL=1` to skip install-time download. diff --git a/npm/deepseek-tui/bin/deepseek-tui.js b/npm/deepseek-tui/bin/deepseek-tui.js new file mode 100755 index 00000000..00d81883 --- /dev/null +++ b/npm/deepseek-tui/bin/deepseek-tui.js @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +const { runDeepseekTui } = require("../scripts/run"); + +runDeepseekTui().catch((error) => { + console.error("Failed to start deepseek-tui:", error.message); + process.exit(1); +}); diff --git a/npm/deepseek-tui/bin/deepseek.js b/npm/deepseek-tui/bin/deepseek.js new file mode 100755 index 00000000..c0aaf5a6 --- /dev/null +++ b/npm/deepseek-tui/bin/deepseek.js @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +const { runDeepseek } = require("../scripts/run"); + +runDeepseek().catch((error) => { + console.error("Failed to start deepseek:", error.message); + process.exit(1); +}); diff --git a/npm/deepseek-tui/package.json b/npm/deepseek-tui/package.json new file mode 100644 index 00000000..6fb07f71 --- /dev/null +++ b/npm/deepseek-tui/package.json @@ -0,0 +1,42 @@ +{ + "name": "deepseek-tui", + "version": "0.3.28", + "description": "Install and run deepseek and deepseek-tui binaries from GitHub release artifacts.", + "author": "Hmbown", + "license": "MIT", + "homepage": "https://github.com/Hmbown/DeepSeek-TUI", + "repository": { + "type": "git", + "url": "git+https://github.com/Hmbown/DeepSeek-TUI.git" + }, + "bugs": { + "url": "https://github.com/Hmbown/DeepSeek-TUI/issues" + }, + "keywords": [ + "deepseek", + "cli", + "tui", + "rust", + "binary", + "terminal" + ], + "type": "commonjs", + "bin": { + "deepseek": "bin/deepseek.js", + "deepseek-tui": "bin/deepseek-tui.js" + }, + "scripts": { + "postinstall": "node scripts/install.js", + "prepack": "node scripts/install.js" + }, + "engines": { + "node": ">=18" + }, + "preferGlobal": true, + "files": [ + "bin/*.js", + "scripts/*.js", + "README.md", + "package.json" + ] +} diff --git a/npm/deepseek-tui/scripts/artifacts.js b/npm/deepseek-tui/scripts/artifacts.js new file mode 100644 index 00000000..c79ad352 --- /dev/null +++ b/npm/deepseek-tui/scripts/artifacts.js @@ -0,0 +1,53 @@ +const path = require("path"); +const os = require("os"); + +const ASSET_MATRIX = { + linux: { + x64: ["deepseek-linux-x64", "deepseek-tui-linux-x64"], + default: ["deepseek-linux-x64", "deepseek-tui-linux-x64"], + }, + darwin: { + x64: ["deepseek-macos-x64", "deepseek-tui-macos-x64"], + arm64: ["deepseek-macos-arm64", "deepseek-tui-macos-arm64"], + default: ["deepseek-macos-x64", "deepseek-tui-macos-x64"], + }, + win32: { + x64: ["deepseek-windows-x64.exe", "deepseek-tui-windows-x64.exe"], + default: ["deepseek-windows-x64.exe", "deepseek-tui-windows-x64.exe"], + }, +}; + +function detectBinaryNames() { + const platform = os.platform(); + const arch = os.arch(); + const defaults = ASSET_MATRIX[platform]; + if (!defaults) { + throw new Error(`Unsupported platform: ${platform}`); + } + const pair = defaults[arch] || defaults.default; + return { + platform, + arch, + deepseek: pair[0], + tui: pair[1], + }; +} + +function executableName(base, platform) { + return platform === "win32" ? `${base}.exe` : base; +} + +function releaseAssetUrl(baseName, version, repo = "Hmbown/DeepSeek-TUI") { + return `https://github.com/${repo}/releases/download/v${version}/${baseName}`; +} + +function releaseBinaryDirectory() { + return path.join(__dirname, "..", "bin", "downloads"); +} + +module.exports = { + detectBinaryNames, + executableName, + releaseAssetUrl, + releaseBinaryDirectory, +}; diff --git a/npm/deepseek-tui/scripts/install.js b/npm/deepseek-tui/scripts/install.js new file mode 100644 index 00000000..2bd6a358 --- /dev/null +++ b/npm/deepseek-tui/scripts/install.js @@ -0,0 +1,142 @@ +const fs = require("fs"); +const https = require("https"); +const http = require("http"); +const { mkdir, chmod, stat, rename, readFile, writeFile } = fs.promises; +const { createWriteStream } = fs; +const { pipeline } = require("stream/promises"); +const path = require("path"); + +const { + detectBinaryNames, + releaseAssetUrl, + releaseBinaryDirectory, +} = require("./artifacts"); + +function resolvePackageVersion() { + const pkg = require("../package.json"); + return process.env.DEEPSEEK_TUI_VERSION || process.env.DEEPSEEK_VERSION || pkg.version; +} + +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 readLocalVersion(file) { + return readFile(file, "utf8").catch(() => ""); +} + +async function fileExists(file) { + try { + const result = await stat(file); + return result.isFile(); + } catch { + return false; + } +} + +async function ensureBinary(targetPath, assetName, version, repo) { + 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 url = releaseAssetUrl(assetName, version, repo); + const destination = `${targetPath}.download`; + await download(url, destination); + 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 }); + + await Promise.all([ + ensureBinary(paths.deepseek.target, paths.deepseek.asset, version, repo), + ensureBinary(paths.tui.target, paths.tui.asset, version, repo), + ]); +} + +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); + }); +} diff --git a/npm/deepseek-tui/scripts/run.js b/npm/deepseek-tui/scripts/run.js new file mode 100644 index 00000000..07f18cb5 --- /dev/null +++ b/npm/deepseek-tui/scripts/run.js @@ -0,0 +1,36 @@ +const { spawnSync } = require("child_process"); +const { getBinaryPath } = require("./install"); + +async function run(binaryName) { + const binaryPath = await getBinaryPath(binaryName); + const result = spawnSync(binaryPath, process.argv.slice(2), { + stdio: "inherit", + }); + if (result.error) { + throw result.error; + } + process.exit(result.status ?? 1); +} + +async function runDeepseek() { + await run("deepseek"); +} + +async function runDeepseekTui() { + await run("deepseek-tui"); +} + +module.exports = { + run, + runDeepseek, + runDeepseekTui, +}; + +if (require.main === module) { + const command = process.argv[1] || ""; + if (command.includes("tui")) { + runDeepseekTui(); + } else { + runDeepseek(); + } +}