deepseek-cli-mode 0.0.0-stage → 1.0.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ ## 1.0.1 — security fixes
4
+
5
+ Fixes from an independent security audit (Cloudflare `security-audit` skill, quick profile).
6
+
7
+ ### Fixed
8
+
9
+ - **Repository-local runtime execution (Medium).** `resolveDshBin()` no longer considers
10
+ `process.cwd()/node_modules/@deepseek-ai/dsh` or a cwd-based `require` resolution. Running `deepseek` inside an
11
+ untrusted repository can no longer execute that repository's own Harness copy. A project-local runtime is available
12
+ only with the explicit `DSH_CLI_ALLOW_LOCAL=1` opt-in, and the resolved package's manifest must declare
13
+ `name: "@deepseek-ai/dsh"`.
14
+ - **One-shot plan-review auto-approval (Medium).** Non-interactive runs no longer select the first option of a
15
+ question. They now answer with a refusal, which keeps plan mode active instead of approving a plan no human
16
+ reviewed.
17
+ - **Terminal escape injection (Medium).** Model- and repository-derived text (assistant output, tool-call previews,
18
+ todo items, reasoning, question fields, error messages, and the one-shot answer) is stripped of C0/C1 control
19
+ characters before it reaches the terminal.
20
+
21
+ ### Changed
22
+
23
+ - `DSH_CLI_ALLOW_LOCAL=1` is a new opt-in for executing a runtime installed inside the current project.
24
+
25
+ ## 1.0.0 — first public release
26
+
27
+ - Interactive terminal mode for DeepSeek Harness: streaming, tools, approvals, plan mode, goals, compaction, skills,
28
+ jobs, todos, subagents, image/file attachments, session resume, and `/export`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek Harness CLI contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,259 @@
1
- # Temporary Holding Version
1
+ # deepseek-cli-mode
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Terminal (CLI) mode for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)** — an interactive,
4
+ Claude Code-style coding agent that lives in your shell instead of a browser or an Electron window.
5
+
6
+ > **Community project.** This is an independent, unofficial project. It is **not** affiliated with, endorsed by, or
7
+ > sponsored by DeepSeek. "DeepSeek" and "DeepSeek Harness" are used only to describe what this tool connects to.
8
+ > Use it at your own risk and review the code before running it against your own API keys.
9
+
10
+ DeepSeek Harness ships two surfaces: the **Web** app and the **Desktop** app. This package adds the third one — a real
11
+ interactive **CLI** that boots the *same* Harness runtime (`dsh-base` + `dsh-headless`: agent loop, tools, sandbox,
12
+ session persistence, approvals) and keeps **one live Agent for the whole session**, so multi-turn context, tool state,
13
+ and the model's request prefix stay warm between messages.
14
+
15
+ ```
16
+ ❯ explain what changed in this repo and run the tests
17
+
18
+ ⌕ grep diff --stat
19
+ $ pwsh npm test
20
+ ◇ read package.json
21
+ The working tree has three modified files …
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Requirements
27
+
28
+ | | |
29
+ |---|---|
30
+ | Node.js | 20 or newer |
31
+ | DeepSeek Harness runtime | `@deepseek-ai/dsh` **0.2.1** or newer, installed locally |
32
+ | Credentials | an API key saved by the Desktop/Web app, or `DEEPSEEK_API_KEY` in the environment |
33
+
34
+ The CLI is a front-end for the Harness runtime; it does not bundle the runtime itself. Install both:
35
+
36
+ ```powershell
37
+ npm install -g @deepseek-ai/dsh
38
+ npm install -g deepseek-cli-mode
39
+ ```
40
+
41
+ The CLI reuses the key already stored in `$DSH_HOME/.credentials.yaml` by the GUI, so in most setups there is nothing
42
+ extra to configure.
43
+
44
+ ---
45
+
46
+ ## Installation
47
+
48
+ ### Global install (recommended)
49
+
50
+ ```powershell
51
+ npm install -g deepseek-cli-mode
52
+ deepseek --version
53
+ ```
54
+
55
+ This installs three equivalent command names: **`deepseek`** (primary), `deepseek-cli-mode`, and `dsh-cli`.
56
+
57
+ ### Run without installing
58
+
59
+ ```powershell
60
+ npx --yes deepseek-cli-mode
61
+ ```
62
+
63
+ ### From a local checkout
64
+
65
+ ```powershell
66
+ cd deepseek-cli-mode # the folder containing package.json
67
+ npm install
68
+ npm link # optional: exposes the `deepseek` command
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Usage
74
+
75
+ ```
76
+ deepseek start an interactive session
77
+ deepseek "task text..." answer one task and exit (scriptable)
78
+ deepseek -r, --resume [id] resume a session (newest in this directory by default)
79
+ deepseek -c, --continue continue the newest session without prompting
80
+ deepseek --list list recent sessions in this directory
81
+ deepseek -y, --yes auto-approve tool actions that would ask
82
+ deepseek --model <id> override the model for this run
83
+ deepseek --reasoning <effort> override the reasoning effort (off|high|max)
84
+ deepseek --help show help
85
+ deepseek --version show the version
86
+ ```
87
+
88
+ ### Examples
89
+
90
+ ```powershell
91
+ # interactive — the normal way to work
92
+ deepseek
93
+
94
+ # scriptable one-shot; stdout carries only the final answer
95
+ deepseek "summarize the uncommitted changes" > summary.md
96
+
97
+ # continue the previous conversation in this folder
98
+ deepseek -c
99
+
100
+ # let it edit files and run commands unattended (approvals auto-accepted)
101
+ deepseek -y "fix the failing test in tests/auth.spec.ts"
102
+
103
+ # pipe a task in
104
+ Get-Content .\error.log | Select-Object -First 50 | deepseek "what causes this error?"
105
+
106
+ # attach a file or an image to the next message
107
+ deepseek "review @src/app.ts and tell me what is risky"
108
+ ```
109
+
110
+ ### Inside a session
111
+
112
+ | Command | What it does |
113
+ |---|---|
114
+ | `/help` | command list, including the live Harness command registry |
115
+ | `/new` | start a fresh session (new id, keeps the process warm) |
116
+ | `/sessions` | recent sessions in this directory (with a ▶ running marker) |
117
+ | `/resume <id>` | switch to another session |
118
+ | `/model [id]` | show or switch the model for the next message |
119
+ | `/reasoning [off\|high\|max]` | show or set the reasoning effort |
120
+ | `/permission [preset]` | switch the sandbox + approval preset |
121
+ | `/skills` | list the skills discovered in this workspace |
122
+ | `/jobs` | list this session's background jobs |
123
+ | `/todos` | show the current todo list |
124
+ | `/image <path>` | attach an image to the next message (or mention `@photo.png`) |
125
+ | `/export [file]` | write this run's transcript as markdown |
126
+ | `/thinking` | print the last turn's reasoning (collapsed by default) |
127
+ | `/session` | print the current session id (for `--resume`) |
128
+ | `/clear` | clear the screen |
129
+ | `/quit`, `/exit` | leave (Ctrl+C at an empty prompt also exits) |
130
+
131
+ Harness commands are shared with the Web and Desktop apps and run through the same registry:
132
+
133
+ | Command | What it does |
134
+ |---|---|
135
+ | `/compact` | compact older conversation history |
136
+ | `/goal [objective\|clear\|edit\|pause\|resume]` | long-running goal mode |
137
+ | `/plan [off]` | plan mode (design first, approve, then execute) |
138
+ | `/feedback` | record feedback about the session |
139
+
140
+ Mention a file with `@path` to attach its text (up to 200 KB); mention an image (`@shot.png`) or use `/image` to attach
141
+ it durably through the same attachment store the GUI uses.
142
+
143
+ Ctrl+C **during a turn** cancels the turn; Ctrl+C **again** force-quits. Approval prompts (`Allow write …? [y/N]`) appear
144
+ when a tool wants to leave the workspace or act outside the sandbox policy.
145
+
146
+ ---
147
+
148
+ ## Configuration
149
+
150
+ | Environment variable | Default | Meaning |
151
+ |---|---|---|
152
+ | `DSH_HOME` | `~/.dsh` | Harness home (profiles, sessions, credentials) |
153
+ | `DEEPSEEK_API_KEY` | stored key | API key override |
154
+ | `DSH_CLI_DSH_BIN` | auto-detected | explicit path to `@deepseek-ai/dsh/lib/bin.js` |
155
+ | `DSH_CLI_ALLOW_LOCAL` | unset | `1` allows executing a dsh installed inside the current project; leave unset in untrusted repositories |
156
+ | `DSH_PERMISSION_MODE` | `workspace-write` | `read-only` \| `workspace-write` \| `danger-full-access` |
157
+ | `DSH_CLI_REASONING` | unset | `1` streams reasoning text instead of collapsing it |
158
+ | `DSH_CLI_YES` | unset | `1` auto-approves tool actions (same as `--yes`) |
159
+ | `NO_COLOR` | unset | disable ANSI styling |
160
+
161
+ Model and reasoning effort come from the Harness settings (the same selection the Desktop app shows). `--model`,
162
+ `--reasoning`, `/model`, and `/reasoning` override them for the session.
163
+
164
+ ---
165
+
166
+ ## How it works
167
+
168
+ ```
169
+ deepseek (bin/deepseek.js)
170
+ ├─ finds the local @deepseek-ai/dsh installation
171
+ ├─ self-heals $DSH_HOME/profiles/dsh-cli (manifest + patch + runner.js)
172
+ └─ spawns: dsh --profile dsh-cli <your arguments>
173
+ └─ profile bundles: @deepseek-ai/dsh-base + @deepseek-ai/dsh-headless
174
+ └─ runner/runner.js (Cordis plugin, this package)
175
+ ├─ agents.create() / agents.resume()
176
+ ├─ agent/assistant-stream → token-level streaming
177
+ ├─ session/event → tool calls, todos, usage
178
+ ├─ user-questions/request → plan review, ask_user_question
179
+ └─ approval/request → y/N prompts
180
+ ```
181
+
182
+ Why a profile instead of a standalone program? The Harness is a Cordis plugin tree; running the CLI as a profile means
183
+ it gets the identical system prompt, tools, sandbox, permission policy, session store, and compaction behavior as the
184
+ GUI. This package only adds the terminal front-end.
185
+
186
+ Files this package writes (once, under `$DSH_HOME/profiles/dsh-cli/`):
187
+
188
+ ```
189
+ package.json profile manifest (bundles)
190
+ cordis.yml empty root
191
+ cordis.patch.yml disables the one-shot headless driver, re-enables the
192
+ credential store, mounts ./runner.js
193
+ pnpm-workspace.yaml
194
+ runner.js copy of runner/runner.js (kept in sync)
195
+ .cli-version version stamp for the files above
196
+ ```
197
+
198
+ Your Web/Desktop profiles are untouched.
199
+
200
+ ### Feature parity with the Web/Desktop apps
201
+
202
+ Roughly four fifths of the agent-facing product is shared by construction (tools, policies, sessions, plans, goals,
203
+ compaction, skills, subagents, workflows). A feature-by-feature comparison — including what stays GUI-only — is in
204
+ [docs/PARITY.md](docs/PARITY.md).
205
+
206
+ ---
207
+
208
+ ## Troubleshooting
209
+
210
+ **`deepseek: cannot find @deepseek-ai/dsh`** — install the runtime once: `npm i -g @deepseek-ai/dsh`, or set
211
+ `DSH_CLI_DSH_BIN` to the `lib/bin.js` path.
212
+
213
+ **`@deepseek-ai/dsh 0.1.x is too old for this CLI`** — upgrade with `npm i -g @deepseek-ai/dsh@latest`. This CLI needs
214
+ the `agent/assistant-stream` and `agentDefaultModel.saveSelection` APIs introduced in 0.2.1.
215
+
216
+ **`MISSING_CREDENTIAL` / no API key** — save a key in the Desktop/Web app (Models page), or
217
+ `$env:DEEPSEEK_API_KEY = "sk-…"` before starting `deepseek`.
218
+
219
+ **Approvals keep rejecting actions** — the default policy is `workspace-write`, so writes outside the working directory
220
+ ask first. Use `-y` for unattended runs, or start with `$env:DSH_PERMISSION_MODE = "danger-full-access"`.
221
+
222
+ **Command not found after `npm i -g`** — the npm global prefix may not be on `PATH`. Check `npm prefix -g` and add its
223
+ `bin` folder, or run through `npx deepseek-cli-mode`.
224
+
225
+ **`npx` with an absolute Windows tarball path does nothing** — pass a relative path or a `file:` spec:
226
+ `npx --yes .\deepseek-cli-mode-1.0.0.tgz`.
227
+
228
+ **Input looks garbled in Git Bash / MSYS** — use Windows Terminal, PowerShell 7, or `cmd.exe`; the runner uses Node's
229
+ readline and expects a normal Windows console.
230
+
231
+ **Startup shows `[llm-deepseek-api-key] model listing …`** — the adapter is discovering models; it is written to stderr,
232
+ so piped stdout stays clean.
233
+
234
+ ---
235
+
236
+ ## Development
237
+
238
+ ```powershell
239
+ npm install
240
+ npm test # reports the CLI version
241
+ npm start # same as `deepseek`
242
+ node bin/deepseek.js --list # boot the profile without a model call
243
+ npm run pack:check # inspect the publish archive
244
+ ```
245
+
246
+ The runner is plain ESM with no build step. It is copied to the profile on every launch when its content changed, so
247
+ editing `runner/runner.js` and re-running `deepseek` picks the change up.
248
+
249
+ Compatibility note: the runner targets the 0.2.1 `@deepseek-ai/dsh` API surface (Cordis plugin entry, `agents.create`,
250
+ `agent/assistant-stream`, `session/event`, `user-questions/request`, `approval/request`,
251
+ `agentDefaultModel.saveSelection`). A future dsh release that changes those internals needs a matching runner update.
252
+
253
+ ---
254
+
255
+ ## License
256
+
257
+ [MIT](LICENSE) © 2026 DeepSeek Harness CLI contributors.
258
+
259
+ This project is independent and community-maintained; see the disclaimer at the top.
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env node
2
+ // deepseek — the DeepSeek Harness terminal CLI launcher.
3
+ //
4
+ // DeepSeek Harness (dsh) ships Web and Desktop surfaces. This launcher adds the
5
+ // missing third surface: a Claude-Code-style terminal session. It resolves the
6
+ // locally installed `@deepseek-ai/dsh`, self-heals a dedicated `dsh-cli` profile
7
+ // under $DSH_HOME/profiles, and boots that profile with this package's runner.
8
+ //
9
+ // The profile keeps ONE live Agent for the whole session, so tools, approvals,
10
+ // session persistence, and the model's request prefix stay warm between turns.
11
+ import { spawn, spawnSync } from "node:child_process";
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { createRequire } from "node:module";
14
+ import { homedir } from "node:os";
15
+ import { dirname, join, resolve } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ const here = dirname(fileURLToPath(import.meta.url));
19
+ const root = resolve(here, "..");
20
+ const requireFromHere = createRequire(import.meta.url);
21
+
22
+ const PROFILE = "dsh-cli";
23
+ const VERSION = readVersion(join(root, "package.json"));
24
+ const DSH_MIN = "0.2.1";
25
+
26
+ function readVersion(file) {
27
+ try {
28
+ return JSON.parse(readFileSync(file, "utf8")).version ?? "0.0.0";
29
+ } catch {
30
+ return "0.0.0";
31
+ }
32
+ }
33
+
34
+ const args = process.argv.slice(2);
35
+
36
+ // Help/version are answered here so they never pay for a profile boot.
37
+ if (args.includes("--version") || args.includes("-v")) {
38
+ console.log(VERSION);
39
+ process.exit(0);
40
+ }
41
+ if (args.length === 0 ? false : args[0] === "--help" || args[0] === "-h") {
42
+ printHelp();
43
+ process.exit(0);
44
+ }
45
+
46
+ function printHelp() {
47
+ console.log(`deepseek ${VERSION} — DeepSeek Harness in your terminal.
48
+
49
+ Usage:
50
+ deepseek start an interactive session
51
+ deepseek "task text..." answer one task and exit (scriptable)
52
+ deepseek -r, --resume resume the newest session in this directory
53
+ deepseek -r <id> resume a specific session
54
+ deepseek -c, --continue continue the newest session without prompting
55
+ deepseek --list list recent sessions in this directory
56
+ deepseek -y, --yes auto-approve tool actions that ask
57
+ deepseek --model <id> override the model for this run
58
+ deepseek --reasoning <effort> override the reasoning effort (off|high|max)
59
+ deepseek --help this help
60
+ deepseek --version print the version
61
+
62
+ Inside a session:
63
+ /help commands /new fresh session
64
+ /sessions recent sessions /resume <id> switch session
65
+ /model [id] show/switch model /reasoning [e] show/set effort
66
+ /thinking last reasoning /session current session id
67
+ /clear clear the screen /quit, /exit leave
68
+
69
+ Environment:
70
+ DSH_HOME Harness home (default: ~/.dsh)
71
+ DSH_CLI_DSH_BIN explicit path to @deepseek-ai/dsh/lib/bin.js
72
+ DSH_PERMISSION_MODE workspace-write (default) | read-only | danger-full-access
73
+ DEEPSEEK_API_KEY API key; otherwise the key saved by the Desktop/Web app is used
74
+
75
+ Requires a local install of @deepseek-ai/dsh (npm i -g @deepseek-ai/dsh).`);
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // 1. Locate the dsh installation.
80
+ // ---------------------------------------------------------------------------
81
+ function resolveDshBin() {
82
+ const candidates = [
83
+ process.env.DSH_CLI_DSH_BIN,
84
+ // Same global prefix as this node binary (npm default layout).
85
+ join(dirname(process.execPath), "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"),
86
+ // Common global prefixes.
87
+ process.env.APPDATA ? join(process.env.APPDATA, "npm", "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js") : undefined,
88
+ process.env.NPM_CONFIG_PREFIX ? join(process.env.NPM_CONFIG_PREFIX, "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js") : undefined,
89
+ process.env.npm_config_prefix ? join(process.env.npm_config_prefix, "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js") : undefined,
90
+ join(homedir(), "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"),
91
+ join(homedir(), ".npm-global", "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"),
92
+ ].filter(Boolean);
93
+ // A project-local runtime is OPT-IN: running the CLI inside an untrusted
94
+ // repository must never execute repository-supplied code implicitly.
95
+ if (process.env.DSH_CLI_ALLOW_LOCAL === "1") {
96
+ candidates.push(join(process.cwd(), "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"));
97
+ }
98
+ for (const candidate of candidates) if (existsSync(candidate)) return candidate;
99
+ // Resolve only from this package's own module graph (never from cwd).
100
+ try {
101
+ return requireFromHere.resolve("@deepseek-ai/dsh/lib/bin.js");
102
+ } catch {}
103
+ // Last resort: ask npm where its global root is (covers custom prefixes).
104
+ try {
105
+ const npmCli = join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
106
+ if (existsSync(npmCli)) {
107
+ const result = spawnSync(process.execPath, [npmCli, "root", "-g"], { encoding: "utf8", timeout: 20000 });
108
+ const dir = result.stdout?.trim();
109
+ if (dir) {
110
+ const candidate = join(dir, "@deepseek-ai", "dsh", "lib", "bin.js");
111
+ if (existsSync(candidate)) return candidate;
112
+ }
113
+ }
114
+ } catch {}
115
+ return undefined;
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // 2. Self-heal the dedicated profile.
120
+ // ---------------------------------------------------------------------------
121
+ const PROFILE_MANIFEST = JSON.stringify(
122
+ {
123
+ name: "dsh-profile-dsh-cli",
124
+ private: true,
125
+ type: "module",
126
+ dependencies: {},
127
+ dsh: { profile: { bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-headless"] } },
128
+ },
129
+ null,
130
+ 2
131
+ ) + "\n";
132
+
133
+ const PROFILE_ROOT =
134
+ "# dsh profile root — an empty entry list. The tree is composed as patches:\n" +
135
+ "# each bundle in package.json's dsh.profile.bundles, then cordis.patch.yml.\n[]\n";
136
+
137
+ const PROFILE_PATCH = `# dsh-cli — interactive terminal profile for the DeepSeek Harness.
138
+ # Bundles: dsh-base (core machinery) + dsh-headless (coding persona, tools,
139
+ # sandbox policy). This layer disables the one-shot headless driver, restores
140
+ # the managed credential store the headless bundle disables, and mounts the
141
+ # interactive runner from ./runner.js.
142
+ - id: headless-startup
143
+ disabled: true
144
+
145
+ - id: headless-runner
146
+ disabled: true
147
+
148
+ # The CLI is interactive: use the API key saved by the Desktop/Web app.
149
+ - id: credentials
150
+ disabled: false
151
+ - id: authorization
152
+ disabled: false
153
+ - id: deepseek-account
154
+ disabled: false
155
+
156
+ - insert:
157
+ - id: cli-runner
158
+ name: './runner.js'
159
+ - id: tool-ask-user
160
+ name: '@deepseek-ai/dsh-tool-ask-user'
161
+ `;
162
+
163
+ const PNPM_WORKSPACE = "packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n";
164
+
165
+ function ensureProfile() {
166
+ const dshHome =
167
+ process.env.DSH_HOME && process.env.DSH_HOME.trim() !== ""
168
+ ? process.env.DSH_HOME
169
+ : join(homedir(), ".dsh");
170
+ const profileDir = join(dshHome, "profiles", PROFILE);
171
+ mkdirSync(profileDir, { recursive: true });
172
+
173
+ const stamp = join(profileDir, ".cli-version");
174
+ let stampVersion = "";
175
+ try {
176
+ stampVersion = readFileSync(stamp, "utf8").trim();
177
+ } catch {}
178
+
179
+ const staticFiles = {
180
+ "package.json": PROFILE_MANIFEST,
181
+ "cordis.yml": PROFILE_ROOT,
182
+ "cordis.patch.yml": PROFILE_PATCH,
183
+ "pnpm-workspace.yaml": PNPM_WORKSPACE,
184
+ };
185
+ for (const [name, content] of Object.entries(staticFiles)) {
186
+ const target = join(profileDir, name);
187
+ if (!existsSync(target) || stampVersion !== VERSION) writeFileSync(target, content);
188
+ }
189
+
190
+ // Keep the runner exactly in sync with this package.
191
+ const runnerSrc = join(root, "runner", "runner.js");
192
+ const runnerDst = join(profileDir, "runner.js");
193
+ const src = readFileSync(runnerSrc, "utf8");
194
+ if (!existsSync(runnerDst) || readFileSync(runnerDst, "utf8") !== src) writeFileSync(runnerDst, src);
195
+
196
+ writeFileSync(stamp, VERSION + "\n");
197
+ return profileDir;
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // 3. Boot.
202
+ // ---------------------------------------------------------------------------
203
+ const dshBin = resolveDshBin();
204
+ if (dshBin === undefined) {
205
+ console.error(
206
+ "deepseek: cannot find @deepseek-ai/dsh.\n" +
207
+ " Install it once: npm i -g @deepseek-ai/dsh\n" +
208
+ " Or point DSH_CLI_DSH_BIN at .../@deepseek-ai/dsh/lib/bin.js"
209
+ );
210
+ process.exit(1);
211
+ }
212
+
213
+ // Verify the resolved package really is the Harness runtime before executing
214
+ // it. This blocks a path that merely exists at a trusted prefix but is not dsh.
215
+ let dshManifest;
216
+ try {
217
+ dshManifest = JSON.parse(readFileSync(join(dshBin, "..", "..", "package.json"), "utf8"));
218
+ } catch {}
219
+ if (dshManifest?.name !== "@deepseek-ai/dsh") {
220
+ console.error(
221
+ `deepseek: resolved ${dshBin} is not @deepseek-ai/dsh (found ${JSON.stringify(dshManifest?.name ?? "no manifest")}).\n` +
222
+ " Point DSH_CLI_DSH_BIN at a real @deepseek-ai/dsh/lib/bin.js."
223
+ );
224
+ process.exit(1);
225
+ }
226
+
227
+ const dshVersion = String(dshManifest.version ?? "0.0.0");
228
+ if (compareVersions(dshVersion, DSH_MIN) < 0) {
229
+ console.error(
230
+ `deepseek: @deepseek-ai/dsh ${dshVersion} is too old for this CLI (needs >= ${DSH_MIN}).\n` +
231
+ " Upgrade with: npm i -g @deepseek-ai/dsh@latest"
232
+ );
233
+ process.exit(1);
234
+ }
235
+
236
+ function compareVersions(a, b) {
237
+ const parse = (v) => String(v).split(/[.+-]/).slice(0, 3).map((n) => Number.parseInt(n, 10) || 0);
238
+ const [x, y, z] = parse(a);
239
+ const [p, q, r] = parse(b);
240
+ if (x !== p) return x < p ? -1 : 1;
241
+ if (y !== q) return y < q ? -1 : 1;
242
+ if (z !== r) return z < r ? -1 : 1;
243
+ return 0;
244
+ }
245
+
246
+ ensureProfile();
247
+
248
+ const child = spawn(process.execPath, [dshBin, "--profile", PROFILE, ...args], {
249
+ stdio: "inherit",
250
+ env: { ...process.env, DSH_CLI_VERSION: VERSION },
251
+ windowsHide: false,
252
+ });
253
+
254
+ // The child shares this console, so it receives the user's Ctrl+C directly and
255
+ // tears down gracefully (flush + dispose + exit). This process must not die
256
+ // first, or the child's session flush is lost: only hard-kill on a second
257
+ // Ctrl+C or after a grace period.
258
+ let sigints = 0;
259
+ process.on("SIGINT", () => {
260
+ sigints += 1;
261
+ if (sigints >= 2) {
262
+ if (child.exitCode === null && !child.killed) child.kill();
263
+ return;
264
+ }
265
+ setTimeout(() => {
266
+ if (child.exitCode === null && !child.killed) child.kill();
267
+ }, 15000).unref();
268
+ });
269
+ process.on("SIGTERM", () => {
270
+ if (child.exitCode === null && !child.killed) child.kill();
271
+ });
272
+
273
+ child.on("error", (error) => {
274
+ console.error(`deepseek: failed to start dsh: ${error.message}`);
275
+ process.exit(1);
276
+ });
277
+ child.on("exit", (code, signal) => {
278
+ process.exit(code ?? (signal ? 1 : 0));
279
+ });