dsh-code 1.0.6 → 1.2.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.en.md +123 -26
- package/README.md +124 -27
- package/bin/deepseek.mjs +283 -35
- package/cordis.patch.yml +97 -0
- package/lib/index.mjs +5008 -881
- package/lib/session-query.mjs +150 -0
- package/lib/startup.mjs +4 -4
- package/lib/{theme-DCT8Y2xf.mjs → theme-7u5Qo3dF.mjs} +657 -20
- package/lib/types/app.d.ts +106 -62
- package/lib/types/authorization-panel.d.ts +3 -3
- package/lib/types/git-workflow.d.ts +91 -2
- package/lib/types/i18n.d.ts +39 -0
- package/lib/types/index.d.ts +100 -1
- package/lib/types/input-split.d.ts +1 -1
- package/lib/types/kernel-panels.d.ts +107 -29
- package/lib/types/language-panel.d.ts +12 -0
- package/lib/types/locales/en.d.ts +450 -0
- package/lib/types/locales/zh.d.ts +9 -0
- package/lib/types/mentions.d.ts +7 -3
- package/lib/types/models.d.ts +14 -0
- package/lib/types/panel-accent.d.ts +28 -0
- package/lib/types/rainbow.d.ts +69 -0
- package/lib/types/render/animations.d.ts +42 -0
- package/lib/types/render/editor.d.ts +4 -3
- package/lib/types/render/ime-cursor.d.ts +60 -0
- package/lib/types/render/inspector.d.ts +26 -0
- package/lib/types/render/lines.d.ts +21 -1
- package/lib/types/render/markdown.d.ts +1 -1
- package/lib/types/render/projection.d.ts +130 -4
- package/lib/types/render/status.d.ts +9 -9
- package/lib/types/render/text.d.ts +6 -0
- package/lib/types/render/usage.d.ts +113 -0
- package/lib/types/session-directory.d.ts +17 -0
- package/lib/types/session-query.d.ts +92 -0
- package/lib/types/startup.d.ts +1 -1
- package/lib/types/terminal-title.d.ts +66 -0
- package/lib/types/theme-panel.d.ts +2 -2
- package/lib/types/theme.d.ts +271 -52
- package/lib/types/update-panel.d.ts +49 -0
- package/lib/types/update.d.ts +75 -0
- package/lib/types/version.d.ts +4 -3
- package/package.json +246 -90
- package/src/app.ts +1369 -509
- package/src/approval.ts +166 -166
- package/src/authorization-panel.ts +19 -16
- package/src/editor-keys.ts +371 -371
- package/src/git-workflow.ts +229 -3
- package/src/i18n.ts +68 -0
- package/src/index.ts +534 -80
- package/src/input-split.ts +3 -3
- package/src/internals.ts +5 -0
- package/src/kernel-panels.ts +554 -86
- package/src/keyboard.ts +5 -4
- package/src/language-panel.ts +53 -0
- package/src/locales/en.ts +489 -0
- package/src/locales/zh.ts +488 -0
- package/src/mentions.ts +8 -4
- package/src/models.ts +264 -212
- package/src/panel-accent.ts +41 -0
- package/src/presets.ts +1 -1
- package/src/provider-settings.ts +1 -1
- package/src/rainbow.ts +208 -0
- package/src/render/animations.ts +104 -6
- package/src/render/editor.ts +25 -24
- package/src/render/export.ts +116 -95
- package/src/render/ime-cursor.ts +147 -0
- package/src/render/inspector.ts +42 -0
- package/src/render/lines.ts +628 -415
- package/src/render/markdown.ts +15 -3
- package/src/render/projection.ts +572 -21
- package/src/render/status.ts +59 -39
- package/src/render/text.ts +14 -0
- package/src/render/tool-preview.ts +77 -77
- package/src/render/usage.ts +430 -0
- package/src/render/width.ts +2 -2
- package/src/session-directory.ts +8 -6
- package/src/session-query.ts +239 -0
- package/src/startup.ts +3 -3
- package/src/subagents.ts +229 -229
- package/src/terminal-title.ts +190 -0
- package/src/theme-panel.ts +17 -21
- package/src/theme.ts +281 -33
- package/src/update-panel.ts +256 -0
- package/src/update.ts +126 -0
- package/src/version.ts +58 -20
- package/src/whale-glyph.ts +23 -23
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The /update panel: one bounded surface over the launcher update
|
|
3
|
+
* pipeline. The panel never decides versions itself — it renders the
|
|
4
|
+
* launcher `update --json` probe (plan plus refusals), takes one
|
|
5
|
+
* confirmation, then streams `update --apply` progress and reports the
|
|
6
|
+
* result with a restart hint. Every alignment guarantee (host pinned to
|
|
7
|
+
* the release peers line, companion plugins carried, downgrade and
|
|
8
|
+
* local-checkout refusals) lives in the launcher and is only displayed
|
|
9
|
+
* here.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createElement, useEffect, useState, type ReactElement } from 'react'
|
|
13
|
+
import { Box, Text, useInput, useStdout } from 'ink'
|
|
14
|
+
import type { LauncherUpdateStatus } from './update.ts'
|
|
15
|
+
import { clampScroll, panelViewport } from './render/inspector.ts'
|
|
16
|
+
import { singleLineText, truncateColumns } from './render/text.ts'
|
|
17
|
+
import { panelAccent } from './panel-accent.ts'
|
|
18
|
+
import { getPalette, inkColor } from './theme.ts'
|
|
19
|
+
import { t } from './i18n.ts'
|
|
20
|
+
|
|
21
|
+
/** Retained apply-progress lines (ring tail; npm output is ephemeral). */
|
|
22
|
+
export const UPDATE_OUTPUT_CAP = 800
|
|
23
|
+
|
|
24
|
+
/** Keep the newest UPDATE_OUTPUT_CAP lines of streamed update output. */
|
|
25
|
+
export function clipUpdateLines(lines: readonly string[]): readonly string[] {
|
|
26
|
+
return lines.length <= UPDATE_OUTPUT_CAP ? lines : lines.slice(lines.length - UPDATE_OUTPUT_CAP)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One display row of the update surface. */
|
|
30
|
+
export interface UpdateRow {
|
|
31
|
+
readonly key: string
|
|
32
|
+
readonly text: string
|
|
33
|
+
readonly tone?: 'ok' | 'warn' | 'error' | 'dim'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The plan view derived from one probe: facts to show and whether apply may run. */
|
|
37
|
+
export interface UpdatePlanView {
|
|
38
|
+
readonly rows: readonly UpdateRow[]
|
|
39
|
+
readonly runnable: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Rendered facts of one probe: current/target versions, actions, refusals. */
|
|
43
|
+
export function updatePlanView(status: LauncherUpdateStatus): UpdatePlanView {
|
|
44
|
+
const rows: UpdateRow[] = []
|
|
45
|
+
const latest = status.code.latest ?? 'unknown'
|
|
46
|
+
rows.push(status.aheadOfRegistry === true
|
|
47
|
+
? { key: 'code', text: `dsh-code ${status.code.running} (newer than npm ${latest})` }
|
|
48
|
+
: status.code.latest !== null && status.code.latest === status.code.running
|
|
49
|
+
? { key: 'code', text: `dsh-code ${status.code.running} (latest)` }
|
|
50
|
+
: { key: 'code', text: `dsh-code ${status.code.running} → ${latest}` })
|
|
51
|
+
if (status.host.targetLine === null) {
|
|
52
|
+
rows.push({ key: 'host', text: 'harness pinned line unreadable — update would install @deepseek-ai/dsh@latest', tone: 'warn' })
|
|
53
|
+
} else if (status.host.installed === null) {
|
|
54
|
+
rows.push({ key: 'host', text: `harness not installed → ${status.host.targetLine}` })
|
|
55
|
+
} else if (status.host.installed === status.host.targetLine) {
|
|
56
|
+
rows.push({ key: 'host', text: `harness ${status.host.installed} (on pinned line)` })
|
|
57
|
+
} else {
|
|
58
|
+
rows.push({ key: 'host', text: `harness ${status.host.installed} → ${status.host.targetLine}` })
|
|
59
|
+
}
|
|
60
|
+
const mounted = status.profile.mounted === null
|
|
61
|
+
? status.profile.spec ?? 'not mounted'
|
|
62
|
+
: `dsh-code ${status.profile.mounted}`
|
|
63
|
+
rows.push({
|
|
64
|
+
key: 'profile',
|
|
65
|
+
text: `profile ${mounted}${status.profile.localCheckout ? ' · local checkout' : ''}`,
|
|
66
|
+
tone: status.profile.localCheckout ? 'dim' : undefined,
|
|
67
|
+
})
|
|
68
|
+
for (const plugin of status.plan.pluginSpecs) {
|
|
69
|
+
rows.push({ key: `plugin:${plugin}`, text: `plugin ${plugin}`, tone: 'dim' })
|
|
70
|
+
}
|
|
71
|
+
// A blocker key may be absent entirely (JSON.stringify drops undefined);
|
|
72
|
+
// a missing value reads as "no blocker", never as an undefined row body.
|
|
73
|
+
const registryBlocker = status.blockers.registry ?? null
|
|
74
|
+
if (registryBlocker !== null) {
|
|
75
|
+
rows.push({ key: 'blocker:registry', text: registryBlocker, tone: 'error' })
|
|
76
|
+
}
|
|
77
|
+
if (status.blockers.downgrade) {
|
|
78
|
+
rows.push({
|
|
79
|
+
key: 'blocker:downgrade',
|
|
80
|
+
text: `refusing to downgrade the host: dsh-code@${latest} needs ${status.host.targetLine ?? 'the pinned line'}, but ${status.host.installed ?? 'the installed host'} is newer — wait for the next dsh-code release`,
|
|
81
|
+
tone: 'error',
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
const checkoutBlocker = status.blockers.localCheckout ?? null
|
|
85
|
+
if (checkoutBlocker !== null) {
|
|
86
|
+
for (const [index, line] of checkoutBlocker.entries()) {
|
|
87
|
+
rows.push({ key: `blocker:checkout:${index}`, text: line, tone: 'error' })
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (status.aheadOfRegistry === true) {
|
|
91
|
+
rows.push({ key: 'ahead', text: 'this install is newer than npm latest — refusing to downgrade', tone: 'warn' })
|
|
92
|
+
} else if (status.upToDate) {
|
|
93
|
+
rows.push({ key: 'uptodate', text: 'everything is already on the pinned line — nothing to update', tone: 'ok' })
|
|
94
|
+
}
|
|
95
|
+
const runnable = status.upToDate !== true
|
|
96
|
+
&& registryBlocker === null
|
|
97
|
+
&& status.blockers.downgrade !== true
|
|
98
|
+
&& checkoutBlocker === null
|
|
99
|
+
return { rows, runnable }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Panel lifecycle phases; the footer names the keys each phase accepts. */
|
|
103
|
+
export type UpdatePhase = 'probe' | 'error' | 'plan' | 'apply' | 'done'
|
|
104
|
+
|
|
105
|
+
/** Footer hint line per phase; the plan phase names the confirm key only when runnable. */
|
|
106
|
+
export function updateFooter(phase: UpdatePhase, runnable: boolean, upToDate: boolean, aheadOfRegistry = false): string {
|
|
107
|
+
if (phase === 'probe') return t('panel.update.footer.probe')
|
|
108
|
+
if (phase === 'error') return t('panel.update.footer.error')
|
|
109
|
+
if (phase === 'apply') return t('panel.update.footer.apply')
|
|
110
|
+
if (phase === 'done') return t('panel.update.footer.error')
|
|
111
|
+
if (aheadOfRegistry) return t('panel.update.footer.ahead')
|
|
112
|
+
if (upToDate) return t('panel.update.footer.current')
|
|
113
|
+
return runnable ? t('panel.update.footer.update') : t('panel.update.footer.blocked')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The /update surface: probe on open (and on r), confirm with enter/y,
|
|
118
|
+
* stream the aligned apply, and land on a bounded result view. Escape is
|
|
119
|
+
* locked while the apply child runs — killing npm mid-install is exactly
|
|
120
|
+
* the half-updated state this command exists to prevent.
|
|
121
|
+
*/
|
|
122
|
+
export function UpdatePanel({ probe, apply, close, notify }: {
|
|
123
|
+
/** Read-only probe of the launcher update status (never installs). */
|
|
124
|
+
probe: () => Promise<LauncherUpdateStatus>
|
|
125
|
+
/** Run the aligned update; streams sanitized progress lines. */
|
|
126
|
+
apply: (onLine: (line: string) => void, plan: LauncherUpdateStatus['plan']) => Promise<number>
|
|
127
|
+
/** Close the panel (App keeps ownership of the flag). */
|
|
128
|
+
close: () => void
|
|
129
|
+
/** One bounded cross-surface notice (phase completions). */
|
|
130
|
+
notify: (text: string, tone?: 'info' | 'warning' | 'error') => void
|
|
131
|
+
}): ReactElement {
|
|
132
|
+
const [phase, setPhase] = useState<UpdatePhase>('probe')
|
|
133
|
+
const [status, setStatus] = useState<LauncherUpdateStatus>()
|
|
134
|
+
const [probeError, setProbeError] = useState<string>()
|
|
135
|
+
const [lines, setLines] = useState<readonly string[]>([])
|
|
136
|
+
const [exit, setExit] = useState<number>()
|
|
137
|
+
const [applyError, setApplyError] = useState<string>()
|
|
138
|
+
// Viewport anchor: 'tail' follows new output; a number pins the first
|
|
139
|
+
// visible row (up moves away from the tail, down onto it re-follows).
|
|
140
|
+
const [anchor, setAnchor] = useState<'tail' | number>('tail')
|
|
141
|
+
const [epoch, setEpoch] = useState(0)
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
let disposed = false
|
|
144
|
+
setPhase('probe')
|
|
145
|
+
setStatus(undefined)
|
|
146
|
+
setProbeError(undefined)
|
|
147
|
+
setAnchor('tail')
|
|
148
|
+
probe().then(value => {
|
|
149
|
+
if (disposed) return
|
|
150
|
+
setStatus(value)
|
|
151
|
+
setPhase('plan')
|
|
152
|
+
}, reason => {
|
|
153
|
+
if (disposed) return
|
|
154
|
+
setProbeError(reason instanceof Error ? reason.message : String(reason))
|
|
155
|
+
setPhase('error')
|
|
156
|
+
})
|
|
157
|
+
return () => { disposed = true }
|
|
158
|
+
}, [epoch, probe])
|
|
159
|
+
const stdout = useStdout().stdout
|
|
160
|
+
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
161
|
+
const start = (): void => {
|
|
162
|
+
if (phase !== 'plan' || status === undefined) return
|
|
163
|
+
if (!updatePlanView(status).runnable) return
|
|
164
|
+
setPhase('apply')
|
|
165
|
+
setLines([])
|
|
166
|
+
setExit(undefined)
|
|
167
|
+
setApplyError(undefined)
|
|
168
|
+
setAnchor('tail')
|
|
169
|
+
apply(line => {
|
|
170
|
+
setLines(previous => clipUpdateLines([...previous, singleLineText(line)]))
|
|
171
|
+
}, status.plan).then(code => {
|
|
172
|
+
setExit(code)
|
|
173
|
+
setPhase('done')
|
|
174
|
+
notify(code === 0 ? 'update installed — restart dsh to activate' : `update failed (exit ${code})`, code === 0 ? 'info' : 'error')
|
|
175
|
+
}, reason => {
|
|
176
|
+
const message = reason instanceof Error ? reason.message : String(reason)
|
|
177
|
+
setApplyError(message)
|
|
178
|
+
setPhase('done')
|
|
179
|
+
notify(`update failed: ${message}`, 'error')
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
const planView = status === undefined ? undefined : updatePlanView(status)
|
|
183
|
+
const rows: readonly UpdateRow[] = phase === 'probe'
|
|
184
|
+
? [{ key: 'probe', text: 'checking npm for the aligned update…', tone: 'dim' }]
|
|
185
|
+
: phase === 'error'
|
|
186
|
+
? [{ key: 'error', text: singleLineText(probeError ?? 'probe failed'), tone: 'error' }]
|
|
187
|
+
: phase === 'plan' && planView !== undefined
|
|
188
|
+
? planView.rows
|
|
189
|
+
: phase === 'apply'
|
|
190
|
+
? lines.map((line, index) => ({ key: `out:${index}`, text: line, tone: 'dim' as const }))
|
|
191
|
+
: [
|
|
192
|
+
...(exit === 0 ? [{ key: 'ok', text: 'update installed — restart dsh to load the new version (/quit or ctrl+c)', tone: 'ok' as const }] : []),
|
|
193
|
+
...(exit !== undefined && exit !== 0 ? [{ key: 'fail', text: `update failed (exit ${exit})`, tone: 'error' as const }] : []),
|
|
194
|
+
...(applyError !== undefined ? [{ key: 'fail:start', text: singleLineText(applyError), tone: 'error' as const }] : []),
|
|
195
|
+
...lines.map((line, index) => ({ key: `out:${index}`, text: line, tone: 'dim' as const })),
|
|
196
|
+
]
|
|
197
|
+
const budget = Math.max(1, viewport.bodyRows)
|
|
198
|
+
const tailOffset = clampScroll(Math.max(0, rows.length - budget), rows.length, budget)
|
|
199
|
+
const offset = anchor === 'tail' ? tailOffset : clampScroll(anchor, rows.length, budget)
|
|
200
|
+
useInput((input, key) => {
|
|
201
|
+
if (phase === 'apply') {
|
|
202
|
+
// Scroll-only while the installer runs: escape stays locked.
|
|
203
|
+
if (key.upArrow) setAnchor(offset <= 0 ? 0 : offset - 1)
|
|
204
|
+
if (key.downArrow && offset >= tailOffset) setAnchor('tail')
|
|
205
|
+
else if (key.downArrow) setAnchor(offset + 1)
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
if (key.escape || input === 'q') return close()
|
|
209
|
+
if (input === 'r') {
|
|
210
|
+
setEpoch(value => value + 1)
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
if (key.return || input === 'y') {
|
|
214
|
+
void start()
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
if (key.upArrow) setAnchor(offset <= 0 ? 0 : offset - 1)
|
|
218
|
+
if (key.downArrow && offset >= tailOffset) setAnchor('tail')
|
|
219
|
+
else if (key.downArrow) setAnchor(offset + 1)
|
|
220
|
+
})
|
|
221
|
+
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
222
|
+
const summary = phase === 'probe' ? t('panel.update.checking')
|
|
223
|
+
: phase === 'error' ? t('panel.update.probeFailed')
|
|
224
|
+
: phase === 'apply' ? singleLineText(lines[lines.length - 1] ?? t('panel.update.updating'))
|
|
225
|
+
: phase === 'done' ? (exit === 0 ? t('panel.update.installed') : t('panel.update.failed'))
|
|
226
|
+
: status?.aheadOfRegistry === true ? t('panel.update.aheadOfNpm') : status?.upToDate === true ? t('panel.update.upToDate') : planView?.runnable === true ? t('panel.update.enter') : t('panel.update.blocked')
|
|
227
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(singleLineText(t('panel.update.compact', { summary })), viewport.contentColumns))
|
|
228
|
+
}
|
|
229
|
+
const visible = rows.slice(offset, offset + budget)
|
|
230
|
+
const toneColor = (tone: UpdateRow['tone']): ReturnType<typeof inkColor> | undefined => tone === 'ok'
|
|
231
|
+
? inkColor(getPalette().success)
|
|
232
|
+
: tone === 'error'
|
|
233
|
+
? inkColor(getPalette().error)
|
|
234
|
+
: tone === 'warn'
|
|
235
|
+
? inkColor(getPalette().warn)
|
|
236
|
+
: tone === 'dim'
|
|
237
|
+
? inkColor(getPalette().dim)
|
|
238
|
+
: undefined
|
|
239
|
+
const title = status === undefined
|
|
240
|
+
? t('panel.update.title')
|
|
241
|
+
: status.aheadOfRegistry === true
|
|
242
|
+
? `/update · dsh-code ${status.code.running}`
|
|
243
|
+
: `/update · dsh-code ${status.code.running}${status.code.latest !== null && status.code.latest !== status.code.running ? ` → ${status.code.latest}` : ''}`
|
|
244
|
+
const accent = panelAccent('update', getPalette().dim, getPalette().brandBright)
|
|
245
|
+
return createElement(
|
|
246
|
+
Box,
|
|
247
|
+
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(accent.border), flexDirection: 'column', paddingX: 1 },
|
|
248
|
+
createElement(Text, { color: inkColor(accent.title), wrap: 'truncate-end' }, truncateColumns(singleLineText(title), viewport.contentColumns)),
|
|
249
|
+
...visible.map(row => createElement(Text, {
|
|
250
|
+
key: row.key,
|
|
251
|
+
color: toneColor(row.tone),
|
|
252
|
+
wrap: 'truncate-end',
|
|
253
|
+
}, truncateColumns(` ${singleLineText(row.text)}`, viewport.contentColumns))),
|
|
254
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(singleLineText(updateFooter(phase, planView?.runnable ?? false, status?.upToDate === true, status?.aheadOfRegistry === true)), viewport.contentColumns)),
|
|
255
|
+
)
|
|
256
|
+
}
|
package/src/update.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child-process adapter for the launcher update pipeline. The launcher
|
|
3
|
+
* (bin/deepseek.mjs) stays the single owner of update semantics — plan,
|
|
4
|
+
* guards, and the aligned install sequence — so the TUI only spawns
|
|
5
|
+
* `update --json` (read-only probe) and `update --apply` (streamed run)
|
|
6
|
+
* and never re-implements version-line decisions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn, type ChildProcess } from 'node:child_process'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
|
|
12
|
+
/** Spawn double acceptable to the adapter (tests inject an EventEmitter). */
|
|
13
|
+
export type SpawnLike = (command: string, args: readonly string[], options: { readonly stdio: readonly string[]; readonly windowsHide: boolean }) => ChildProcess
|
|
14
|
+
|
|
15
|
+
/** Structured `update --json` payload; mirrors the launcher buildUpdateStatus. */
|
|
16
|
+
export interface LauncherUpdateStatus {
|
|
17
|
+
readonly code: { readonly running: string; readonly latest: string | null }
|
|
18
|
+
readonly host: { readonly installed: string | null; readonly targetLine: string | null }
|
|
19
|
+
readonly profile: { readonly spec: string | null; readonly mounted: string | null; readonly localCheckout: boolean }
|
|
20
|
+
readonly plan: { readonly dshSpec: string; readonly codeSpec: string; readonly pluginSpecs: readonly string[] }
|
|
21
|
+
readonly blockers: { readonly registry: string | null; readonly downgrade: boolean; readonly localCheckout: readonly string[] | null }
|
|
22
|
+
readonly upToDate: boolean
|
|
23
|
+
/** True when this install is newer than npm latest (apply would downgrade). */
|
|
24
|
+
readonly aheadOfRegistry?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Exact specs the TUI confirmed; apply must install these, not re-query latest. */
|
|
28
|
+
export interface LauncherUpdatePlan {
|
|
29
|
+
readonly dshSpec: string
|
|
30
|
+
readonly codeSpec: string
|
|
31
|
+
readonly pluginSpecs: readonly string[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The launcher entrypoint that ships beside this bundle (lib/../bin). */
|
|
35
|
+
export function launcherUpdateCommand(args: readonly string[], moduleUrl: string = import.meta.url): { readonly command: string; readonly args: string[] } {
|
|
36
|
+
return { command: process.execPath, args: [fileURLToPath(new URL('../bin/deepseek.mjs', moduleUrl)), ...args] }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Split a streamed chunk sequence into complete display lines: CR is
|
|
41
|
+
* stripped, a chunk boundary may split a line, and the trailing partial
|
|
42
|
+
* stays pending until its newline arrives (npm writes whole lines, but a
|
|
43
|
+
* pipe may cut anywhere). Blank lines carry no progress information and
|
|
44
|
+
* are dropped so the panel budget is not spent on gaps.
|
|
45
|
+
*/
|
|
46
|
+
export function createLineSplitter(onLine: (line: string) => void): (chunk: string) => void {
|
|
47
|
+
let pending = ''
|
|
48
|
+
return chunk => {
|
|
49
|
+
pending += chunk
|
|
50
|
+
for (let index = pending.indexOf('\n'); index >= 0; index = pending.indexOf('\n')) {
|
|
51
|
+
const line = pending.slice(0, index).replace(/\r$/u, '')
|
|
52
|
+
pending = pending.slice(index + 1)
|
|
53
|
+
if (line !== '') onLine(line)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Probe the aligned update status. Read-only: `update --json` never
|
|
60
|
+
* installs anything. The probe is bounded (npm view may hang on a broken
|
|
61
|
+
* network) and resolves with the parsed status.
|
|
62
|
+
*/
|
|
63
|
+
export async function probeLauncherUpdate(spawnProcess: SpawnLike = spawn as SpawnLike): Promise<LauncherUpdateStatus> {
|
|
64
|
+
const command = launcherUpdateCommand(['update', '--json'])
|
|
65
|
+
return await new Promise((resolve, reject) => {
|
|
66
|
+
const child = spawnProcess(command.command, command.args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
|
|
67
|
+
const timer = setTimeout(() => {
|
|
68
|
+
child.kill()
|
|
69
|
+
reject(new Error('update probe timed out'))
|
|
70
|
+
}, 30_000)
|
|
71
|
+
let stdout = ''
|
|
72
|
+
let stderr = ''
|
|
73
|
+
child.stdout?.on('data', (chunk: Buffer) => { stdout += chunk.toString() })
|
|
74
|
+
child.stderr?.on('data', (chunk: Buffer) => { stderr += chunk.toString() })
|
|
75
|
+
child.once('error', error => {
|
|
76
|
+
clearTimeout(timer)
|
|
77
|
+
reject(new Error(`update probe failed to start: ${error.message}`))
|
|
78
|
+
})
|
|
79
|
+
child.once('exit', code => {
|
|
80
|
+
clearTimeout(timer)
|
|
81
|
+
if (code === 0) {
|
|
82
|
+
try {
|
|
83
|
+
resolve(JSON.parse(stdout) as LauncherUpdateStatus)
|
|
84
|
+
} catch {
|
|
85
|
+
reject(new Error('update probe returned an unreadable status'))
|
|
86
|
+
}
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
const tail = stderr.trim().split(/\r?\n/u).pop() ?? ''
|
|
90
|
+
reject(new Error(`update probe failed${tail === '' ? '' : `: ${tail}`}`))
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Run the aligned update (`update --apply`) as a child process and stream
|
|
97
|
+
* its sanitized progress lines to the caller. Resolves with the child
|
|
98
|
+
* exit code (0 success); rejects only when the process could not start.
|
|
99
|
+
* No timeout: an npm install may legitimately take minutes.
|
|
100
|
+
*/
|
|
101
|
+
export function applyPlanArgs(plan: LauncherUpdatePlan): string[] {
|
|
102
|
+
const args = ['update', '--apply', '--dsh', plan.dshSpec, '--code', plan.codeSpec]
|
|
103
|
+
for (const plugin of plan.pluginSpecs) args.push('--plugin', plugin)
|
|
104
|
+
return args
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function applyLauncherUpdate(onLine: (line: string) => void, spawnProcess?: SpawnLike, plan?: LauncherUpdatePlan): Promise<number> {
|
|
108
|
+
const spawnFn = spawnProcess ?? (spawn as SpawnLike)
|
|
109
|
+
const command = launcherUpdateCommand(plan === undefined ? ['update', '--apply'] : applyPlanArgs(plan))
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
const child = spawnFn(command.command, command.args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
|
|
112
|
+
const split = createLineSplitter(onLine)
|
|
113
|
+
child.stdout?.on('data', (chunk: Buffer) => { split(chunk.toString()) })
|
|
114
|
+
child.stderr?.on('data', (chunk: Buffer) => { split(chunk.toString()) })
|
|
115
|
+
child.once('error', error => {
|
|
116
|
+
reject(new Error(`update failed to start: ${error.message}`))
|
|
117
|
+
})
|
|
118
|
+
// 'close' fires once the stdio streams have ended, so a final line the
|
|
119
|
+
// child left without its newline still reaches the panel; a bare '\n'
|
|
120
|
+
// flushes any pending partial without adding a blank row.
|
|
121
|
+
child.once('close', (code, signal) => {
|
|
122
|
+
split('\n')
|
|
123
|
+
resolve(code ?? (signal === null ? 0 : 1))
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Installed dsh-code version exposed by the terminal header. */
|
|
2
2
|
|
|
3
|
-
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { readFileSync, realpathSync } from 'node:fs'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
4
5
|
import { dirname, join, resolve } from 'node:path'
|
|
5
6
|
|
|
6
7
|
/** Read one package manifest version without making terminal startup depend on it. */
|
|
@@ -22,27 +23,28 @@ const DSH_HOST_PACKAGE_NAME = '@deepseek-ai/dsh'
|
|
|
22
23
|
/** Parent levels above the host entry allowed to hold its package manifest. */
|
|
23
24
|
const DSH_HOST_WALK_LIMIT = 4
|
|
24
25
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
/** Read `@deepseek-ai/dsh`'s version from a directory's package.json, or undefined. */
|
|
27
|
+
function readHostVersion(directory: string): string | undefined {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8')) as {
|
|
30
|
+
name?: unknown
|
|
31
|
+
version?: unknown
|
|
32
|
+
}
|
|
33
|
+
if (parsed.name === DSH_HOST_PACKAGE_NAME && typeof parsed.version === 'string' && parsed.version.length > 0) {
|
|
34
|
+
return parsed.version
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// No readable host manifest at this level.
|
|
38
|
+
}
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Climb from an entry file toward a host package.json. */
|
|
43
|
+
function walkFrom(entry: string): string | undefined {
|
|
33
44
|
let directory = dirname(resolve(entry))
|
|
34
45
|
for (let depth = 0; depth < DSH_HOST_WALK_LIMIT; depth += 1) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
name?: unknown
|
|
38
|
-
version?: unknown
|
|
39
|
-
}
|
|
40
|
-
if (parsed.name === DSH_HOST_PACKAGE_NAME && typeof parsed.version === 'string' && parsed.version.length > 0) {
|
|
41
|
-
return parsed.version
|
|
42
|
-
}
|
|
43
|
-
} catch {
|
|
44
|
-
// No readable manifest at this level: keep climbing.
|
|
45
|
-
}
|
|
46
|
+
const version = readHostVersion(directory)
|
|
47
|
+
if (version !== undefined) return version
|
|
46
48
|
const parent = dirname(directory)
|
|
47
49
|
if (parent === directory) return undefined
|
|
48
50
|
directory = parent
|
|
@@ -50,6 +52,42 @@ export function resolveDshHostVersion(entry: string | undefined = process.argv[1
|
|
|
50
52
|
return undefined
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the host package via Node's module lookup from an entry file.
|
|
57
|
+
* Covers global `bin/dsh` shims that are not inside the package tree.
|
|
58
|
+
*/
|
|
59
|
+
function resolveFromNode(from: string): string | undefined {
|
|
60
|
+
try {
|
|
61
|
+
const manifest = createRequire(from).resolve(`${DSH_HOST_PACKAGE_NAME}/package.json`)
|
|
62
|
+
return readHostVersion(dirname(manifest))
|
|
63
|
+
} catch {
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the running dsh CLI host's version from its entry file
|
|
70
|
+
* (`process.argv[1]`, e.g. `.../@deepseek-ai/dsh/lib/bin.js` or a PATH
|
|
71
|
+
* symlink `.../bin/dsh` → that file). Only a manifest literally named
|
|
72
|
+
* `@deepseek-ai/dsh` counts, so an unrelated entry (vitest, a plain node
|
|
73
|
+
* script) resolves to undefined instead of faking a kernel version.
|
|
74
|
+
*/
|
|
75
|
+
export function resolveDshHostVersion(entry: string | undefined = process.argv[1]): string | undefined {
|
|
76
|
+
if (entry === undefined || entry === '') return undefined
|
|
77
|
+
const fromWalk = walkFrom(entry)
|
|
78
|
+
if (fromWalk !== undefined) return fromWalk
|
|
79
|
+
try {
|
|
80
|
+
const real = realpathSync(entry)
|
|
81
|
+
if (real !== resolve(entry)) {
|
|
82
|
+
const fromReal = walkFrom(real)
|
|
83
|
+
if (fromReal !== undefined) return fromReal
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// Broken or missing symlink: fall through to Node resolution.
|
|
87
|
+
}
|
|
88
|
+
return resolveFromNode(resolve(entry))
|
|
89
|
+
}
|
|
90
|
+
|
|
53
91
|
let cachedDshKernelVersion: string | undefined
|
|
54
92
|
let dshKernelVersionResolved = false
|
|
55
93
|
|
package/src/whale-glyph.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
// GENERATED by scripts/gen-whale-glyph.ts — do not edit by hand. Rerun the
|
|
2
|
-
// generator after changing the FishLogo path. Half-block rendering of the
|
|
3
|
-
// DeepSeek fish logo (figma I39:24057;88:8943 fillGeometry, exact extract;
|
|
4
|
-
// native 23.16x17.04 → 26 columns × 8 half-block rows).
|
|
5
|
-
// Blank cells are part of the glyph's fixed 26-column grid: pad, never trim.
|
|
6
|
-
|
|
7
|
-
/** Half-block whale glyph rows; render with the brand color. */
|
|
8
|
-
export const WHALE_GLYPH: readonly string[] = [
|
|
9
|
-
' ▄▄▄▄▄▄▄▄█ ▄█▄ ▄',
|
|
10
|
-
' ▄▄██████████▄▄ ▀███▄████',
|
|
11
|
-
'▄███████████████▄ ███▀▀▀ ',
|
|
12
|
-
'██ ▀▀█████▄▀██████ ',
|
|
13
|
-
'██▄ ▀████▄▄████ ',
|
|
14
|
-
' ██▄ ▀██████▀ ',
|
|
15
|
-
' ▀██▄▄ ██▄ ▀███▄▄ ',
|
|
16
|
-
' ▀▀███████▀▀ ▀▀▀ ',
|
|
17
|
-
]
|
|
18
|
-
|
|
19
|
-
/** Fixed glyph width in terminal columns. */
|
|
20
|
-
export const WHALE_GLYPH_COLUMNS = 26
|
|
21
|
-
|
|
22
|
-
/** Fixed glyph height in half-block rows. */
|
|
23
|
-
export const WHALE_GLYPH_ROWS = 8
|
|
1
|
+
// GENERATED by scripts/gen-whale-glyph.ts — do not edit by hand. Rerun the
|
|
2
|
+
// generator after changing the FishLogo path. Half-block rendering of the
|
|
3
|
+
// DeepSeek fish logo (figma I39:24057;88:8943 fillGeometry, exact extract;
|
|
4
|
+
// native 23.16x17.04 → 26 columns × 8 half-block rows).
|
|
5
|
+
// Blank cells are part of the glyph's fixed 26-column grid: pad, never trim.
|
|
6
|
+
|
|
7
|
+
/** Half-block whale glyph rows; render with the brand color. */
|
|
8
|
+
export const WHALE_GLYPH: readonly string[] = [
|
|
9
|
+
' ▄▄▄▄▄▄▄▄█ ▄█▄ ▄',
|
|
10
|
+
' ▄▄██████████▄▄ ▀███▄████',
|
|
11
|
+
'▄███████████████▄ ███▀▀▀ ',
|
|
12
|
+
'██ ▀▀█████▄▀██████ ',
|
|
13
|
+
'██▄ ▀████▄▄████ ',
|
|
14
|
+
' ██▄ ▀██████▀ ',
|
|
15
|
+
' ▀██▄▄ ██▄ ▀███▄▄ ',
|
|
16
|
+
' ▀▀███████▀▀ ▀▀▀ ',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
/** Fixed glyph width in terminal columns. */
|
|
20
|
+
export const WHALE_GLYPH_COLUMNS = 26
|
|
21
|
+
|
|
22
|
+
/** Fixed glyph height in half-block rows. */
|
|
23
|
+
export const WHALE_GLYPH_ROWS = 8
|