dsh-code 1.0.3 → 1.0.5

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.
Files changed (45) hide show
  1. package/README.md +293 -285
  2. package/bin/deepseek.mjs +245 -12
  3. package/cordis.patch.yml +12 -14
  4. package/lib/index.mjs +1939 -903
  5. package/lib/types/app.d.ts +11 -2
  6. package/lib/types/commands.d.ts +13 -0
  7. package/lib/types/git-workflow.d.ts +7 -2
  8. package/lib/types/history.d.ts +18 -11
  9. package/lib/types/index.d.ts +28 -0
  10. package/lib/types/input-split.d.ts +54 -0
  11. package/lib/types/kernel-panels.d.ts +3 -1
  12. package/lib/types/keyboard.d.ts +8 -0
  13. package/lib/types/presets.d.ts +4 -1
  14. package/lib/types/provider-settings.d.ts +77 -0
  15. package/lib/types/questions.d.ts +16 -12
  16. package/lib/types/render/projection.d.ts +9 -2
  17. package/lib/types/render/status.d.ts +22 -15
  18. package/lib/types/settings-file.d.ts +8 -0
  19. package/lib/types/skills.d.ts +1 -1
  20. package/package.json +49 -46
  21. package/src/app.ts +5459 -4900
  22. package/src/approval.ts +8 -3
  23. package/src/authorization-panel.ts +2 -4
  24. package/src/commands.ts +27 -3
  25. package/src/git-workflow.ts +29 -10
  26. package/src/history.ts +22 -13
  27. package/src/index.ts +203 -61
  28. package/src/input-split.ts +191 -0
  29. package/src/internals.ts +26 -8
  30. package/src/kernel-panels.ts +26 -10
  31. package/src/keyboard.ts +123 -88
  32. package/src/mentions.ts +42 -9
  33. package/src/permissions.ts +1 -1
  34. package/src/presets.ts +19 -6
  35. package/src/provider-settings.ts +204 -0
  36. package/src/questions.ts +58 -55
  37. package/src/render/export.ts +7 -7
  38. package/src/render/lines.ts +24 -12
  39. package/src/render/markdown.ts +15 -13
  40. package/src/render/projection.ts +101 -13
  41. package/src/render/status.ts +76 -71
  42. package/src/render/text.ts +9 -3
  43. package/src/settings-file.ts +38 -6
  44. package/src/skills.ts +19 -6
  45. package/src/theme-panel.ts +79 -72
package/bin/deepseek.mjs CHANGED
@@ -4,7 +4,7 @@ import { spawn, spawnSync } from 'node:child_process'
4
4
  import { existsSync, readFileSync, realpathSync } from 'node:fs'
5
5
  import { createRequire } from 'node:module'
6
6
  import { homedir } from 'node:os'
7
- import { join, resolve } from 'node:path'
7
+ import { dirname, join, resolve } from 'node:path'
8
8
  import { fileURLToPath, pathToFileURL } from 'node:url'
9
9
 
10
10
  const packageRequire = createRequire(import.meta.url)
@@ -49,6 +49,81 @@ export function profileHasDshCode(profileDir = cliProfileDir(), fileExists = exi
49
49
  }
50
50
  }
51
51
 
52
+ /** The dsh-code dependency the cli profile declares (e.g. '1.0.5' or 'link:C:/repo'). */
53
+ export function profileDependencySpec(profileDir = cliProfileDir(), fileExists = existsSync, readFile = readFileSync) {
54
+ const manifest = join(profileDir, 'package.json')
55
+ if (!fileExists(manifest)) return undefined
56
+ try {
57
+ const raw = JSON.parse(readFile(manifest, 'utf8'))
58
+ const spec = raw?.dependencies?.['dsh-code']
59
+ return typeof spec === 'string' ? spec : undefined
60
+ } catch {
61
+ return undefined
62
+ }
63
+ }
64
+
65
+ /** The dsh-code version the cli profile actually resolves and boots. */
66
+ export function profileMountedVersion(profileDir = cliProfileDir(), fileExists = existsSync, readFile = readFileSync) {
67
+ const manifest = join(profileDir, 'node_modules', 'dsh-code', 'package.json')
68
+ if (!fileExists(manifest)) return undefined
69
+ try {
70
+ return JSON.parse(readFile(manifest, 'utf8'))?.version ?? undefined
71
+ } catch {
72
+ return undefined
73
+ }
74
+ }
75
+
76
+ /** The harness release line a dsh-code release expects, read from its peers. */
77
+ export function harnessLineFromPeers(peers) {
78
+ if (typeof peers !== 'object' || peers === null) return undefined
79
+ const anchor = peers['@deepseek-ai/dsh-session']
80
+ if (typeof anchor === 'string' && anchor !== '') return anchor
81
+ for (const [name, spec] of Object.entries(peers)) {
82
+ if (name.startsWith('@deepseek-ai/dsh-') && typeof spec === 'string' && spec !== '') return spec
83
+ }
84
+ return undefined
85
+ }
86
+
87
+ /**
88
+ * Decide what `update --apply` installs. The global DSH launcher is pinned
89
+ * to the harness line the target dsh-code release declares in its peers,
90
+ * so the launcher can never move ahead of the plugin it must boot. A
91
+ * profile that mounts a local checkout (link:/file:) keeps its mount.
92
+ */
93
+ export function updatePlan({ latestCode, peers, profileSpec }) {
94
+ const line = harnessLineFromPeers(peers)
95
+ return {
96
+ dshSpec: line === undefined ? '@deepseek-ai/dsh@latest' : `@deepseek-ai/dsh@${line}`,
97
+ lineLocked: line !== undefined,
98
+ codeSpec: `dsh-code@${latestCode}`,
99
+ profileStep: !(typeof profileSpec === 'string' && /^(link|file):/iu.test(profileSpec)),
100
+ }
101
+ }
102
+
103
+ /** Run wrapper-owned child steps in order, stopping at the first failure. */
104
+ export function runSequence(steps, spawnProcess = spawn) {
105
+ return new Promise(resolve => {
106
+ const run = index => {
107
+ const step = steps[index]
108
+ if (step === undefined) {
109
+ resolve(0)
110
+ return
111
+ }
112
+ const needsShell = process.platform === 'win32' && /\.(cmd|bat)$/iu.test(step.command)
113
+ const child = spawnProcess(step.command, step.args, { stdio: 'inherit', ...(needsShell ? { shell: true } : {}) })
114
+ child.once('error', error => {
115
+ console.error(`dsh-code: ${step.label} failed: ${error.message}`)
116
+ resolve(1)
117
+ })
118
+ child.once('exit', (code, signal) => {
119
+ if (code === 0) run(index + 1)
120
+ else resolve(code ?? 1)
121
+ })
122
+ }
123
+ run(0)
124
+ })
125
+ }
126
+
52
127
  /** Npm global-prefix roots that may contain DSH when this launcher is globally linked to a checkout. */
53
128
  export function globalDshRoots() {
54
129
  return [...new Set([
@@ -58,6 +133,12 @@ export function globalDshRoots() {
58
133
  ].filter(Boolean))]
59
134
  }
60
135
 
136
+ /** Convert a file URL to a windows-style path even when resolved on a non-windows host. */
137
+ function fileUrlToWindowsPath(url) {
138
+ const pathname = decodeURIComponent(new URL(url).pathname)
139
+ return pathname.replace(/^\/([A-Za-z]:)/, '$1').replace(/\//g, '\\')
140
+ }
141
+
61
142
  /** Resolve the DSH entrypoint without passing user arguments through a Windows shell. */
62
143
  export function rawDshCommand(args, {
63
144
  platform = process.platform,
@@ -67,7 +148,9 @@ export function rawDshCommand(args, {
67
148
  resolvePackage = packageRequire.resolve,
68
149
  } = {}) {
69
150
  if (platform !== 'win32') return { command: 'dsh', args }
70
- const adjacentEntrypoint = fileURLToPath(new URL('../../@deepseek-ai/dsh/lib/bin.js', moduleUrl))
151
+ // fileURLToPath follows the HOST separator; the win32 target needs a
152
+ // windows-style path regardless of where this resolver itself runs.
153
+ const adjacentEntrypoint = fileUrlToWindowsPath(new URL('../../@deepseek-ai/dsh/lib/bin.js', moduleUrl))
71
154
  if (fileExists(adjacentEntrypoint)) return { command: process.execPath, args: [adjacentEntrypoint, ...args] }
72
155
  for (const root of roots) {
73
156
  try {
@@ -111,7 +194,12 @@ function printDoctor(resolveCommand = rawDshCommand, spawnCommand = spawnSync) {
111
194
  }
112
195
 
113
196
  function launchChild(command, args) {
114
- const child = spawn(command, args, { stdio: 'inherit' })
197
+ // Windows .cmd/.bat shims (npm.cmd) need a shell since Node's
198
+ // CVE-2024-27980 fix rejects spawning them directly with EINVAL. Every
199
+ // launch through here uses fixed, wrapper-owned arguments, so the shell
200
+ // surface adds no injection risk.
201
+ const needsShell = process.platform === 'win32' && /\.(cmd|bat)$/iu.test(command)
202
+ const child = spawn(command, args, { stdio: 'inherit', ...(needsShell ? { shell: true } : {}) })
115
203
  child.once('error', error => {
116
204
  console.error(`dsh-code: command failed: ${error.message}`)
117
205
  process.exitCode = 1
@@ -120,6 +208,144 @@ function launchChild(command, args) {
120
208
  return child
121
209
  }
122
210
 
211
+ /**
212
+ * How to invoke npm. The JavaScript entrypoint is preferred: running it
213
+ * with this process's node avoids the Windows .cmd shim, whose shell
214
+ * workaround draws a Node deprecation warning on every call.
215
+ */
216
+ export function npmInvocation({
217
+ fileExists = existsSync,
218
+ roots = globalDshRoots(),
219
+ resolvePackage = packageRequire.resolve,
220
+ } = {}) {
221
+ const besideNode = join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js')
222
+ if (fileExists(besideNode)) return { command: process.execPath, args: [besideNode] }
223
+ for (const root of roots) {
224
+ try {
225
+ const cli = resolvePackage('npm/bin/npm-cli.js', { paths: [root] })
226
+ return { command: process.execPath, args: [cli] }
227
+ } catch {
228
+ // The next configured global prefix may own npm instead.
229
+ }
230
+ }
231
+ return { command: process.platform === 'win32' ? 'npm.cmd' : 'npm', args: [] }
232
+ }
233
+
234
+ /**
235
+ * Read one `npm view` field as JSON, or undefined when the query fails.
236
+ * The subject arrives as argument parts: without a shell, a space inside
237
+ * one argument would reach npm as a single selector and fail.
238
+ */
239
+ function viewJson(subjectParts, spawnCommand = spawnSync, invocation = npmInvocation()) {
240
+ const needsShell = process.platform === 'win32' && /\.(cmd|bat)$/iu.test(invocation.command)
241
+ const result = spawnCommand(invocation.command, [...invocation.args, 'view', ...subjectParts, '--json'], { encoding: 'utf8', windowsHide: true, ...(needsShell ? { shell: true } : {}) })
242
+ if (result.status !== 0) return undefined
243
+ try {
244
+ return JSON.parse(result.stdout)
245
+ } catch {
246
+ return undefined
247
+ }
248
+ }
249
+
250
+ /**
251
+ * The version of @deepseek-ai/dsh this launcher would boot, when present.
252
+ * Probes the same surfaces rawDshCommand resolves: the adjacent global
253
+ * install first, then every configured npm global prefix.
254
+ */
255
+ function installedDshVersion({
256
+ platform = process.platform,
257
+ moduleUrl = import.meta.url,
258
+ fileExists = existsSync,
259
+ roots = globalDshRoots(),
260
+ resolvePackage = packageRequire.resolve,
261
+ } = {}) {
262
+ const readVersion = path => {
263
+ try {
264
+ return JSON.parse(readFileSync(path, 'utf8')).version
265
+ } catch {
266
+ return undefined
267
+ }
268
+ }
269
+ if (platform === 'win32') {
270
+ const adjacent = fileUrlToWindowsPath(new URL('../../@deepseek-ai/dsh/package.json', moduleUrl))
271
+ if (fileExists(adjacent)) return readVersion(adjacent)
272
+ } else {
273
+ try {
274
+ const adjacent = fileURLToPath(new URL('../../@deepseek-ai/dsh/package.json', moduleUrl))
275
+ if (fileExists(adjacent)) return readVersion(adjacent)
276
+ } catch {
277
+ // A non-file URL or an unreadable sibling falls through to the roots.
278
+ }
279
+ }
280
+ for (const root of roots) {
281
+ try {
282
+ return readVersion(resolvePackage('@deepseek-ai/dsh/package.json', { paths: [root] }))
283
+ } catch {
284
+ // The next configured global prefix may own DSH instead.
285
+ }
286
+ }
287
+ return undefined
288
+ }
289
+
290
+ /** Show what is installed, what is latest, and what the cli profile boots. */
291
+ function printUpdateStatus() {
292
+ const codeLatest = viewJson(['dsh-code', 'version'])
293
+ const dshLatest = viewJson(['@deepseek-ai/dsh', 'version'])
294
+ const dshInstalled = installedDshVersion()
295
+ const spec = profileDependencySpec()
296
+ const mounted = profileMountedVersion()
297
+ console.log(`dsh-code: ${packageVersion} (this launcher), latest ${codeLatest ?? 'unknown'}`)
298
+ console.log(`@deepseek-ai/dsh: ${dshInstalled ?? 'not found'} (global), latest ${dshLatest ?? 'unknown'}`)
299
+ console.log(`cli profile: ${mounted !== undefined ? `dsh-code ${mounted}` : spec ?? 'not mounted'}`)
300
+ console.log('Run `deepseek update --apply` to upgrade the global packages and the cli profile together.')
301
+ }
302
+
303
+ /**
304
+ * Upgrade the global launcher and the cli profile plugin together. The
305
+ * global install is pinned to the harness line the new dsh-code release
306
+ * declares, the profile follows through `dsh plugin add` with the same
307
+ * pinned spec, and the mounted version is verified at the end.
308
+ */
309
+ async function applyUpdate({
310
+ view = viewJson,
311
+ resolveCommand = rawDshCommand,
312
+ spawnProcess = spawn,
313
+ } = {}) {
314
+ const npm = npmInvocation()
315
+ const latestCode = view(['dsh-code', 'version'], undefined, npm)
316
+ if (latestCode === undefined) {
317
+ console.error('dsh-code: could not read the latest dsh-code version from npm')
318
+ process.exitCode = 1
319
+ return
320
+ }
321
+ const peers = view([`dsh-code@${latestCode}`, 'peerDependencies'], undefined, npm)
322
+ const plan = updatePlan({ latestCode, peers, profileSpec: profileDependencySpec() })
323
+ if (!plan.lineLocked) {
324
+ console.log('dsh-code: could not read the compatible harness line; installing @deepseek-ai/dsh@latest')
325
+ }
326
+ const steps = [{ command: npm.command, args: [...npm.args, 'install', '-g', plan.dshSpec, plan.codeSpec], label: 'npm install' }]
327
+ if (plan.profileStep) {
328
+ const command = resolveCommand(['plugin', '--profile', 'cli', 'add', plan.codeSpec])
329
+ if (command === undefined) {
330
+ console.error('dsh-code: could not resolve the dsh command for the profile update')
331
+ process.exitCode = 1
332
+ return
333
+ }
334
+ steps.push({ command: command.command, args: command.args, label: 'dsh plugin add' })
335
+ } else {
336
+ console.log('cli profile mounts a local checkout; leaving the profile untouched')
337
+ }
338
+ const code = await runSequence(steps, spawnProcess)
339
+ const mounted = profileMountedVersion()
340
+ if (mounted !== undefined) console.log(`cli profile now mounts dsh-code ${mounted}`)
341
+ if (code === 0 && plan.profileStep && mounted !== latestCode) {
342
+ console.error(`dsh-code: the cli profile still mounts ${mounted ?? 'nothing'}; run: dsh plugin --profile cli add ${plan.codeSpec}`)
343
+ process.exitCode = 1
344
+ return
345
+ }
346
+ process.exitCode = code
347
+ }
348
+
123
349
  /** Run one wrapper-owned operational command. */
124
350
  export function launchOperation(args = process.argv.slice(2)) {
125
351
  const operation = operationName(args)
@@ -148,16 +374,14 @@ export function launchOperation(args = process.argv.slice(2)) {
148
374
  }
149
375
  return launchChild(command.command, command.args)
150
376
  }
151
- if (args.includes('--apply')) {
152
- return launchChild(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install', '-g', '@deepseek-ai/dsh', 'dsh-code'])
153
- }
154
- const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
155
- for (const name of ['@deepseek-ai/dsh', 'dsh-code']) {
156
- const result = spawnSync(npm, ['view', name, 'version'], { encoding: 'utf8', windowsHide: true })
157
- console.log(`${name}: ${result.status === 0 ? String(result.stdout).trim() : 'version check failed'}`)
377
+ if (operation === 'update') {
378
+ if (args.includes('--apply')) {
379
+ void applyUpdate()
380
+ return true
381
+ }
382
+ printUpdateStatus()
383
+ return true
158
384
  }
159
- console.log('Run `deepseek update --apply` to install the latest versions.')
160
- return true
161
385
  }
162
386
 
163
387
  /** Launch the installed DSH CLI while preserving its exit status and stdio. */
@@ -166,12 +390,21 @@ export function launchDsh(
166
390
  spawnProcess = spawn,
167
391
  resolveCommand = dshCommand,
168
392
  isProfileReady = profileHasDshCode,
393
+ isInteractiveStdin = () => process.stdin.isTTY === true,
169
394
  ) {
170
395
  if (!isProfileReady()) {
171
396
  console.error('dsh-code: the cli profile does not mount dsh-code yet. Run: dsh plugin --profile cli add dsh-code')
172
397
  process.exitCode = 1
173
398
  return undefined
174
399
  }
400
+ // The interactive TUI is a raw-mode terminal application: with piped or
401
+ // otherwise non-TTY stdin it would die deep inside Ink's raw-mode gate
402
+ // with a cryptic stack. Fail here with one actionable line instead.
403
+ if (!isInteractiveStdin()) {
404
+ console.error('dsh-code: this terminal UI requires an interactive TTY on stdin; run it in a real terminal instead of a pipe')
405
+ process.exitCode = 1
406
+ return undefined
407
+ }
175
408
  const command = resolveCommand(args)
176
409
  if (command === undefined) {
177
410
  console.error('dsh-code: @deepseek-ai/dsh must be installed globally beside dsh-code; run: npm install -g @deepseek-ai/dsh dsh-code')
package/cordis.patch.yml CHANGED
@@ -40,6 +40,8 @@
40
40
  disabled: true
41
41
  - id: tool-skill
42
42
  disabled: true
43
+ - id: command-goal
44
+ disabled: true
43
45
  - id: tool-goal
44
46
  disabled: true
45
47
  - id: plan-mode
@@ -68,15 +70,9 @@
68
70
  disabled: true
69
71
  - id: tool-todo
70
72
  disabled: true
71
- - id: tool-web
72
- config:
73
- # web_fetch is a core TUI capability, not an opt-in: the fetch provider
74
- # (web-fetch-http below) needs no API key. The per-preset tool-web rows
75
- # keep `fetch: false` on the agent plane; through the scope-layered tool
76
- # registry the preset's web_search shadows the host's same-name tool while
77
- # the host's web_fetch stays visible, so every model still sees both.
78
- fetch: true
79
- searchTimeoutMs: 60000
73
+ # tool-web keeps the 0.1.2-rc.1 base default (web_fetch enabled over the
74
+ # bundled web-fetch-http provider): the host row exposes web_fetch while the
75
+ # per-preset agent-plane rows keep their own fetch stance.
80
76
 
81
77
  - insert:
82
78
  # Code Mode is a core execution capability, not a Web component.
@@ -111,11 +107,13 @@
111
107
  - id: cordis-host-runner
112
108
  name: '@deepseek-ai/dsh-cordis-host-runner'
113
109
 
114
- # Anonymous public HTTP(S) fetch provider for web_fetch (no key): the web
115
- # service and its search provider stay on the host plane (dsh-base), and
116
- # this row is what makes fetching real URLs work out of the box.
117
- - id: web-fetch-http
118
- name: '@deepseek-ai/dsh-web-fetch-http'
110
+ # Host-owned opt-in the preset delegation tools sample: standard/ptc's
111
+ # tool-subagent rows set `modelSelectionSettings: true`, whose start
112
+ # resolves this service and fails the whole preset mount when absent
113
+ # ("failed to apply loader entry delegation (cordis:group)"). The upstream
114
+ # web-app bundle adds this host row in 0.1.2-rc.1; dsh-base does not ship it.
115
+ - id: subagent-model-selection-settings
116
+ name: '@deepseek-ai/dsh-tool-subagent/model-selection-settings'
119
117
 
120
118
  # Interactive provider authorization. The base bundle supplies the
121
119
  # credential store; mounting the neutral authorization seam wakes the