dsh-oc-tui 0.1.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.
package/README.md ADDED
@@ -0,0 +1,247 @@
1
+ # Deepseek Harness opencode-like TUI Plugin
2
+
3
+ An opencode-inspired **terminal UI (TUI)** for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness), shipped as a dsh profile app plugin. It boots a chat client inside the dsh process: it creates/resumes agents through `ctx.agents`, renders the durable `session/event` stream (user messages, streaming assistant tokens, tool cards, todo lists), routes human input back via `agent.followup()`, and answers `approval/request` prompts inline.
4
+
5
+ The UI follows a DeepSeek blue-white dark design language (brand blue `#4D6BFE` accents on a blue-tinted canvas, with a blue→white gradient on the DeepSeek Harness title): a responsive session rail, markdown transcript and tool activity, a bordered multiline composer with slash-command suggestions, and a telemetry footer for session tokens, average time to first token (TTFT), decode throughput, KV-cache hit rate, and a **context meter** showing the current context length over the context-length limit. Thinking and running tools (read/write/...) show flowing spinner animations, the status row carries a flowing wave, and the composer border becomes a flowing gold marching-ants frame (dashes chasing clockwise around the box) while the agent is working.
6
+
7
+ ![max thinking effect](docs/max-thinking.gif)
8
+
9
+ 中文用户手册见 [docs/用户手册.md](docs/用户手册.md)。
10
+
11
+ ## Requirements
12
+
13
+ - Node.js >= 22, an installed `@deepseek-ai/dsh` CLI, and an interactive terminal (Windows Terminal / ConPTY, iTerm2, GNOME Terminal, ...).
14
+ - `pnpm` on PATH for the one-time plugin install (`dsh plugin` forwards to pnpm).
15
+ - A configured model route: the profile reuses `$DSH_HOME/settings.yaml` (`llm-pi-ai` providers or `llm-deepseek`) and `$DSH_HOME/.credentials.yaml` — the same setup the Web GUI uses.
16
+
17
+ ## Quick start
18
+
19
+ ```sh
20
+ # 1. One-time setup: create the tui profile and install this plugin into it.
21
+ dsh plugin --profile tui add dsh-oc-tui
22
+
23
+ # 2. Launch.
24
+ dsh --profile tui
25
+ ```
26
+
27
+ The first command initializes `$DSH_HOME/profiles/tui` with `@deepseek-ai/dsh-base`, installs this package with pnpm, and appends it to the profile's `dsh.profile.bundles` because the package declares `dsh.bundle`. Nothing else needs editing.
28
+
29
+ Verify the layer without booting:
30
+
31
+ ```sh
32
+ dsh --profile tui --dump-config
33
+ ```
34
+
35
+ The dump shows the `dsh-oc-tui` bundle layer after `@deepseek-ai/dsh-base`.
36
+
37
+ ## Launch
38
+
39
+ ### `dsh --profile tui`
40
+
41
+ The canonical launch is the dsh launcher itself:
42
+
43
+ ```sh
44
+ dsh --profile tui # open the title screen; first message creates a session
45
+ dsh --profile tui --resume <sessionId> # resume a persisted session
46
+ dsh --profile tui --model <modelId> # default model for new sessions
47
+ dsh --profile tui --provider <route> # default provider route
48
+ dsh --profile tui --no-sidebar # start without the sidebar
49
+ dsh --profile tui --help # the TUI's own flags
50
+ ```
51
+
52
+ The stock dsh launcher only hardcodes `web` and `plugin` as bare subcommands, so the profile flag is the intended shape for custom surfaces. If you want the exact string `dsh tui`, add a one-line shell alias (for example `doskey tui=dsh --profile tui $*` in CMD).
53
+
54
+ ### `dsh-oc-tui` convenience launcher
55
+
56
+ The package also ships a `dsh-oc-tui` bin. It is equivalent to `dsh --profile tui`, but it verifies that the `tui` profile actually has the plugin installed and prints the one-time setup command when it does not:
57
+
58
+ ```sh
59
+ dsh-oc-tui # boot the tui profile
60
+ dsh-oc-tui --profile mytui # boot a profile with a different name
61
+ dsh-oc-tui --help # launcher help
62
+ ```
63
+
64
+ The launcher prefers an installed `dsh` on PATH and falls back to `npx --yes @deepseek-ai/dsh`. Install the bin globally with `npm install -g dsh-oc-tui` (or run it from a local checkout with `node bin/dsh-oc-tui.js`). Environment overrides:
65
+
66
+ | Variable | Effect |
67
+ | --- | --- |
68
+ | `DSH_TUI_PROFILE` | Default profile name when `--profile` is not given. |
69
+ | `DSH_TUI_SKIP_CHECK` | Set to `1` to skip the profile preflight (advanced installs). |
70
+
71
+ ## Usage
72
+
73
+ | Flag | Effect |
74
+ | --- | --- |
75
+ | `--resume <sessionId>` | Resume a persisted session by id. |
76
+ | `--model <modelId>` | Default model id for new sessions. |
77
+ | `--provider <provider>` | Default provider route for new sessions. |
78
+ | `--sidebar` / `--no-sidebar` | Show (default) or hide the session sidebar. |
79
+ | `--help` | Print the TUI's own flag help. |
80
+
81
+ ### Keybindings
82
+
83
+ | Key | Action |
84
+ | --- | --- |
85
+ | Enter | send message |
86
+ | Ctrl+Enter / Shift+Enter / Alt+Enter | insert a newline |
87
+ | Ctrl+C | clear a non-empty prompt, cancel a running turn, or press twice while idle to exit |
88
+ | Ctrl+P | open the settings menu |
89
+ | Ctrl+E | toggle the thinking slider below the input box |
90
+ | Tab | cycle the thinking intensity (session page); switch the settings left menu (settings page) |
91
+ | Ctrl+N | new session |
92
+ | Ctrl+D | delete the focused session in Settings → Manage sessions |
93
+ | Ctrl+L | clear the transcript view |
94
+ | Up / Down | move the caret across multi-line input; on the first/last row they step through input history |
95
+ | Left / Right | move the caret left/right in the input box |
96
+ | PgUp / PgDn | scroll the transcript |
97
+ | Esc | close the context-meter panel / thinking slider / help, or cancel an approval prompt |
98
+ | y / n | answer an inline approval prompt |
99
+
100
+ Mouse: the wheel scrolls the transcript (or the settings window when Settings is open — it no longer moves the settings focus). Hold the left button and drag across the transcript to select text, then press the right button to copy the selection to the clipboard.
101
+
102
+ ### Commands
103
+
104
+ `/help` `/settings` `/new` `/resume <id>` `/model <id>` `/provider <route>` `/clear` `/cancel` `/quit`
105
+
106
+ Harness human commands (`/compact`, `/goal`, ...) are forwarded to `ctx.commands` and run without a model turn.
107
+
108
+ ## Install
109
+
110
+ ### Automatic install script
111
+
112
+ The repo ships one-command installers for Linux/macOS and Windows. They check Node.js >= 22, ensure `pnpm` is present, install the plugin from GitHub into the `tui` profile, and can also install the `dsh-oc-tui` launcher globally.
113
+
114
+ ```sh
115
+ # Linux / macOS
116
+ curl -fsSL https://raw.githubusercontent.com/rayafriandion/dsh-oc-tui/main/install.sh | bash
117
+
118
+ # Windows (PowerShell)
119
+ powershell -ExecutionPolicy Bypass -Command "iwr https://raw.githubusercontent.com/rayafriandion/dsh-oc-tui/main/install.ps1 -OutFile install.ps1; & .\install.ps1"
120
+ ```
121
+
122
+ From a checkout you can run `./install.sh` (Linux/macOS) or `.\install.ps1` (Windows) directly. Add `--launcher` / `-Launcher` to also put the `dsh-oc-tui` command on PATH:
123
+
124
+ ```sh
125
+ ./install.sh --launcher # Linux/macOS
126
+ .\install.ps1 -Launcher # Windows
127
+ ```
128
+
129
+ Other options: `--local` (`-Local`) installs the current checkout instead of GitHub, `--source <spec>` (`-Source <spec>`) uses a custom source (e.g. `dsh-oc-tui` once published to npm, or a tarball path), and `--profile <name>` (`-Profile <name>`) targets a non-default profile.
130
+
131
+ ### From the npm registry
132
+
133
+ ```sh
134
+ dsh plugin --profile tui add dsh-oc-tui
135
+ ```
136
+
137
+ ### From a local checkout or tarball
138
+
139
+ ```sh
140
+ dsh plugin --profile tui add ./dsh-oc-tui
141
+ # or a packed tarball:
142
+ dsh plugin --profile tui add ./dsh-oc-tui-0.1.0.tgz
143
+ ```
144
+
145
+ `dsh plugin` anchors relative paths to your invoking directory before forwarding to pnpm.
146
+
147
+ ### From GitHub
148
+
149
+ ```sh
150
+ dsh plugin --profile tui add github:you/dsh-oc-tui
151
+ ```
152
+
153
+ This package ships plain JavaScript, so a git install needs no build step. pnpm ≥ 10 may still require allowlisting the git dependency's package key under `allowBuilds` in the profile's `pnpm-workspace.yaml` if a build step is ever added.
154
+
155
+ ### What the install does
156
+
157
+ 1. `dsh plugin` initializes `$DSH_HOME/profiles/tui` on first use (`@deepseek-ai/dsh-base` plus an empty user patch layer).
158
+ 2. pnpm installs `dsh-oc-tui` into the profile's `node_modules`.
159
+ 3. `dsh` appends `dsh-oc-tui` to `dsh.profile.bundles` because the package declares `dsh.bundle.patch`; the bundle patch inserts the `tui-startup` and `tui-app` rows.
160
+ 4. `dsh --profile tui` composes `@deepseek-ai/dsh-base` + `dsh-oc-tui` and boots the UI.
161
+
162
+ To remove:
163
+
164
+ ```sh
165
+ dsh plugin --profile tui remove dsh-oc-tui
166
+ ```
167
+
168
+ ## Settings
169
+
170
+ The settings menu projects the same Host settings namespaces used by the WebUI and persists changes through `ctx.settings` to `$DSH_HOME/settings.yaml`. A left menu bar splits it into two tabs that `Tab` (or a click) switches: **Main** keeps the general settings — General (Busy Enter behavior, default agent and permission presets), Sessions (new session, session management), and System; **Model** merges the former provider settings and model settings into one tree: the default provider/model/reasoning choice, then one block per provider holding its Provider URL, Provider API key, and Models. Only providers you have actually added (present in your user settings layer) are listed — preset providers that were never added stay hidden. Under Models the saved selection is listed one row per model (Enter makes a listed model the default route); pressing Enter on the Models row auto-fetches the provider's advertised catalog (`ctx.llm.discoverModels` — the installed catalog for a known route, or an endpoint interrogation for a custom route) and opens a selection window of checkboxes where you choose which models to keep. The default agent preset is chosen from the roster the profile mounts (`agent-presets`): the shipped presets plus any you authored under `$DSH_HOME/.agent-presets`; TUI sessions keep composing process-wide from the base, so the stored default only applies where a session is created from a preset. WebUI-only options (`ui-theme` Appearance and `locale` Language) are intentionally not shown because they have no effect inside the TUI. Choice items open their option list with Enter - there is no inline left/right value cycling.
171
+
172
+ **Thinking intensity.** The effective level stays visible on the composer's top-right border as the bare level name (no "effort" caption), diagonally opposite the `provider · model` label. Press `Tab` on the session page to cycle the intensity through the current model's levels (wrapping strongest → weakest); `Shift+Tab` steps backwards. Press `Ctrl+E` to open a slider below the input box; `Tab` or `←`/`→` move it and persist the choice, `Esc` or `Ctrl+E` closes it. The slider is driven by the current model's actual selectable levels reported by the provider adapter (`ctx.llm.resolveModelInfo`), so a boolean-thinking model shows exactly its two ends, a full-range model shows every level it advertises, and a partial model (for example DeepSeek's `Off`/`High`/`Max`) shows only those — never a blanket `none → max` scale. At the strongest available level, the track gradient and bright sweep move left to right, the empty track shimmers, and the top-right label receives a flowing gradient with a pulsing text arrow. The Settings → Model → Reasoning entry stays available as a list menu over the same levels. The chosen level is applied to the session's requests through the `agent/request` waterfall and stored in `agent-default-model.reasoningEffort`.
173
+
174
+ **Context meter.** The status row carries a live context-occupancy bar (`ctx ▓▓░░ 32K/128K 25%`) fed by the token-meter `contextPressure` projection, the same source as the Web UI's composer ring. It shows the current context length over the context-length limit once the provider reports both; the fill shifts toward the warning/error palette as occupancy climbs. Click the meter (or press `Esc` to close) to open a breakdown panel with the occupancy reading and the heuristic composition shares — system prompt, tools, and messages — matching the Web UI's ContextMeter dialog. The meter stays hidden when the profile lacks the token-meter projections.
175
+
176
+ ## Plugin model
177
+
178
+ This package is a DeepSeek Harness Cordis plugin, not a standalone agent runtime. The optional `dsh-oc-tui` binary only launches `dsh --profile tui`; DSH continues to own model routing, agent execution, tools, approvals, commands, durable sessions, and credentials. The plugin owns terminal input and presentation.
179
+
180
+ ## How it works
181
+
182
+ - The plugin is a Cordis function plugin loaded by the `tui` profile. `lib/startup.js` parses the app's flags and provides the `tuiStartup` service; `lib/index.js` owns the UI loop.
183
+ - `lib/term.js` is a zero-dependency terminal engine: raw mode, alternate screen, a diffing cell buffer, and a key decoder (truecolor ANSI, CJK-aware widths). It parks the (hidden) terminal cursor at the input caret so the OS IME anchors its composition window inside the composer, and it understands both SGR and legacy X10 mouse encodings so wheel/click bytes can never leak into the input text.
184
+ - `lib/ui.js` is the responsive view model + renderer (DeepSeek blue-white theme, session rail, transcript, multiline composer, command suggestions, and telemetry footer). Rendered transcript lines are cached per block, only the visible window is materialised each frame, streaming paints are coalesced, and the live block is re-rendered on a short throttle — so render cost stays bounded and output speed does not degrade as the history grows. `thinking` content is shown inside a gray-emphasised box that stays collapsed while streaming, collapses by default once finished, and toggles on click; thinking and running tools animate with flowing spinners.
185
+ - `lib/metrics.js` folds durable step/chunk/message events into session token, average TTFT, decode throughput, and disjoint-token cache-hit metrics.
186
+ - `lib/interrupt.js` owns the clear/cancel/double-exit state machine used by stdin and `SIGINT`.
187
+ - `lib/markdown.js` renders model output (headings, lists, quotes, code, inline spans) to styled lines.
188
+ - Agents are created/resumed through `ctx.agents`, the transcript is rebuilt from `session.surface` on resume and fed live by `session/event` (including `assistant/chunk` streaming), model defaults come from `ctx.agentDefaultModel`, and approvals answer the `approval/request` waterfall inline.
189
+
190
+ ## Development
191
+
192
+ ```sh
193
+ node tests/smoke.test.mjs # standalone pure-module tests (no dsh needed)
194
+ node --check lib/*.js # syntax
195
+ node --check bin/*.js # launcher syntax
196
+ ```
197
+
198
+ The full end-to-end path (profile boot → session → live LLM streaming → commands → clean exit) was verified through a pseudo-terminal (node-pty + ConPTY) on Windows.
199
+
200
+ For a zero-install development bootstrap that avoids pnpm, create the profile once and point its `cordis.patch.yml` at this checkout with absolute module paths:
201
+
202
+ ```sh
203
+ dsh --profile tui --dump-config # initializes the base profile once
204
+ ```
205
+
206
+ Then add to `$DSH_HOME/profiles/tui/cordis.patch.yml`:
207
+
208
+ ```yaml
209
+ - insert:
210
+ - id: tui-startup
211
+ name: 'file:///D:/Projects/DeepSeekHarnessPlugins/dsh-oc-tui/lib/startup.js'
212
+ - id: tui-app
213
+ name: 'file:///D:/Projects/DeepSeekHarnessPlugins/dsh-oc-tui/lib/index.js'
214
+ config:
215
+ sidebar: true
216
+ showReasoning: true
217
+ ```
218
+
219
+ The plugin's dsh imports resolve through the shared `$DSH_HOME/profiles/node_modules` fallback that dsh maintains, so no pnpm install into the plugin directory is required.
220
+
221
+ ## Known limitations
222
+
223
+ - IME composition and bracketed-paste image attachments are not exposed by the zero-dependency terminal engine yet.
224
+ - Saved sessions are managed from Settings → Manage sessions; the shared `sessionQuery` service is required for the list.
225
+ - `dsh tui` as a bare subcommand needs a shell alias — the stock launcher hardcodes only `web` and `plugin` subcommands.
226
+ - Editing the plugin source does not hot-reload (the profile's HMR root is the profile dir, not the plugin dir); restart the profile to pick up changes.
227
+ - `--resume` and Settings → Manage sessions require the shared `sessionQuery` service (mounted by `dsh-base`).
228
+
229
+ ## Layout
230
+
231
+ ```
232
+ lib/index.js plugin entry: agents, events, input, commands, approvals
233
+ lib/startup.js command-line provider (tuiStartup service)
234
+ lib/term.js terminal engine (raw mode, screen, key decoding)
235
+ lib/ui.js responsive view model + renderer
236
+ lib/metrics.js durable event telemetry fold
237
+ lib/interrupt.js Ctrl+C lifecycle state
238
+ lib/web-settings.js shared WebUI settings projection
239
+ lib/markdown.js markdown -> styled lines
240
+ lib/util.js text/display helpers
241
+ bin/dsh-oc-tui.js convenience launcher for `dsh --profile tui`
242
+ install.sh one-command installer (Linux/macOS)
243
+ install.ps1 one-command installer (Windows)
244
+ cordis.patch.yml bundle patch layer (TUI rows)
245
+ docs/用户手册.md Chinese user manual
246
+ tests/smoke.test.mjs standalone smoke tests
247
+ ```
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+ // dsh-oc-tui: convenience launcher for the DeepSeek Harness terminal UI.
3
+ //
4
+ // It is equivalent to `dsh --profile tui <args...>`, but it:
5
+ // 1. prefers an installed dsh CLI on PATH,
6
+ // 2. falls back to npx (@deepseek-ai/dsh) when dsh is not installed,
7
+ // 3. verifies that the target profile exists and has this plugin installed,
8
+ // so a missing one-time setup prints the exact command instead of a
9
+ // confusing empty boot,
10
+ // 4. accepts --profile <name> (or DSH_TUI_PROFILE) to target a non-default
11
+ // profile; every other argument is forwarded to the TUI app.
12
+ import { spawn } from 'node:child_process'
13
+ import { existsSync, readFileSync } from 'node:fs'
14
+ import { createRequire } from 'node:module'
15
+ import { delimiter, dirname, join, resolve } from 'node:path'
16
+ import { homedir } from 'node:os'
17
+
18
+ const PACKAGE_NAME = 'dsh-oc-tui'
19
+ const DSH_PACKAGE = '@deepseek-ai/dsh'
20
+
21
+ const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
22
+
23
+ function printHelp() {
24
+ process.stdout.write(`dsh-oc-tui ${manifest.version} — launch the DeepSeek Harness terminal UI
25
+
26
+ Usage:
27
+ dsh-oc-tui [--profile <name>] [app args...]
28
+
29
+ The launcher boots the dsh profile that has ${PACKAGE_NAME} installed and
30
+ forwards every other argument to the TUI app (--resume, --model, --provider,
31
+ --sidebar, --no-sidebar; see dsh --profile <name> --help).
32
+
33
+ Options:
34
+ --profile <name> profile to boot (default: tui; env DSH_TUI_PROFILE)
35
+ -h, --help show this help
36
+ -V, --version show the dsh-oc-tui version
37
+
38
+ One-time setup (requires pnpm on PATH):
39
+ dsh plugin --profile tui add ${PACKAGE_NAME}
40
+
41
+ Then:
42
+ dsh-oc-tui
43
+ `)
44
+ }
45
+
46
+ /**
47
+ * Parse the launcher's own arguments. The TUI app never sees --profile,
48
+ * --help, or --version; everything else is forwarded verbatim.
49
+ */
50
+ function parseArgs(argv) {
51
+ let profile = process.env.DSH_TUI_PROFILE || 'tui'
52
+ const forwarded = []
53
+ let help = false
54
+ let version = false
55
+ for (let i = 0; i < argv.length; i++) {
56
+ const arg = argv[i]
57
+ if (arg === '-h' || arg === '--help') {
58
+ help = true
59
+ } else if (arg === '-V' || arg === '--version') {
60
+ version = true
61
+ } else if (arg === '--profile') {
62
+ const value = argv[++i]
63
+ if (value === undefined || value === '' || value.startsWith('-')) {
64
+ process.stderr.write('dsh-oc-tui: --profile needs a name\n')
65
+ process.exit(1)
66
+ }
67
+ profile = value
68
+ } else if (arg.startsWith('--profile=')) {
69
+ profile = arg.slice('--profile='.length)
70
+ } else {
71
+ forwarded.push(arg)
72
+ }
73
+ }
74
+ return { profile, forwarded, help, version }
75
+ }
76
+
77
+ /** The harness home directory, matching dsh's own DSH_HOME convention. */
78
+ function dshHome() {
79
+ return process.env.DSH_HOME || join(homedir(), '.dsh')
80
+ }
81
+
82
+ /**
83
+ * Check whether `profile` has this plugin installed. The official install path
84
+ * lists the package in dsh.profile.bundles; the zero-install development path
85
+ * restates the rows with absolute paths in the profile's cordis.patch.yml, so
86
+ * either marker counts.
87
+ */
88
+ function profileStatus(profile) {
89
+ const profileDir = join(dshHome(), 'profiles', profile)
90
+ const manifestPath = join(profileDir, 'package.json')
91
+ if (!existsSync(manifestPath)) return 'missing'
92
+ try {
93
+ const profileManifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
94
+ const bundles = profileManifest?.dsh?.profile?.bundles ?? []
95
+ if (bundles.includes(PACKAGE_NAME)) return 'ready'
96
+ } catch {
97
+ // A broken profile manifest is dsh's error to report, not the launcher's.
98
+ return 'ready'
99
+ }
100
+ const patchPath = join(profileDir, 'cordis.patch.yml')
101
+ if (existsSync(patchPath) && readFileSync(patchPath, 'utf8').includes(PACKAGE_NAME)) return 'ready'
102
+ return 'incomplete'
103
+ }
104
+
105
+ function checkProfile(profile, setupCommand) {
106
+ if (process.env.DSH_TUI_SKIP_CHECK === '1') return true
107
+ const status = profileStatus(profile)
108
+ if (status === 'ready') return true
109
+ if (status === 'missing') {
110
+ process.stderr.write(`dsh-oc-tui: profile '${profile}' is not set up.\n`)
111
+ process.stderr.write(`Create it once with (requires pnpm on PATH):\n ${setupCommand}\n`)
112
+ } else {
113
+ process.stderr.write(`dsh-oc-tui: profile '${profile}' exists but does not have ${PACKAGE_NAME} installed.\n`)
114
+ process.stderr.write(`Add it once with:\n ${setupCommand}\n`)
115
+ }
116
+ process.stderr.write(`Then run: dsh-oc-tui --profile ${profile}\n`)
117
+ process.stderr.write('Set DSH_TUI_SKIP_CHECK=1 to skip this check.\n')
118
+ return false
119
+ }
120
+
121
+ /** Find an executable by name on PATH, with Windows extension probing. */
122
+ function findOnPath(name) {
123
+ const path = process.env.PATH || ''
124
+ const names = process.platform === 'win32' ? [name + '.cmd', name + '.exe', name] : [name]
125
+ for (const directory of path.split(delimiter)) {
126
+ if (directory === '') continue
127
+ for (const candidate of names) {
128
+ const full = join(directory, candidate)
129
+ if (existsSync(full)) return full
130
+ }
131
+ }
132
+ return undefined
133
+ }
134
+
135
+ /**
136
+ * Resolve the dsh package's bin entry from an npm shim next to its install
137
+ * (the shim directory is the npm prefix, so @deepseek-ai/dsh resolves from
138
+ * there). Returning the JS entry lets Windows run it through node directly,
139
+ * which avoids both the .cmd EINVAL hardening and shell-argument quoting.
140
+ */
141
+ function resolveDshEntry(dshExecutable) {
142
+ try {
143
+ const packagePath = createRequire(resolve(dshExecutable)).resolve(`${DSH_PACKAGE}/package.json`)
144
+ const packageManifest = JSON.parse(readFileSync(packagePath, 'utf8'))
145
+ const entry = typeof packageManifest.bin === 'string'
146
+ ? packageManifest.bin
147
+ : packageManifest.bin?.dsh
148
+ if (typeof entry === 'string' && entry !== '') return resolve(dirname(packagePath), entry)
149
+ } catch {
150
+ // Not an npm shim beside the dsh install, or a non-npm layout.
151
+ }
152
+ return undefined
153
+ }
154
+
155
+ /** Spawn one resolved command, using node for npm shims on Windows when possible. */
156
+ function spawnCommand(command, args) {
157
+ if (process.platform === 'win32') {
158
+ const entry = resolveDshEntry(command)
159
+ if (entry !== undefined) return spawn(process.execPath, [entry, ...args], { stdio: 'inherit' })
160
+ if (/\.(cmd|bat)$/i.test(command)) {
161
+ return spawn('cmd.exe', ['/d', '/s', '/c', command, ...args], { stdio: 'inherit' })
162
+ }
163
+ }
164
+ return spawn(command, args, { stdio: 'inherit' })
165
+ }
166
+
167
+ /** The npx fallback: prefer the npm CLI bundled with this Node, then PATH. */
168
+ function npxCommand() {
169
+ const bundled = join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npx-cli.js')
170
+ if (existsSync(bundled)) return { command: process.execPath, args: [bundled] }
171
+ const onPath = findOnPath('npx')
172
+ if (onPath !== undefined) return { command: onPath, args: [] }
173
+ return undefined
174
+ }
175
+
176
+ function launchDsh(profile, forwarded) {
177
+ const dshArgs = ['--profile', profile, ...forwarded]
178
+ const dshExecutable = findOnPath('dsh')
179
+ if (dshExecutable !== undefined) return spawnCommand(dshExecutable, dshArgs)
180
+ const npx = npxCommand()
181
+ if (npx !== undefined) return spawnCommand(npx.command, [...npx.args, '--yes', DSH_PACKAGE, ...dshArgs])
182
+ return undefined
183
+ }
184
+
185
+ function main() {
186
+ const { profile, forwarded, help, version } = parseArgs(process.argv.slice(2))
187
+ if (help) {
188
+ printHelp()
189
+ process.exit(0)
190
+ }
191
+ if (version) {
192
+ process.stdout.write(manifest.version + '\n')
193
+ process.exit(0)
194
+ }
195
+ if (profile === '' || profile === '.' || profile === '..' || profile === 'node_modules'
196
+ || profile.includes('/') || profile.includes('\\')) {
197
+ process.stderr.write(`dsh-oc-tui: invalid profile name ${JSON.stringify(profile)}\n`)
198
+ process.exit(1)
199
+ }
200
+ const setupCommand = findOnPath('dsh') !== undefined
201
+ ? `dsh plugin --profile ${profile} add ${PACKAGE_NAME}`
202
+ : `npx --yes ${DSH_PACKAGE} plugin --profile ${profile} add ${PACKAGE_NAME}`
203
+ if (!checkProfile(profile, setupCommand)) process.exit(2)
204
+
205
+ const child = launchDsh(profile, forwarded)
206
+ if (child === undefined) {
207
+ process.stderr.write(`dsh-oc-tui: could not find dsh or npx on PATH — install ${DSH_PACKAGE} first\n`)
208
+ process.exit(1)
209
+ }
210
+ child.on('error', (error) => {
211
+ process.stderr.write('dsh-oc-tui: failed to launch dsh: ' + error.message + '\n')
212
+ process.exit(1)
213
+ })
214
+ child.on('exit', (code, signal) => {
215
+ process.exit(code ?? (signal ? 1 : 0))
216
+ })
217
+ }
218
+
219
+ main()
@@ -0,0 +1,24 @@
1
+ # dsh-oc-tui bundle patch: the terminal UI over the dsh-base layer.
2
+ # Rows reference this package by name, so the package must be resolvable from
3
+ # the profile — the official path is `dsh plugin --profile tui add
4
+ # dsh-oc-tui`, which installs it into the profile's node_modules and
5
+ # appends this bundle. The zero-install development bootstrap may instead
6
+ # restate these rows in the profile's own patch with absolute module paths.
7
+
8
+ - insert:
9
+ - id: tui-startup
10
+ name: 'dsh-oc-tui/startup'
11
+
12
+ - id: tui-app
13
+ name: 'dsh-oc-tui'
14
+ config:
15
+ sidebar: true
16
+ showReasoning: true
17
+
18
+ # The preset roster behind the Settings -> Default preset list.
19
+ # Sessions keep composing process-wide from the base (single-session TUI);
20
+ # this row only makes the roster and its settings namespace available.
21
+ - id: agent-presets
22
+ name: '@deepseek-ai/dsh-agent-presets'
23
+ config:
24
+ default: standard
Binary file