@crazyhappyone/dsh-tui 0.1.0-alpha.49 → 0.1.0-alpha.50

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,128 @@
1
+ # Agent preset standard: one `@deepseek-ai/dsh-agent-preset` declaration inserted
2
+ # after the web patch. Edits saved from the Web editor override this row's
3
+ # `config.plugins` by id from the profile patch.
4
+ - insert:
5
+ - id: preset-standard
6
+ name: '@deepseek-ai/dsh-agent-preset'
7
+ config:
8
+ id: standard
9
+ order: 1
10
+ plugins:
11
+ - id: persona
12
+ name: '@deepseek-ai/dsh-persona'
13
+ config:
14
+ prefix: You are a coding agent powered by the {{model}} model.
15
+ - id: agent-instructions
16
+ name: '@deepseek-ai/dsh-agent-instructions'
17
+ config:
18
+ maxBytes: 65536
19
+ - id: time-context
20
+ name: '@deepseek-ai/dsh-time-context'
21
+ - id: tool-bash
22
+ name: '@deepseek-ai/dsh-tool-bash'
23
+ disabled: !!js process.platform === 'win32'
24
+ - id: tool-pwsh
25
+ name: '@deepseek-ai/dsh-tool-pwsh'
26
+ disabled: !!js process.platform !== 'win32'
27
+ - id: tool-fs
28
+ name: '@deepseek-ai/dsh-tool-fs'
29
+ - id: tool-fs-search
30
+ name: '@deepseek-ai/dsh-tool-fs-search'
31
+ config:
32
+ sampleOverCapGlobResults: false
33
+ - id: tool-jobs
34
+ name: '@deepseek-ai/dsh-tool-jobs'
35
+ - id: tool-schedule
36
+ name: '@deepseek-ai/dsh-tool-schedule'
37
+ - id: skill-filesystem
38
+ name: '@deepseek-ai/dsh-skill-filesystem'
39
+ - id: tool-skill
40
+ name: '@deepseek-ai/dsh-tool-skill'
41
+ - id: command-goal
42
+ name: '@deepseek-ai/dsh-command-goal'
43
+ - id: tool-goal
44
+ name: '@deepseek-ai/dsh-tool-goal'
45
+ - id: planning
46
+ name: cordis:group
47
+ group: true
48
+ isolate:
49
+ planMode: true
50
+ config:
51
+ - id: plan-mode
52
+ name: '@deepseek-ai/dsh-plan-mode'
53
+ config:
54
+ section: |
55
+ You are in plan mode. Stay in plan mode until the user approves your plan through exit_plan_mode or switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
56
+
57
+ Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
58
+
59
+ The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
60
+
61
+ Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
62
+
63
+ Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
64
+
65
+ When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
66
+ - id: compaction
67
+ name: cordis:group
68
+ group: true
69
+ isolate:
70
+ compaction: true
71
+ toolResultPruner: true
72
+ config:
73
+ - id: compaction-basic
74
+ name: '@deepseek-ai/dsh-compaction-basic'
75
+ - id: command-compact
76
+ name: '@deepseek-ai/dsh-command-compact'
77
+ - id: tool-result-pruner
78
+ name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
79
+ config:
80
+ thresholdChars: 8192
81
+ headChars: 4096
82
+ tailChars: 1024
83
+ - id: delegation
84
+ name: cordis:group
85
+ group: true
86
+ isolate:
87
+ workflowEngine: true
88
+ config:
89
+ - id: tool-subagent-control
90
+ name: '@deepseek-ai/dsh-tool-subagent-control'
91
+ - id: tool-subagent-list-agents
92
+ name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
93
+ - id: tool-subagent
94
+ name: '@deepseek-ai/dsh-tool-subagent'
95
+ config:
96
+ provider: spawn
97
+ toolName: subagent
98
+ modelSelectionSettings: true
99
+ - id: tool-subagent-fork
100
+ name: '@deepseek-ai/dsh-tool-subagent'
101
+ config:
102
+ provider: fork
103
+ toolName: subagent_fork
104
+ - id: workflow-ptc
105
+ name: '@deepseek-ai/dsh-workflow-ptc'
106
+ config:
107
+ provider: spawn
108
+ - id: tool-workflow
109
+ name: '@deepseek-ai/dsh-tool-workflow'
110
+ - id: tool-ask-user
111
+ name: '@deepseek-ai/dsh-tool-ask-user'
112
+ - id: tool-todo
113
+ name: '@deepseek-ai/dsh-tool-todo'
114
+ config:
115
+ allowParallelInProgress: true
116
+ - id: tool-web
117
+ name: '@deepseek-ai/dsh-tool-web'
118
+ config:
119
+ fetch: true
120
+ searchTimeoutMs: 60000
121
+ - id: present
122
+ name: '@deepseek-ai/dsh-tool-present'
123
+ - id: tool-plugin-manager
124
+ name: '@deepseek-ai/dsh-plugin-manager/tools'
125
+ disabled: true
126
+ - id: tool-plugin-market
127
+ name: '@deepseek-ai/dsh-plugin-market/tools'
128
+ disabled: true
package/src/launcher.js CHANGED
@@ -6,9 +6,14 @@
6
6
 
7
7
  import { dirname, isAbsolute, join, resolve } from 'node:path'
8
8
 
9
- const DEFAULT_SOURCE_URL = 'https://github.com/zhangyang-crazy-one/deepseek-harness.git'
10
- const DEFAULT_SOURCE_REF = 'feat/deepseek-tui'
11
- const PROBE_TIMEOUT_MS = 3000
9
+ const DEFAULT_SOURCE_URL = 'https://github.com/deepseek-ai/deepseek-harness.git'
10
+ const DEFAULT_SOURCE_REF = 'dsh-v0.2.1-rc.2'
11
+ // Bounds a hung probe; a cold first start of `dsh --version` can exceed 3s.
12
+ const PROBE_TIMEOUT_MS = 10_000
13
+ /** dsh release the bundle patch and dist were composed against. */
14
+ export const COMPATIBLE_DSH_VERSION = '0.2.1-rc.2'
15
+ /** Engine release line (major.minor) whose plugin APIs the bundle targets. */
16
+ const COMPATIBLE_DSH_LINE = COMPATIBLE_DSH_VERSION.split('.').slice(0, 2).join('.')
12
17
 
13
18
  /** @typedef {'missing' | 'directory' | 'file' | 'symlink' | 'other'} PathKind */
14
19
 
@@ -37,11 +42,10 @@ const PROBE_TIMEOUT_MS = 3000
37
42
  * @property {(path: string) => PathKind} inspectPath
38
43
  * @property {(path: string) => void} makeDirectory
39
44
  * @property {(path: string) => string} readText
40
- * @property {(path: string, content: string) => void} [writeText]
41
45
  * @property {string} [nodeExecutable] Node executable for package bin entries without an executable bit.
42
46
  * @property {string} [currentDirectory] Working directory for local package resolution.
43
47
  * @property {(directory: string) => string | undefined} [resolveDshPackage] Resolve an installed @deepseek-ai/dsh package.json.
44
- * @property {(command: string, args: string[], options?: {cwd?: string, stdio?: 'inherit', capture?: boolean, timeoutMs?: number}) => CommandResult} run
48
+ * @property {(command: string, args: string[], options?: {cwd?: string, stdio?: 'inherit' | 'output', capture?: boolean, timeoutMs?: number}) => CommandResult} run
45
49
  * @property {(text: string) => void} writeOut
46
50
  * @property {(text: string) => void} writeError
47
51
  */
@@ -49,7 +53,7 @@ const PROBE_TIMEOUT_MS = 3000
49
53
  /**
50
54
  * Parse the launcher's reserved management commands and flags.
51
55
  * @param {readonly string[]} argv - arguments after the executable.
52
- * @returns {{kind: 'version'} | {kind: 'update'} | {kind: 'probe'} | {kind: 'launch', args: string[], mode?: 'auto' | 'lightweight' | 'source'}}
56
+ * @returns {{kind: 'version'} | {kind: 'update'} | {kind: 'probe'} | {kind: 'launch', args: string[], mode?: 'auto' | 'lightweight' | 'source', profile?: string}}
53
57
  */
54
58
  export function parseLauncherInvocation(argv) {
55
59
  if (argv[0] === '--') return { kind: 'launch', args: [...argv.slice(1)] }
@@ -66,10 +70,18 @@ export function parseLauncherInvocation(argv) {
66
70
  return { kind: 'probe' }
67
71
  }
68
72
  let mode = undefined
73
+ let profile = undefined
69
74
  const args = []
70
75
  for (let i = 0; i < argv.length; i++) {
71
76
  const item = argv[i]
72
- if (item === '--source') {
77
+ if (item === '--') {
78
+ args.push(...argv.slice(i + 1))
79
+ break
80
+ } else if (item === '--profile' || item.startsWith('--profile=')) {
81
+ if (profile !== undefined) throw new Error('--profile may only be specified once')
82
+ profile = item === '--profile' ? argv[++i] : item.slice('--profile='.length)
83
+ if (!profile?.trim() || profile.startsWith('-')) throw new Error('--profile needs a name')
84
+ } else if (item === '--source') {
73
85
  mode = 'source'
74
86
  } else if (item === '--lightweight') {
75
87
  mode = 'lightweight'
@@ -77,7 +89,7 @@ export function parseLauncherInvocation(argv) {
77
89
  args.push(item)
78
90
  }
79
91
  }
80
- return mode !== undefined ? { kind: 'launch', args, mode } : { kind: 'launch', args }
92
+ return { kind: 'launch', args, ...(mode === undefined ? {} : { mode }), ...(profile === undefined ? {} : { profile }) }
81
93
  }
82
94
 
83
95
  /**
@@ -180,49 +192,63 @@ export function probeInstalledDsh({ env = {}, settings, adapters }) {
180
192
  }
181
193
  }
182
194
 
195
+ /** npm name this package installs under; the tui profile lists it as a bundle. */
196
+ export const BUNDLE_PACKAGE = '@crazyhappyone/dsh-tui'
197
+ /** Both runtime modes must read the same official profile and model settings. */
198
+ const TUI_PROFILE = 'deepseek-tui'
199
+
183
200
  /**
184
- * Locate the bundled cordis.patch.yml within the package root.
201
+ * Locate this package's bundle root: the directory whose cordis.patch.yml the
202
+ * package manifest declares as `dsh.bundle.patch`.
185
203
  * @param {string | undefined} packageRoot
186
204
  * @param {LauncherAdapters} adapters
187
- * @returns {string | undefined}
205
+ * @returns {string | undefined} the package root, or undefined without a bundle patch.
188
206
  */
189
- export function findBundledPatch(packageRoot, adapters) {
207
+ export function findBundleRoot(packageRoot, adapters) {
190
208
  if (!packageRoot || typeof packageRoot !== 'string') return undefined
191
- const candidates = [
192
- join(packageRoot, 'cordis.patch.yml'),
193
- join(packageRoot, 'tui', 'cordis.patch.yml'),
194
- ]
195
- for (const candidate of candidates) {
196
- if (adapters.inspectPath(candidate) === 'file') return candidate
197
- }
198
- return undefined
209
+ // pnpm records `link:` targets without a trailing separator.
210
+ const root = resolve(packageRoot)
211
+ return adapters.inspectPath(join(root, 'cordis.patch.yml')) === 'file' ? root : undefined
199
212
  }
200
213
 
201
214
  /**
202
- * Ensure the custom tui profile uses dsh-base with startup-only patch reload.
203
- * The release patch supplies TUI plugins; the headless bundle would add a second runner.
204
- * @param {{homeDirectory?: string, env?: Readonly<Record<string, string | undefined>>, adapters: LauncherAdapters}} input
215
+ * Whether the tui profile already lists this package root as a linked bundle.
216
+ * @param {{bundleRoot: string, settings: LauncherSettings, env?: Readonly<Record<string, string | undefined>>, adapters: LauncherAdapters, profile?: string}} input
217
+ * @returns {boolean}
205
218
  */
206
- export function ensureTuiProfile({ homeDirectory, env = {}, adapters }) {
207
- const dshHome = env['DSH_HOME'] || (homeDirectory ? join(homeDirectory, '.dsh') : undefined)
208
- if (!dshHome) return
209
- const profileDir = join(dshHome, 'profiles', 'tui')
210
- const manifestPath = join(profileDir, 'package.json')
211
- if (adapters.inspectPath(manifestPath) === 'file') return
212
- adapters.makeDirectory(profileDir)
213
- const manifest = {
214
- name: 'dsh-profile-tui',
215
- private: true,
216
- dsh: {
217
- profile: {
218
- bundles: ['@deepseek-ai/dsh-base'],
219
- patchReload: 'startup',
220
- },
221
- },
222
- }
223
- if (typeof adapters.writeText === 'function') {
224
- adapters.writeText(manifestPath, JSON.stringify(manifest, undefined, 2) + '\n')
219
+ export function tuiBundleInstalled({ bundleRoot, settings, env = {}, adapters, profile = TUI_PROFILE }) {
220
+ const dshHome = env['DSH_HOME'] || (settings.homeDirectory ? join(settings.homeDirectory, '.dsh') : undefined)
221
+ if (!dshHome) return false
222
+ const manifestPath = join(dshHome, 'profiles', profile, 'package.json')
223
+ if (adapters.inspectPath(manifestPath) !== 'file') return false
224
+ let manifest
225
+ try {
226
+ manifest = JSON.parse(adapters.readText(manifestPath))
227
+ } catch {
228
+ return false
225
229
  }
230
+ return manifest?.dependencies?.[BUNDLE_PACKAGE] === `link:${bundleRoot}`
231
+ && Array.isArray(manifest?.dsh?.profile?.bundles)
232
+ && manifest.dsh.profile.bundles.includes(BUNDLE_PACKAGE)
233
+ }
234
+
235
+ /**
236
+ * Install this package as a bundle of the tui profile through dsh's own
237
+ * plugin manager, which links it, appends it after @deepseek-ai/dsh-base, and
238
+ * keeps the profile's user patch layer after it so settings edits apply.
239
+ * @param {{probe: {binPath: string, command?: string, args?: string[], cwd?: string}, bundleRoot: string, settings: LauncherSettings, env?: Readonly<Record<string, string | undefined>>, adapters: LauncherAdapters, profile?: string}} input
240
+ * @returns {{ok: true} | {ok: false, reason: string}}
241
+ */
242
+ export function ensureTuiBundle({ probe, bundleRoot, settings, env = {}, adapters, profile = TUI_PROFILE }) {
243
+ if (tuiBundleInstalled({ bundleRoot, settings, env, adapters, profile })) return { ok: true }
244
+ const result = adapters.run(
245
+ probe.command ?? probe.binPath,
246
+ [...(probe.args ?? []), 'plugin', '--profile', profile, 'add', bundleRoot],
247
+ // The install reads nothing; input typed meanwhile stays queued for the TUI.
248
+ { ...(probe.cwd === undefined ? {} : { cwd: probe.cwd }), stdio: 'output' },
249
+ )
250
+ if (result.status === 0) return { ok: true }
251
+ return { ok: false, reason: `\`dsh plugin --profile ${profile} add ${bundleRoot}\` failed with exit code ${result.status}` }
226
252
  }
227
253
 
228
254
  /**
@@ -239,7 +265,7 @@ export function runLauncher({ invocation, settings, packageVersion, adapters, en
239
265
  case 'probe':
240
266
  return showProbe({ settings, packageVersion, adapters, env })
241
267
  case 'launch':
242
- return launchTui({ args: invocation.args, mode: invocation.mode, settings, adapters, env })
268
+ return launchTui({ args: invocation.args, mode: invocation.mode, profile: invocation.profile, settings, adapters, env })
243
269
  default:
244
270
  return assertNever(invocation)
245
271
  }
@@ -264,69 +290,116 @@ function runtimeState(settings, adapters) {
264
290
  return { ok: true, initialized: true }
265
291
  }
266
292
 
267
- function launchTui({ args, mode, settings, adapters, env = {} }) {
293
+ /**
294
+ * Check a probed engine version against the bundle's release line. A different
295
+ * major.minor line changes plugin APIs the bundle calls (0.2 reworked jobs and
296
+ * llm-deepseek config), so launching would crash after the TUI takes the screen.
297
+ * @param {string} version - `dsh --version` output.
298
+ * @param {Readonly<Record<string, string | undefined>>} env - launcher environment.
299
+ * @returns {{ok: true} | {ok: false, reason: string}}
300
+ */
301
+ export function engineCompatibility(version, env = {}) {
302
+ const match = /^(\d+)\.(\d+)\./u.exec(version.trim())
303
+ if (!match || env['DSH_TUI_SKIP_ENGINE_CHECK'] === '1') return { ok: true }
304
+ if (`${match[1]}.${match[2]}` === COMPATIBLE_DSH_LINE) return { ok: true }
305
+ return {
306
+ ok: false,
307
+ reason: `installed dsh ${version.trim()} is not supported by this dsh-tui, which is built for @deepseek-ai/dsh ${COMPATIBLE_DSH_VERSION}. `
308
+ + `Install the matching engine with \`npm install --global @deepseek-ai/dsh@${COMPATIBLE_DSH_VERSION}\` `
309
+ + '(or set DSH_TUI_SKIP_ENGINE_CHECK=1 to launch anyway)',
310
+ }
311
+ }
312
+
313
+ function launchTui({ args, mode, profile = TUI_PROFILE, settings, adapters, env = {} }) {
268
314
  const effectiveMode = mode ?? settings.mode ?? 'auto'
269
315
 
270
316
  if (effectiveMode === 'source') {
271
- return launchSourceTui({ args, settings, adapters })
317
+ return launchSourceTui({ args, profile, settings, adapters, env })
272
318
  }
273
319
 
274
- const patchPath = findBundledPatch(settings.packageRoot, adapters)
320
+ const bundleRoot = findBundleRoot(settings.packageRoot, adapters)
275
321
 
276
- if (patchPath !== undefined) {
322
+ if (bundleRoot !== undefined) {
277
323
  const probe = probeInstalledDsh({ env, settings, adapters })
278
324
  if (probe.ok) {
279
- return launchLightweightTui({ args, probe, patchPath, adapters, env, settings })
325
+ const compatibility = engineCompatibility(probe.version, env)
326
+ if (!compatibility.ok) return reportError(adapters, compatibility.reason)
327
+ return launchLightweightTui({ args, profile, probe, bundleRoot, adapters, env, settings })
280
328
  }
281
329
  const explicit = settings.dshBin ?? env['DSH_BIN']
282
330
  if (effectiveMode === 'lightweight' || explicit?.trim()) {
283
- return reportError(adapters, `lightweight mode requires installed dsh (${probe.reason}); run \`npm i -g @deepseek-ai/dsh\` or use \`dsh-tui --source\``)
331
+ return reportError(adapters, `lightweight mode requires installed dsh (${probe.reason}); run \`npm i -g @deepseek-ai/dsh@${COMPATIBLE_DSH_VERSION}\` or use \`dsh-tui --source\``)
284
332
  }
285
333
  } else if (effectiveMode === 'lightweight') {
286
- return reportError(adapters, `bundled TUI patch not found under ${settings.packageRoot}; run \`dsh-tui update\` to initialize source runtime`)
334
+ return reportError(adapters, `bundle patch not found under ${settings.packageRoot}; run \`dsh-tui update\` to initialize source runtime`)
287
335
  }
288
336
 
289
337
  // Fallback to source runtime if initialized
290
338
  const state = runtimeState(settings, adapters)
291
339
  if (state.ok && state.initialized) {
292
- return launchSourceTui({ args, settings, adapters })
340
+ return launchSourceTui({ args, profile, settings, adapters, env })
293
341
  }
294
342
 
295
343
  if (state.ok && !state.initialized) {
296
344
  adapters.writeError('dsh-tui: 未检测到系统已安装的 dsh 核心引擎。\n')
297
345
  adapters.writeError('dsh-tui: 推荐通过 npm 极速安装官方运行时(无需克隆完整源码仓库,仅需数十 MB):\n')
298
- adapters.writeError('dsh-tui: npm install --global @deepseek-ai/dsh\n\n')
346
+ adapters.writeError(`dsh-tui: npm install --global @deepseek-ai/dsh@${COMPATIBLE_DSH_VERSION}\n\n`)
299
347
  adapters.writeError('dsh-tui: 若您需要进行底层源码开发与测试,可运行:\n')
300
348
  adapters.writeError('dsh-tui: dsh-tui update\n')
301
349
  adapters.writeError('dsh-tui: dsh-tui --source\n')
302
350
  return reportError(adapters, `runtime is not initialized at ${settings.runtimeDirectory}; run \`dsh-tui update\` first`)
303
351
  }
304
352
 
305
- return launchSourceTui({ args, settings, adapters })
353
+ return launchSourceTui({ args, profile, settings, adapters, env })
306
354
  }
307
355
 
308
- function launchLightweightTui({ args, probe, patchPath, adapters, env = {}, settings }) {
309
- ensureTuiProfile({ homeDirectory: settings?.homeDirectory, env, adapters })
356
+ function launchLightweightTui({ args, profile, probe, bundleRoot, adapters, env = {}, settings }) {
357
+ const installed = ensureTuiBundle({ probe, bundleRoot, settings, env, adapters, profile })
358
+ if (!installed.ok) return reportError(adapters, installed.reason)
310
359
  const result = adapters.run(
311
360
  probe.command ?? probe.binPath,
312
- [...(probe.args ?? []), '--profile', 'tui', '--patch', patchPath, ...args],
361
+ [...(probe.args ?? []), '--profile', profile, ...args],
313
362
  { stdio: 'inherit' },
314
363
  )
315
364
  return result.status
316
365
  }
317
366
 
318
- function launchSourceTui({ args, settings, adapters }) {
367
+ function launchSourceTui({ args, profile, settings, adapters, env = {} }) {
319
368
  const state = runtimeState(settings, adapters)
320
369
  if (!state.ok) return reportError(adapters, state.message)
321
370
  if (!state.initialized) {
322
371
  return reportError(adapters, `runtime is not initialized at ${settings.runtimeDirectory}; run \`dsh-tui update\` first`)
323
372
  }
324
- const result = adapters.run(
325
- settings.packageManager,
326
- ['dsh', '--profile', 'deepseek-tui', ...args],
327
- { cwd: settings.runtimeDirectory, stdio: 'inherit' },
328
- )
329
- return result.status
373
+ const bundleRoot = findBundleRoot(settings.packageRoot, adapters)
374
+ // Preserve the library's legacy fork-only entry when no packaged bundle exists.
375
+ if (bundleRoot === undefined) {
376
+ return adapters.run(settings.packageManager, ['dsh', '--profile', profile, ...args], {
377
+ cwd: settings.runtimeDirectory, stdio: 'inherit',
378
+ }).status
379
+ }
380
+ const compatibility = engineCompatibility(readDshVersion(settings.runtimeDirectory, adapters), env)
381
+ if (!compatibility.ok) return reportError(adapters, compatibility.reason)
382
+ const command = adapters.nodeExecutable ?? 'node'
383
+ const cwd = settings.runtimeDirectory
384
+ // Source aliases do not cover generated package exports. Upstream's generator
385
+ // emits these contracts without requiring a complete Web/Desktop build.
386
+ if (adapters.inspectPath(join(cwd, 'packages/api/remote-access/lib/typert.host.js')) !== 'file') {
387
+ if (adapters.inspectPath(join(cwd, 'scripts/generate-typert.ts')) !== 'file') {
388
+ return reportError(adapters, 'source runtime is missing generated contracts and scripts/generate-typert.ts; update the dsh checkout')
389
+ }
390
+ const generated = adapters.run(command, ['--import', 'tsx/esm', 'scripts/generate-typert.ts'], { cwd, stdio: 'output' })
391
+ if (generated.status !== 0) return reportCommandFailure(adapters, 'generate source runtime contracts', generated)
392
+ }
393
+ // pnpm exec preserves the checkout's pinned package-manager context for the
394
+ // profile plugin manager, without running the `predsh` installation hook.
395
+ const probe = {
396
+ command: settings.packageManager, binPath: settings.packageManager,
397
+ args: ['exec', command, '--import', 'tsx/esm', 'apps/cli/src/bin.ts'], cwd,
398
+ }
399
+ const installed = ensureTuiBundle({ probe, bundleRoot, settings, env, adapters, profile })
400
+ if (!installed.ok) return reportError(adapters, installed.reason)
401
+ // Avoid `pnpm dsh`: its pre-script reinstalls the whole workspace on every boot.
402
+ return adapters.run(probe.command, [...probe.args, '--profile', profile, ...args], { cwd, stdio: 'inherit' }).status
330
403
  }
331
404
 
332
405
  function showProbe({ settings, packageVersion, adapters, env = {} }) {
@@ -338,8 +411,17 @@ function showProbe({ settings, packageVersion, adapters, env = {} }) {
338
411
  adapters.writeOut(`dsh: installed\n`)
339
412
  adapters.writeOut(`executable: ${probe.binPath}\n`)
340
413
  adapters.writeOut(`dsh engine version: ${probe.version}\n`)
341
- const patchPath = findBundledPatch(settings.packageRoot, adapters)
342
- adapters.writeOut(`bundled patch: ${patchPath ?? 'missing'}\n`)
414
+ const compatibility = engineCompatibility(probe.version, env)
415
+ adapters.writeOut(compatibility.ok
416
+ ? `engine compatibility: ok (built for ${COMPATIBLE_DSH_VERSION})\n`
417
+ : `engine compatibility: ${compatibility.reason}\n`)
418
+ const bundleRoot = findBundleRoot(settings.packageRoot, adapters)
419
+ let bundle = 'missing'
420
+ if (bundleRoot !== undefined) {
421
+ const state = tuiBundleInstalled({ bundleRoot, settings, env, adapters }) ? 'installed in' : 'not yet installed in'
422
+ bundle = `${bundleRoot} (${state} profile ${TUI_PROFILE})`
423
+ }
424
+ adapters.writeOut(`bundle: ${bundle}\n`)
343
425
  return 0
344
426
  }
345
427
  adapters.writeOut(`dsh: not installed (${probe.reason})\n`)
@@ -1,23 +0,0 @@
1
- /** Release patch projection from the matching Harness TUI composition. */
2
-
3
- /**
4
- * Point the three bundled plugins at release artifacts, retaining all other rows.
5
- * @param {string} source - Harness packages/tui/tui/cordis.patch.yml content.
6
- * @param {string} distDirectory - Relative dist directory from the output patch.
7
- * @returns {string} Patch text suitable for dsh --patch.
8
- */
9
- export function releasePatch(source, distDirectory) {
10
- const entries = new Map([
11
- ['@deepseek-ai/dsh-tui/startup', 'startup.js'],
12
- ['./zen-proxy/index.js', 'zen-proxy.js'],
13
- ['@deepseek-ai/dsh-tui', 'index.js'],
14
- ])
15
- const patch = source.replace(/^(\s+name: )(["'])([^"']+)\2$/gmu, (line, prefix, quote, name) => {
16
- const file = entries.get(name)
17
- if (!file) return line
18
- entries.delete(name)
19
- return `${prefix}${quote}${distDirectory}/${file}${quote}`
20
- })
21
- if (entries.size) throw new Error(`TUI patch is missing plugin entries: ${[...entries.keys()].join(', ')}`)
22
- return patch
23
- }