u1s1-cli 1.19.0 → 1.20.0

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.
@@ -0,0 +1,91 @@
1
+ import type { SiteId } from "../site.js";
2
+ export declare const DSH_MIN_NODE = "24.9.0";
3
+ /** 钉死的 Node 24 LTS(2026-11-10 发布的安全更新)。 */
4
+ export declare const NODE24_VERSION = "24.22.3";
5
+ /** node-v24.22.3 官方 SHASUMS256.txt(https://nodejs.org/dist/v24.22.3/SHASUMS256.txt)里对应的条目。 */
6
+ export declare const NODE24_SHA256: Readonly<Record<string, string>>;
7
+ export declare const NODE_BIN_ENV = "U1S1_DSH_NODE";
8
+ export interface NodeArchive {
9
+ /** nodejs.org 的平台名(darwin-arm64、win-x64…) */
10
+ target: string;
11
+ /** 压缩包文件名 */
12
+ file: string;
13
+ /** 解压后的顶层目录 */
14
+ dir: string;
15
+ /** node 可执行文件在顶层目录下的位置(posix 写法) */
16
+ binInDir: string;
17
+ format: "tar.gz" | "zip";
18
+ sha256: string;
19
+ }
20
+ export declare function nodeArchive(platform?: NodeJS.Platform, arch?: string, version?: string): NodeArchive | undefined;
21
+ /** 国际站走官方 nodejs.org;中国站走 npmmirror 的 Node 镜像(同一份文件,按官方 SHA-256 校验)。U1S1_NODE_MIRROR 可覆盖。 */
22
+ export declare function nodeDistBase(site: SiteId, env?: NodeJS.ProcessEnv): string;
23
+ export declare function nodeDownloadUrl(site: SiteId, archive: NodeArchive, version?: string, env?: NodeJS.ProcessEnv): string;
24
+ /** ~/.u1s1/agents/node/<版本>/ 下的 node(解压后只留可执行文件和 LICENSE)。 */
25
+ export declare function managedNodeBin(agentsRoot: string, version?: string, platform?: NodeJS.Platform): string;
26
+ /** `<node> --version` → 版本号;跑不起来返回 undefined。 */
27
+ export declare function probeNodeVersion(bin: string): string | undefined;
28
+ export declare function nodeVersionOk(version: string | undefined, min?: string): boolean;
29
+ export type NodeSource = "env" | "self" | "path" | "managed" | "download";
30
+ export interface FoundNode {
31
+ bin: string;
32
+ version: string;
33
+ source: NodeSource;
34
+ }
35
+ export declare class NodeRuntimeError extends Error {
36
+ readonly code: "env_unusable" | "declined" | "unsupported_platform" | "download_failed" | "verify_failed";
37
+ constructor(code: "env_unusable" | "declined" | "unsupported_platform" | "download_failed" | "verify_failed", message: string);
38
+ }
39
+ export interface FindNodeDeps {
40
+ env?: NodeJS.ProcessEnv;
41
+ execPath?: string;
42
+ selfVersion?: string;
43
+ platform?: NodeJS.Platform;
44
+ probe?: (bin: string) => string | undefined;
45
+ exists?: (p: string) => boolean;
46
+ }
47
+ /** 不下载:按 1-4 的顺序找够新的 node。U1S1_DSH_NODE 指定了但不能用时抛 env_unusable。 */
48
+ export declare function findNode(agentsRoot: string, deps?: FindNodeDeps): FoundNode | undefined;
49
+ export interface NodeDownloadInfo {
50
+ version: string;
51
+ url: string;
52
+ host: string;
53
+ downloadBytes?: number;
54
+ }
55
+ export interface EnsureNodeOptions {
56
+ agentsRoot: string;
57
+ site: SiteId;
58
+ /** 下载前征得同意;返回 false = 拒绝 */
59
+ consent: (info: NodeDownloadInfo) => Promise<boolean>;
60
+ log: (line: string) => void;
61
+ text: {
62
+ declined: () => string;
63
+ unsupported: () => string;
64
+ downloadFailed: (detail: string) => string;
65
+ verifyFailed: (detail: string) => string;
66
+ downloading: (version: string) => string;
67
+ installed: (version: string) => string;
68
+ };
69
+ env?: NodeJS.ProcessEnv;
70
+ fetchImpl?: typeof fetch;
71
+ /** 测试注入 */
72
+ find?: () => FoundNode | undefined;
73
+ }
74
+ /** 找到够新的 node;没有就征得同意后下载钉死的 Node 24 LTS。 */
75
+ export declare function ensureNode(opts: EnsureNodeOptions): Promise<FoundNode>;
76
+ /** 各平台压缩包约 30-55 MB;提示用。 */
77
+ export declare const NODE24_APPROX_BYTES: number;
78
+ /**
79
+ * 下载 → 按钉死的 SHA-256 校验 → 只解出 node 和 LICENSE → 确认能跑且版本对 → 改名成正式目录。
80
+ * 中途失败只留暂存目录的残渣(下次启动前清掉),不影响已有的东西。
81
+ */
82
+ export declare function downloadNode(input: {
83
+ agentsRoot: string;
84
+ archive: NodeArchive;
85
+ url: string;
86
+ fetchImpl: typeof fetch;
87
+ text: EnsureNodeOptions["text"];
88
+ version?: string;
89
+ }): Promise<string>;
90
+ /** 暂存目录残渣(下载中途被打断)。 */
91
+ export declare function cleanNodeStaging(agentsRoot: string): void;
@@ -0,0 +1,223 @@
1
+ /**
2
+ * dsh(DeepSeek Harness)要的 Node(engines.node >=24.9.0)。便携包只带 Node 22,所以:
3
+ *
4
+ * 1. U1S1_DSH_NODE 指定的 node(排查/测试用;版本不够直接报错,不悄悄换别的)
5
+ * 2. 运行本 CLI 的 Node 就够新(用户自己 npm 装的 u1s1 跑在 Node 24+ 上)
6
+ * 3. PATH 里的 node 够新
7
+ * 4. 已下载到 ~/.u1s1/agents/node/<版本>/ 的官方 Node 24 LTS
8
+ * 5. 都没有:征得同意后从 nodejs.org(中国站走 npmmirror 的 Node 镜像)下载钉死的
9
+ * Node 24 LTS 压缩包,按代码里的 SHA-256(抄自该版本官方 SHASUMS256.txt)校验后解出
10
+ * node 可执行文件和 LICENSE。
11
+ *
12
+ * 只给 dsh 用:不改 PATH、不碰便携包自带的 Node,也不装 npm(npm 仍用 CLI 自带的那份,
13
+ * 只是改由这个 Node 运行,见 runtime.ts installRuntime)。
14
+ */
15
+ import { spawn, spawnSync } from "node:child_process";
16
+ import { createHash, randomBytes } from "node:crypto";
17
+ import { createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "node:fs";
18
+ import { posix, win32 } from "node:path";
19
+ import { pipeline } from "node:stream/promises";
20
+ import { Readable } from "node:stream";
21
+ import { stripSecretEnv } from "../secret-env.js";
22
+ import { compareVersions, findOnPath, normalizeVersion, versionFromOutput } from "./runtime.js";
23
+ export const DSH_MIN_NODE = "24.9.0";
24
+ /** 钉死的 Node 24 LTS(2026-11-10 发布的安全更新)。 */
25
+ export const NODE24_VERSION = "24.22.3";
26
+ /** node-v24.22.3 官方 SHASUMS256.txt(https://nodejs.org/dist/v24.22.3/SHASUMS256.txt)里对应的条目。 */
27
+ export const NODE24_SHA256 = {
28
+ "darwin-arm64": "ff6e17d4874b76d272212b908cc020d6e815fba5a6c7eaaa06add725840307fd",
29
+ "darwin-x64": "977614af0504c81045e7fd85596dbca0e5f951b96681516dadafedcca8c447f7",
30
+ "linux-arm64": "261af2af250dfd7e9f25cf151d1b2713a0adf8f10a9ad116c4947f68627affe8",
31
+ "linux-x64": "061ec3e3062115705b56b51fa9cc330ab50d34b6969387d4e4b1a50c6809acc2",
32
+ "win-arm64": "b3cc74a295b39d4f273271e96f58c06dd2b35757ca12abec64c7d0598b434f14",
33
+ "win-x64": "e612af99f626ab415ea225af6bce5c971feb448cb5cae250bb1c3fa50c5f7d9f",
34
+ };
35
+ export const NODE_BIN_ENV = "U1S1_DSH_NODE";
36
+ export function nodeArchive(platform = process.platform, arch = process.arch, version = NODE24_VERSION) {
37
+ const os = platform === "win32" ? "win" : platform === "darwin" ? "darwin" : platform === "linux" ? "linux" : undefined;
38
+ if (!os || (arch !== "x64" && arch !== "arm64"))
39
+ return undefined;
40
+ const target = `${os}-${arch}`;
41
+ const sha256 = version === NODE24_VERSION ? NODE24_SHA256[target] : undefined;
42
+ if (!sha256)
43
+ return undefined;
44
+ const dir = `node-v${version}-${target}`;
45
+ const format = os === "win" ? "zip" : "tar.gz";
46
+ return { target, file: `${dir}.${format}`, dir, binInDir: os === "win" ? "node.exe" : "bin/node", format, sha256 };
47
+ }
48
+ /** 国际站走官方 nodejs.org;中国站走 npmmirror 的 Node 镜像(同一份文件,按官方 SHA-256 校验)。U1S1_NODE_MIRROR 可覆盖。 */
49
+ export function nodeDistBase(site, env = process.env) {
50
+ const override = env["U1S1_NODE_MIRROR"]?.trim();
51
+ if (override && /^https?:\/\//.test(override))
52
+ return override.replace(/\/+$/, "");
53
+ return site === "cn" ? "https://cdn.npmmirror.com/binaries/node" : "https://nodejs.org/dist";
54
+ }
55
+ export function nodeDownloadUrl(site, archive, version = NODE24_VERSION, env = process.env) {
56
+ return `${nodeDistBase(site, env)}/v${version}/${archive.file}`;
57
+ }
58
+ function join(...parts) {
59
+ return (process.platform === "win32" ? win32 : posix).join(...parts);
60
+ }
61
+ /** ~/.u1s1/agents/node/<版本>/ 下的 node(解压后只留可执行文件和 LICENSE)。 */
62
+ export function managedNodeBin(agentsRoot, version = NODE24_VERSION, platform = process.platform) {
63
+ return join(agentsRoot, "node", version, platform === "win32" ? "node.exe" : "node");
64
+ }
65
+ /** `<node> --version` → 版本号;跑不起来返回 undefined。 */
66
+ export function probeNodeVersion(bin) {
67
+ try {
68
+ const r = spawnSync(bin, ["--version"], { env: stripSecretEnv(process.env), encoding: "utf8", timeout: 15_000, windowsHide: true });
69
+ return r.status === 0 ? versionFromOutput(r.stdout ?? "") : undefined;
70
+ }
71
+ catch {
72
+ return undefined;
73
+ }
74
+ }
75
+ export function nodeVersionOk(version, min = DSH_MIN_NODE) {
76
+ const v = normalizeVersion(version);
77
+ return !!v && compareVersions(v, min) >= 0;
78
+ }
79
+ export class NodeRuntimeError extends Error {
80
+ code;
81
+ constructor(code, message) {
82
+ super(message);
83
+ this.code = code;
84
+ this.name = "NodeRuntimeError";
85
+ }
86
+ }
87
+ /** 不下载:按 1-4 的顺序找够新的 node。U1S1_DSH_NODE 指定了但不能用时抛 env_unusable。 */
88
+ export function findNode(agentsRoot, deps = {}) {
89
+ const env = deps.env ?? process.env;
90
+ const probe = deps.probe ?? probeNodeVersion;
91
+ const exists = deps.exists ?? existsSync;
92
+ const pinned = env[NODE_BIN_ENV]?.trim();
93
+ if (pinned) {
94
+ const v = probe(pinned);
95
+ if (!v || !nodeVersionOk(v)) {
96
+ throw new NodeRuntimeError("env_unusable", `${NODE_BIN_ENV}=${pinned}: ${v ? `Node ${v} < ${DSH_MIN_NODE}` : "not runnable"}`);
97
+ }
98
+ return { bin: pinned, version: v, source: "env" };
99
+ }
100
+ const selfVersion = deps.selfVersion ?? process.versions.node;
101
+ if (nodeVersionOk(selfVersion))
102
+ return { bin: deps.execPath ?? process.execPath, version: normalizeVersion(selfVersion), source: "self" };
103
+ const onPath = findOnPath("node", { env, platform: deps.platform ?? process.platform, exists });
104
+ if (onPath) {
105
+ const v = probe(onPath);
106
+ if (v && nodeVersionOk(v))
107
+ return { bin: onPath, version: v, source: "path" };
108
+ }
109
+ const managed = managedNodeBin(agentsRoot, NODE24_VERSION, deps.platform ?? process.platform);
110
+ if (exists(managed)) {
111
+ const v = probe(managed);
112
+ if (v && nodeVersionOk(v))
113
+ return { bin: managed, version: v, source: "managed" };
114
+ }
115
+ return undefined;
116
+ }
117
+ /** 找到够新的 node;没有就征得同意后下载钉死的 Node 24 LTS。 */
118
+ export async function ensureNode(opts) {
119
+ const found = (opts.find ?? (() => findNode(opts.agentsRoot, { env: opts.env })))();
120
+ if (found)
121
+ return found;
122
+ const archive = nodeArchive();
123
+ if (!archive)
124
+ throw new NodeRuntimeError("unsupported_platform", opts.text.unsupported());
125
+ const url = nodeDownloadUrl(opts.site, archive, NODE24_VERSION, opts.env);
126
+ const fetchImpl = opts.fetchImpl ?? fetch;
127
+ const ok = await opts.consent({ version: NODE24_VERSION, url, host: new URL(url).host, downloadBytes: NODE24_APPROX_BYTES });
128
+ if (!ok)
129
+ throw new NodeRuntimeError("declined", opts.text.declined());
130
+ opts.log(opts.text.downloading(NODE24_VERSION));
131
+ const bin = await downloadNode({ agentsRoot: opts.agentsRoot, archive, url, fetchImpl, text: opts.text });
132
+ opts.log(opts.text.installed(NODE24_VERSION));
133
+ return { bin, version: NODE24_VERSION, source: "download" };
134
+ }
135
+ /** 各平台压缩包约 30-55 MB;提示用。 */
136
+ export const NODE24_APPROX_BYTES = 55 * 1024 * 1024;
137
+ async function sha256File(file) {
138
+ const hash = createHash("sha256");
139
+ await pipeline(createReadStream(file), hash);
140
+ return hash.digest("hex");
141
+ }
142
+ function runTar(args, cwd) {
143
+ return new Promise((resolve) => {
144
+ const child = spawn("tar", args, { cwd, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
145
+ let output = "";
146
+ child.stdout?.on("data", (c) => { output += c.toString("utf8"); });
147
+ child.stderr?.on("data", (c) => { output += c.toString("utf8"); });
148
+ child.on("error", (e) => resolve({ code: 1, output: output + String(e) }));
149
+ child.on("exit", (code) => resolve({ code: code ?? 1, output }));
150
+ });
151
+ }
152
+ /**
153
+ * 下载 → 按钉死的 SHA-256 校验 → 只解出 node 和 LICENSE → 确认能跑且版本对 → 改名成正式目录。
154
+ * 中途失败只留暂存目录的残渣(下次启动前清掉),不影响已有的东西。
155
+ */
156
+ export async function downloadNode(input) {
157
+ const version = input.version ?? NODE24_VERSION;
158
+ const root = join(input.agentsRoot, "node");
159
+ mkdirSync(root, { recursive: true, mode: 0o700 });
160
+ const staging = join(root, `.staging-${version}-${randomBytes(4).toString("hex")}`);
161
+ mkdirSync(staging, { recursive: true });
162
+ try {
163
+ const archivePath = join(staging, input.archive.file);
164
+ let res;
165
+ try {
166
+ res = await input.fetchImpl(input.url, { signal: AbortSignal.timeout(10 * 60_000) });
167
+ }
168
+ catch (e) {
169
+ throw new NodeRuntimeError("download_failed", input.text.downloadFailed(e instanceof Error ? e.message : String(e)));
170
+ }
171
+ if (!res.ok || !res.body)
172
+ throw new NodeRuntimeError("download_failed", input.text.downloadFailed(`HTTP ${res.status}`));
173
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(archivePath));
174
+ const got = await sha256File(archivePath);
175
+ if (got !== input.archive.sha256) {
176
+ throw new NodeRuntimeError("verify_failed", input.text.verifyFailed(`sha256 ${got} != ${input.archive.sha256}`));
177
+ }
178
+ const members = [`${input.archive.dir}/${input.archive.binInDir}`, `${input.archive.dir}/LICENSE`];
179
+ const extract = await runTar(["-xf", archivePath, ...members], staging);
180
+ if (extract.code !== 0)
181
+ throw new NodeRuntimeError("verify_failed", input.text.verifyFailed(`tar: ${extract.output.trim().slice(-300)}`));
182
+ const extracted = join(staging, input.archive.dir, ...input.archive.binInDir.split("/"));
183
+ const v = probeNodeVersion(extracted);
184
+ if (v !== version)
185
+ throw new NodeRuntimeError("verify_failed", input.text.verifyFailed(`node reports ${v ?? "nothing"}, expected ${version}`));
186
+ const target = join(root, version);
187
+ const finalDir = join(staging, "final");
188
+ mkdirSync(finalDir);
189
+ const binName = process.platform === "win32" ? "node.exe" : "node";
190
+ renameSync(extracted, join(finalDir, binName));
191
+ const license = join(staging, input.archive.dir, "LICENSE");
192
+ if (existsSync(license))
193
+ renameSync(license, join(finalDir, "LICENSE"));
194
+ rmSync(target, { recursive: true, force: true });
195
+ try {
196
+ renameSync(finalDir, target);
197
+ }
198
+ catch (e) {
199
+ // 另一个 u1s1 进程刚装好同一版本:用它的
200
+ if (!existsSync(join(target, binName)))
201
+ throw e;
202
+ }
203
+ return join(target, binName);
204
+ }
205
+ finally {
206
+ rmSync(staging, { recursive: true, force: true });
207
+ }
208
+ }
209
+ /** 暂存目录残渣(下载中途被打断)。 */
210
+ export function cleanNodeStaging(agentsRoot) {
211
+ const root = join(agentsRoot, "node");
212
+ let names;
213
+ try {
214
+ names = readdirSync(root);
215
+ }
216
+ catch {
217
+ return;
218
+ }
219
+ for (const name of names) {
220
+ if (name.startsWith(".staging-"))
221
+ rmSync(join(root, name), { recursive: true, force: true });
222
+ }
223
+ }
@@ -32,7 +32,7 @@ export declare const allowedByBrake: (v: string, brake: RuntimeBrake) => boolean
32
32
  * latest 超过 max_tested 时自然退到 max_tested(它是测过的版本,必然已发布)。
33
33
  */
34
34
  export declare function bestInstallTarget(candidates: Array<string | undefined>, brake: RuntimeBrake): string | undefined;
35
- export type ManagedAgent = "claude" | "codex";
35
+ export type ManagedAgent = "claude" | "codex" | "dsh";
36
36
  export declare const MANAGED_AGENTS: readonly ManagedAgent[];
37
37
  export declare function isManagedAgent(value: unknown): value is ManagedAgent;
38
38
  /** 平台二进制所在的 npm 包:装进 node_modules 的目录名,以及它在 npm 源上的名字和版本。 */
@@ -60,6 +60,16 @@ export interface AgentRuntimeSpec {
60
60
  platformPackage: (version: string, platform: NodeJS.Platform, arch: string, musl: boolean) => PlatformPackageRef | undefined;
61
61
  /** 可执行文件在 node_modules 下的相对路径(posix 写法);不支持的系统/架构返回 undefined */
62
62
  binInNodeModules: (platform: NodeJS.Platform, arch: string, musl: boolean) => string | undefined;
63
+ /**
64
+ * 入口是 JS 文件、要用不低于这个版本的 Node 运行(dsh:engines.node >=24.9.0)。探测版本、npm 的
65
+ * postinstall 和启动都用调用方给的那个 node(EnsureRuntimeOptions.node);没给就当跑不起来。
66
+ */
67
+ minNode?: string;
68
+ /** 没有单独的平台包、npm 元数据量不出整棵依赖树时,首次下载前提示的大致大小(字节) */
69
+ sizeHint?: {
70
+ downloadBytes: number;
71
+ unpackedBytes: number;
72
+ };
63
73
  }
64
74
  /**
65
75
  * 官方包的 bin 就是 `bin/claude.exe`(各平台同名):postinstall 把对应平台的原生二进制
@@ -74,6 +84,15 @@ export declare const CLAUDE_RUNTIME: AgentRuntimeSpec;
74
84
  * 让它提示用 npm 升级;更新由启动器管,不需要)。原生二进制按自己所在目录找 rg 等附带文件。
75
85
  */
76
86
  export declare const CODEX_RUNTIME: AgentRuntimeSpec;
87
+ /** dsh 实测过的版本;网关刹车没给 max_tested_version(老网关、老缓存)时也不越过它(dsh.ts)。 */
88
+ export declare const DSH_TESTED_VERSION = "0.2.2-rc.1";
89
+ /**
90
+ * DeepSeek Harness:`@deepseek-ai/dsh`(MIT)是纯 JS 包,bin 是 `lib/bin.js`,依赖树里的原生模块
91
+ * (node-pty 等)带各平台预编译文件,由 npm 的 postinstall 处理。没有单独的平台包,校验时
92
+ * 主包同时充当「平台包」。engines.node >=24.9.0:便携包自带的 Node 22 跑不了,由 node-runtime.ts
93
+ * 找/下载 Node 24。0.2.2-rc.1 实测:下载约 290 MB,装好约 920 MB。
94
+ */
95
+ export declare const DSH_RUNTIME: AgentRuntimeSpec;
77
96
  export declare function runtimeSpec(agent: ManagedAgent): AgentRuntimeSpec;
78
97
  export interface RuntimePaths {
79
98
  /** ~/.u1s1/agents/<agent> */
@@ -121,7 +140,7 @@ export declare function findOnPath(command: string, opts: {
121
140
  * 跑 `<bin> --version` 取版本。环境里去掉密钥、关掉自动更新;配置目录(CLAUDE_CONFIG_DIR /
122
141
  * CODEX_HOME)指到启动器自己的目录,免得探测用户自己装的客户端时碰到用户的 ~/.claude、~/.codex。
123
142
  */
124
- export declare function probeVersion(bin: string, configDir?: string, spec?: AgentRuntimeSpec): string | undefined;
143
+ export declare function probeVersion(bin: string, configDir?: string, spec?: AgentRuntimeSpec, node?: string): string | undefined;
125
144
  /** 版本高于 max_tested、这次没用上的那份客户端(给用户解释为什么换版本 / 另装一份)。 */
126
145
  export interface SkippedRuntime extends FoundRuntime {
127
146
  maxTestedVersion: string;
@@ -274,6 +293,8 @@ export interface InstallOptions {
274
293
  retryDelayMs?: number;
275
294
  /** 校验时跑 --version 用的隔离配置目录(CLAUDE_CONFIG_DIR / CODEX_HOME) */
276
295
  probeConfigDir?: string;
296
+ /** spec.minNode 时运行入口、npm 及其 postinstall 用的 Node */
297
+ node?: string;
277
298
  }
278
299
  /**
279
300
  * 装一个确切版本:npm install 到暂存目录 → 校验(锁文件里的 integrity 与 registry 元数据一致、
@@ -329,6 +350,8 @@ export interface EnsureRuntimeOptions {
329
350
  install?: (opts: InstallOptions) => Promise<FoundRuntime>;
330
351
  /** 见 PlanInput.autoUpdate;缺省开 */
331
352
  autoUpdate?: boolean;
353
+ /** spec.minNode 的客户端(dsh)用来探测、安装的 Node(node-runtime.ts 找好的) */
354
+ node?: string;
332
355
  }
333
356
  export interface EnsuredRuntime extends FoundRuntime {
334
357
  /** pin 的版本被刹车挡住、已忽略 */
@@ -347,7 +370,7 @@ export declare class RuntimePlanError extends Error {
347
370
  }>);
348
371
  }
349
372
  /**
350
- * 找到(必要时下载或更新)可用的客户端运行时。CLI(`u1s1 claude` / `u1s1 codex`)与桌面端共用。
373
+ * 找到(必要时下载或更新)可用的客户端运行时。CLI(`u1s1 claude` / `u1s1 codex` / `u1s1 dsh`)与桌面端共用。
351
374
  * 失败抛 RuntimePlanError(刹车/版本问题)或 RuntimeInstallError(下载问题)。
352
375
  */
353
376
  export declare function ensureAgentRuntime(agent: ManagedAgent, opts: EnsureRuntimeOptions): Promise<EnsuredRuntime>;
@@ -5,7 +5,8 @@
5
5
  * 用户确认后才下载;不打进我们的包、不改二进制、不动它自带的登录方式。
6
6
  * 安装交给 npm(`npm install --prefix`),上游调整包结构(平台二进制、postinstall)由 npm 处理。
7
7
  *
8
- * 目录布局(以 claude 为例;codex 同理,可执行文件在平台包里,见 CODEX_RUNTIME):
8
+ * 目录布局(以 claude 为例;codex 同理,可执行文件在平台包里,见 CODEX_RUNTIME;dsh 的入口是 JS 文件,
9
+ * 由 Node ≥24.9 运行,见 DSH_RUNTIME 与 node-runtime.ts):
9
10
  * ~/.u1s1/agents/claude/runtime/<版本>/node_modules/@anthropic-ai/claude-code/bin/claude.exe
10
11
  * ~/.u1s1/agents/claude/state.json 当前版本、pin、每日检查时间、待更新版本
11
12
  * ~/.u1s1/agents/claude/config/ CLAUDE_CONFIG_DIR(见 claude.ts;codex 是 home/,见 codex.ts)
@@ -128,9 +129,9 @@ export function bestInstallTarget(candidates, brake) {
128
129
  }
129
130
  return best;
130
131
  }
131
- export const MANAGED_AGENTS = ["claude", "codex"];
132
+ export const MANAGED_AGENTS = ["claude", "codex", "dsh"];
132
133
  export function isManagedAgent(value) {
133
- return value === "claude" || value === "codex";
134
+ return value === "claude" || value === "codex" || value === "dsh";
134
135
  }
135
136
  function npmOs(platform) {
136
137
  return platform === "win32" ? "win32" : platform === "darwin" ? "darwin" : platform === "linux" ? "linux" : undefined;
@@ -192,11 +193,37 @@ export const CODEX_RUNTIME = {
192
193
  return `@openai/codex-${os}-${arch}/vendor/${triple}/bin/${os === "win32" ? "codex.exe" : "codex"}`;
193
194
  },
194
195
  };
196
+ /** dsh 实测过的版本;网关刹车没给 max_tested_version(老网关、老缓存)时也不越过它(dsh.ts)。 */
197
+ export const DSH_TESTED_VERSION = "0.2.2-rc.1";
198
+ /**
199
+ * DeepSeek Harness:`@deepseek-ai/dsh`(MIT)是纯 JS 包,bin 是 `lib/bin.js`,依赖树里的原生模块
200
+ * (node-pty 等)带各平台预编译文件,由 npm 的 postinstall 处理。没有单独的平台包,校验时
201
+ * 主包同时充当「平台包」。engines.node >=24.9.0:便携包自带的 Node 22 跑不了,由 node-runtime.ts
202
+ * 找/下载 Node 24。0.2.2-rc.1 实测:下载约 290 MB,装好约 920 MB。
203
+ */
204
+ export const DSH_RUNTIME = {
205
+ agent: "dsh",
206
+ npmPackage: "@deepseek-ai/dsh",
207
+ command: "dsh",
208
+ binEnv: "U1S1_DSH_BIN",
209
+ configDirEnv: "DSH_HOME",
210
+ probeEnv: { DSH_TELEMETRY_DISABLED: "1" },
211
+ platformPackage: (version, platform, arch) => {
212
+ if (!npmOs(platform) || (arch !== "x64" && arch !== "arm64"))
213
+ return undefined;
214
+ return { dir: "@deepseek-ai/dsh", name: "@deepseek-ai/dsh", version };
215
+ },
216
+ binInNodeModules: (platform, arch) => npmOs(platform) && (arch === "x64" || arch === "arm64") ? "@deepseek-ai/dsh/lib/bin.js" : undefined,
217
+ minNode: "24.9.0",
218
+ sizeHint: { downloadBytes: 290 * 1024 * 1024, unpackedBytes: 920 * 1024 * 1024 },
219
+ };
195
220
  export function runtimeSpec(agent) {
196
221
  if (agent === "claude")
197
222
  return CLAUDE_RUNTIME;
198
223
  if (agent === "codex")
199
224
  return CODEX_RUNTIME;
225
+ if (agent === "dsh")
226
+ return DSH_RUNTIME;
200
227
  throw new Error(`unknown agent runtime: ${String(agent)}`);
201
228
  }
202
229
  export function runtimePaths(agentsRoot, agent) {
@@ -296,7 +323,7 @@ function isFile(p) {
296
323
  * 跑 `<bin> --version` 取版本。环境里去掉密钥、关掉自动更新;配置目录(CLAUDE_CONFIG_DIR /
297
324
  * CODEX_HOME)指到启动器自己的目录,免得探测用户自己装的客户端时碰到用户的 ~/.claude、~/.codex。
298
325
  */
299
- export function probeVersion(bin, configDir, spec = CLAUDE_RUNTIME) {
326
+ export function probeVersion(bin, configDir, spec = CLAUDE_RUNTIME, node) {
300
327
  const env = stripSecretEnv(process.env);
301
328
  for (const [key, value] of Object.entries(spec.probeEnv))
302
329
  env[key] = value;
@@ -304,8 +331,12 @@ export function probeVersion(bin, configDir, spec = CLAUDE_RUNTIME) {
304
331
  env[spec.configDirEnv] = configDir;
305
332
  else
306
333
  delete env[spec.configDirEnv];
334
+ // JS 入口(dsh):用调用方给的 Node 跑;没给就不试(系统 node 可能太旧,报出来的错不是版本号)
335
+ if (spec.minNode && !node)
336
+ return undefined;
337
+ const [command, args] = spec.minNode ? [node, [bin, "--version"]] : [bin, ["--version"]];
307
338
  try {
308
- const r = spawnSync(bin, ["--version"], { env, encoding: "utf8", timeout: 20_000, windowsHide: true });
339
+ const r = spawnSync(command, args, { env, encoding: "utf8", timeout: 20_000, windowsHide: true });
309
340
  if (r.status !== 0)
310
341
  return undefined;
311
342
  return versionFromOutput(`${r.stdout ?? ""}`);
@@ -548,7 +579,7 @@ export async function installRuntime(opts) {
548
579
  fetchPackageMeta(registry, spec.npmPackage, version),
549
580
  fetchPackageMeta(registry, plat.name, plat.version),
550
581
  ]);
551
- const downloadBytes = await downloadSize(platMeta?.tarball);
582
+ const downloadBytes = spec.sizeHint?.downloadBytes ?? await downloadSize(platMeta?.tarball);
552
583
  if (opts.needsConsent) {
553
584
  const ok = hooks.consent
554
585
  ? await hooks.consent({
@@ -557,17 +588,20 @@ export async function installRuntime(opts) {
557
588
  version,
558
589
  registry,
559
590
  downloadBytes,
560
- unpackedBytes: platMeta?.unpackedSize !== undefined
591
+ unpackedBytes: spec.sizeHint?.unpackedBytes ?? (platMeta?.unpackedSize !== undefined
561
592
  ? platMeta.unpackedSize + (mainMeta?.unpackedSize ?? 0)
562
- : undefined,
593
+ : undefined),
563
594
  })
564
595
  : false;
565
596
  if (!ok)
566
597
  throw new RuntimeInstallError("declined", hooks.text.declined());
567
598
  }
568
- const npm = opts.npm ?? findNpm({ execPath: process.execPath, platform: process.platform, env: process.env });
569
- if (!npm)
599
+ const found = opts.npm ?? findNpm({ execPath: process.execPath, platform: process.platform, env: process.env });
600
+ if (!found)
570
601
  throw new RuntimeInstallError("no_npm", hooks.text.noNpm());
602
+ // JS 入口的客户端:npm 本身和 postinstall 都用那个够新的 Node 跑(npm 按运行它的 Node 核对 engines)
603
+ const scriptNode = spec.minNode ? opts.node : undefined;
604
+ const npm = scriptNode && found.command === process.execPath ? { ...found, command: scriptNode } : found;
571
605
  mkdirSync(paths.runtimeDir, { recursive: true, mode: 0o700 });
572
606
  const maxAttempts = opts.maxAttempts ?? 3;
573
607
  const retryDelayMs = opts.retryDelayMs ?? 3_000;
@@ -584,6 +618,7 @@ export async function installRuntime(opts) {
584
618
  registry,
585
619
  progress: hooks.progress,
586
620
  totalBytes: downloadBytes,
621
+ node: scriptNode,
587
622
  });
588
623
  lastOutput = result.output;
589
624
  if (result.code !== 0) {
@@ -604,7 +639,7 @@ export async function installRuntime(opts) {
604
639
  platVersion: plat.version,
605
640
  mainMeta,
606
641
  platMeta,
607
- probe: (bin) => probeVersion(bin, opts.probeConfigDir, spec),
642
+ probe: (bin) => probeVersion(bin, opts.probeConfigDir, spec, opts.node),
608
643
  });
609
644
  if (problem) {
610
645
  rmSync(staging, { recursive: true, force: true });
@@ -724,7 +759,7 @@ export function npmEnv(base, execPath, platform = process.platform) {
724
759
  function runNpmInstall(npm, o) {
725
760
  return new Promise((resolve) => {
726
761
  const child = spawn(npm.command, [...npm.prefixArgs, ...npmInstallArgs(o)], {
727
- env: npmEnv(process.env, process.execPath),
762
+ env: npmEnv(process.env, o.node ?? process.execPath),
728
763
  stdio: ["ignore", "pipe", "pipe"],
729
764
  windowsHide: true,
730
765
  });
@@ -813,7 +848,7 @@ export class RuntimePlanError extends Error {
813
848
  }
814
849
  }
815
850
  /**
816
- * 找到(必要时下载或更新)可用的客户端运行时。CLI(`u1s1 claude` / `u1s1 codex`)与桌面端共用。
851
+ * 找到(必要时下载或更新)可用的客户端运行时。CLI(`u1s1 claude` / `u1s1 codex` / `u1s1 dsh`)与桌面端共用。
817
852
  * 失败抛 RuntimePlanError(刹车/版本问题)或 RuntimeInstallError(下载问题)。
818
853
  */
819
854
  export async function ensureAgentRuntime(agent, opts) {
@@ -821,7 +856,7 @@ export async function ensureAgentRuntime(agent, opts) {
821
856
  const paths = runtimePaths(opts.agentsRoot, agent);
822
857
  const env = opts.env ?? process.env;
823
858
  const registry = npmRegistryFor(opts.site, env);
824
- const probe = (bin) => probeVersion(bin, opts.probeConfigDir, spec);
859
+ const probe = (bin) => probeVersion(bin, opts.probeConfigDir, spec, opts.node);
825
860
  const state = readRuntimeState(paths);
826
861
  let latestMemo;
827
862
  const plan = await planRuntime({
@@ -867,6 +902,7 @@ export async function ensureAgentRuntime(agent, opts) {
867
902
  needsConsent: plan.needsConsent,
868
903
  hooks: opts.hooks,
869
904
  probeConfigDir: opts.probeConfigDir,
905
+ node: opts.node,
870
906
  });
871
907
  return { ...installed, ...extra };
872
908
  }
@@ -10,6 +10,10 @@ export interface InstallCommandDeps {
10
10
  fetchPublicRuntime?: (url: URL) => Promise<Response>;
11
11
  ensure?: typeof ensureAgentRuntime;
12
12
  checkLatest?: typeof dailyUpdateCheck;
13
+ /** dsh 的 Node;测试注入 */
14
+ ensureNode?: () => Promise<{
15
+ bin: string;
16
+ } | undefined>;
13
17
  }
14
18
  /**
15
19
  * `u1s1 agents install claude|codex`:找到或下载原版,不启动。与 `u1s1 claude` / `u1s1 codex` 走同一套