dsh-code 1.0.7 → 1.3.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 +70 -24
- package/README.md +71 -25
- package/bin/deepseek.mjs +202 -39
- package/cordis.patch.yml +13 -4
- package/lib/index.mjs +5002 -1061
- package/lib/session-query.mjs +3 -2
- package/lib/startup.mjs +4 -4
- package/lib/{theme-DCT8Y2xf.mjs → theme-B3orFUYz.mjs} +665 -20
- package/lib/types/app.d.ts +120 -63
- package/lib/types/attachments.d.ts +16 -7
- package/lib/types/authorization-panel.d.ts +3 -3
- package/lib/types/git-workflow.d.ts +91 -2
- package/lib/types/history.d.ts +10 -0
- package/lib/types/i18n.d.ts +39 -0
- package/lib/types/index.d.ts +74 -2
- package/lib/types/input-split.d.ts +1 -1
- package/lib/types/kernel-panels.d.ts +89 -32
- package/lib/types/language-panel.d.ts +12 -0
- package/lib/types/locales/en.d.ts +498 -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/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 +95 -4
- package/lib/types/render/status.d.ts +12 -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 +42 -1
- package/lib/types/session-switch.d.ts +8 -0
- package/lib/types/startup.d.ts +1 -1
- package/lib/types/terminal-title.d.ts +8 -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 +27 -6
- package/lib/types/update.d.ts +10 -1
- package/lib/types/version.d.ts +6 -3
- package/package.json +26 -7
- package/src/app.ts +1426 -627
- package/src/approval.ts +166 -166
- package/src/attachments.ts +65 -19
- package/src/authorization-panel.ts +24 -18
- package/src/editor-keys.ts +371 -371
- package/src/fork.ts +11 -7
- package/src/git-workflow.ts +229 -3
- package/src/history.ts +14 -0
- package/src/i18n.ts +68 -0
- package/src/index.ts +503 -128
- package/src/input-split.ts +27 -7
- package/src/kernel-panels.ts +528 -113
- package/src/keyboard.ts +5 -4
- package/src/language-panel.ts +53 -0
- package/src/locales/en.ts +538 -0
- package/src/locales/zh.ts +537 -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 +218 -0
- package/src/render/animations.ts +104 -6
- package/src/render/editor.ts +20 -20
- package/src/render/export.ts +116 -95
- 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 +429 -19
- package/src/render/status.ts +119 -62
- package/src/render/text.ts +15 -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 +90 -9
- package/src/session-query.ts +8 -4
- package/src/session-switch.ts +14 -0
- package/src/startup.ts +3 -3
- package/src/store.ts +19 -1
- package/src/subagents.ts +229 -229
- package/src/terminal-title.ts +22 -5
- package/src/theme-panel.ts +17 -21
- package/src/theme.ts +281 -33
- package/src/update-panel.ts +148 -31
- package/src/update.ts +19 -3
- package/src/version.ts +63 -20
- package/src/whale-glyph.ts +23 -23
package/src/update-panel.ts
CHANGED
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
|
|
12
12
|
import { createElement, useEffect, useState, type ReactElement } from 'react'
|
|
13
13
|
import { Box, Text, useInput, useStdout } from 'ink'
|
|
14
|
-
import type { LauncherUpdateStatus } from './update.ts'
|
|
14
|
+
import type { LauncherUpdatePlan, LauncherUpdateStatus } from './update.ts'
|
|
15
15
|
import { clampScroll, panelViewport } from './render/inspector.ts'
|
|
16
16
|
import { singleLineText, truncateColumns } from './render/text.ts'
|
|
17
|
+
import { panelAccent } from './panel-accent.ts'
|
|
17
18
|
import { getPalette, inkColor } from './theme.ts'
|
|
19
|
+
import { t } from './i18n.ts'
|
|
18
20
|
|
|
19
21
|
/** Retained apply-progress lines (ring tail; npm output is ephemeral). */
|
|
20
22
|
export const UPDATE_OUTPUT_CAP = 800
|
|
@@ -24,6 +26,84 @@ export function clipUpdateLines(lines: readonly string[]): readonly string[] {
|
|
|
24
26
|
return lines.length <= UPDATE_OUTPUT_CAP ? lines : lines.slice(lines.length - UPDATE_OUTPUT_CAP)
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
/** One in-flight `update --apply`. Closing the panel must not start a second. */
|
|
30
|
+
interface UpdateApplyJob {
|
|
31
|
+
readonly promise: Promise<number>
|
|
32
|
+
readonly lines: string[]
|
|
33
|
+
readonly lineListeners: Set<(line: string) => void>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let updateApplyJob: UpdateApplyJob | undefined
|
|
37
|
+
const updateApplyRunningListeners = new Set<(running: boolean) => void>()
|
|
38
|
+
|
|
39
|
+
function emitUpdateApplyRunning(running: boolean): void {
|
|
40
|
+
for (const listener of updateApplyRunningListeners) listener(running)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** True while an apply child is alive, even if the /update panel is closed. */
|
|
44
|
+
export function isUpdateApplyRunning(): boolean {
|
|
45
|
+
return updateApplyJob !== undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Subscribe to apply-running changes (frozen-hint "esc waits"). */
|
|
49
|
+
export function subscribeUpdateApplyRunning(listener: (running: boolean) => void): () => void {
|
|
50
|
+
updateApplyRunningListeners.add(listener)
|
|
51
|
+
listener(updateApplyJob !== undefined)
|
|
52
|
+
return () => { updateApplyRunningListeners.delete(listener) }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The live apply job, if any: lines already streamed plus the shared promise. */
|
|
56
|
+
export function currentUpdateApply(): UpdateApplyJob | undefined {
|
|
57
|
+
return updateApplyJob
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Run `apply` once. A second call while the child is alive reuses the same
|
|
62
|
+
* promise and replays buffered lines — closing /update and opening it again
|
|
63
|
+
* must not spawn a second `npm install`.
|
|
64
|
+
*/
|
|
65
|
+
export function runUpdateApply(
|
|
66
|
+
apply: (onLine: (line: string) => void, plan: LauncherUpdatePlan) => Promise<number>,
|
|
67
|
+
plan: LauncherUpdatePlan,
|
|
68
|
+
onLine?: (line: string) => void,
|
|
69
|
+
): Promise<number> {
|
|
70
|
+
if (updateApplyJob !== undefined) {
|
|
71
|
+
if (onLine !== undefined) {
|
|
72
|
+
for (const line of updateApplyJob.lines) onLine(line)
|
|
73
|
+
updateApplyJob.lineListeners.add(onLine)
|
|
74
|
+
}
|
|
75
|
+
return updateApplyJob.promise
|
|
76
|
+
}
|
|
77
|
+
const lines: string[] = []
|
|
78
|
+
const lineListeners = new Set<(line: string) => void>()
|
|
79
|
+
if (onLine !== undefined) lineListeners.add(onLine)
|
|
80
|
+
const promise = apply(line => {
|
|
81
|
+
const text = singleLineText(line)
|
|
82
|
+
lines.push(text)
|
|
83
|
+
if (lines.length > UPDATE_OUTPUT_CAP) lines.splice(0, lines.length - UPDATE_OUTPUT_CAP)
|
|
84
|
+
for (const listener of lineListeners) listener(text)
|
|
85
|
+
}, plan)
|
|
86
|
+
updateApplyJob = { promise, lines, lineListeners }
|
|
87
|
+
emitUpdateApplyRunning(true)
|
|
88
|
+
const settle = (): void => {
|
|
89
|
+
if (updateApplyJob?.promise === promise) {
|
|
90
|
+
updateApplyJob = undefined
|
|
91
|
+
emitUpdateApplyRunning(false)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Handle both outcomes directly. `finally()` mirrors a rejected source
|
|
95
|
+
// promise onto its returned promise; discarding that returned promise would
|
|
96
|
+
// create an unhandled rejection when the update child cannot start.
|
|
97
|
+
void promise.then(settle, settle)
|
|
98
|
+
return promise
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Drop a leftover job between tests. */
|
|
102
|
+
export function resetUpdateApply(): void {
|
|
103
|
+
updateApplyJob = undefined
|
|
104
|
+
emitUpdateApplyRunning(false)
|
|
105
|
+
}
|
|
106
|
+
|
|
27
107
|
/** One display row of the update surface. */
|
|
28
108
|
export interface UpdateRow {
|
|
29
109
|
readonly key: string
|
|
@@ -41,9 +121,11 @@ export interface UpdatePlanView {
|
|
|
41
121
|
export function updatePlanView(status: LauncherUpdateStatus): UpdatePlanView {
|
|
42
122
|
const rows: UpdateRow[] = []
|
|
43
123
|
const latest = status.code.latest ?? 'unknown'
|
|
44
|
-
rows.push(status.
|
|
45
|
-
? { key: 'code', text: `dsh-code ${status.code.running} (latest)` }
|
|
46
|
-
:
|
|
124
|
+
rows.push(status.aheadOfRegistry === true
|
|
125
|
+
? { key: 'code', text: `dsh-code ${status.code.running} (newer than npm ${latest})` }
|
|
126
|
+
: status.code.latest !== null && status.code.latest === status.code.running
|
|
127
|
+
? { key: 'code', text: `dsh-code ${status.code.running} (latest)` }
|
|
128
|
+
: { key: 'code', text: `dsh-code ${status.code.running} → ${latest}` })
|
|
47
129
|
if (status.host.targetLine === null) {
|
|
48
130
|
rows.push({ key: 'host', text: 'harness pinned line unreadable — update would install @deepseek-ai/dsh@latest', tone: 'warn' })
|
|
49
131
|
} else if (status.host.installed === null) {
|
|
@@ -83,7 +165,9 @@ export function updatePlanView(status: LauncherUpdateStatus): UpdatePlanView {
|
|
|
83
165
|
rows.push({ key: `blocker:checkout:${index}`, text: line, tone: 'error' })
|
|
84
166
|
}
|
|
85
167
|
}
|
|
86
|
-
if (status.
|
|
168
|
+
if (status.aheadOfRegistry === true) {
|
|
169
|
+
rows.push({ key: 'ahead', text: 'this install is newer than npm latest — refusing to downgrade', tone: 'warn' })
|
|
170
|
+
} else if (status.upToDate) {
|
|
87
171
|
rows.push({ key: 'uptodate', text: 'everything is already on the pinned line — nothing to update', tone: 'ok' })
|
|
88
172
|
}
|
|
89
173
|
const runnable = status.upToDate !== true
|
|
@@ -97,13 +181,14 @@ export function updatePlanView(status: LauncherUpdateStatus): UpdatePlanView {
|
|
|
97
181
|
export type UpdatePhase = 'probe' | 'error' | 'plan' | 'apply' | 'done'
|
|
98
182
|
|
|
99
183
|
/** Footer hint line per phase; the plan phase names the confirm key only when runnable. */
|
|
100
|
-
export function updateFooter(phase: UpdatePhase, runnable: boolean, upToDate: boolean): string {
|
|
101
|
-
if (phase === 'probe') return '
|
|
102
|
-
if (phase === 'error') return '
|
|
103
|
-
if (phase === 'apply') return '
|
|
104
|
-
if (phase === 'done') return '
|
|
105
|
-
if (
|
|
106
|
-
|
|
184
|
+
export function updateFooter(phase: UpdatePhase, runnable: boolean, upToDate: boolean, aheadOfRegistry = false): string {
|
|
185
|
+
if (phase === 'probe') return t('panel.update.footer.probe')
|
|
186
|
+
if (phase === 'error') return t('panel.update.footer.error')
|
|
187
|
+
if (phase === 'apply') return t('panel.update.footer.apply')
|
|
188
|
+
if (phase === 'done') return t('panel.update.footer.error')
|
|
189
|
+
if (aheadOfRegistry) return t('panel.update.footer.ahead')
|
|
190
|
+
if (upToDate) return t('panel.update.footer.current')
|
|
191
|
+
return runnable ? t('panel.update.footer.update') : t('panel.update.footer.blocked')
|
|
107
192
|
}
|
|
108
193
|
|
|
109
194
|
/**
|
|
@@ -114,13 +199,13 @@ export function updateFooter(phase: UpdatePhase, runnable: boolean, upToDate: bo
|
|
|
114
199
|
*/
|
|
115
200
|
export function UpdatePanel({ probe, apply, close, notify }: {
|
|
116
201
|
/** Read-only probe of the launcher update status (never installs). */
|
|
117
|
-
probe()
|
|
202
|
+
probe: () => Promise<LauncherUpdateStatus>
|
|
118
203
|
/** Run the aligned update; streams sanitized progress lines. */
|
|
119
|
-
apply(onLine: (line: string) => void
|
|
204
|
+
apply: (onLine: (line: string) => void, plan: LauncherUpdateStatus['plan']) => Promise<number>
|
|
120
205
|
/** Close the panel (App keeps ownership of the flag). */
|
|
121
|
-
close()
|
|
206
|
+
close: () => void
|
|
122
207
|
/** One bounded cross-surface notice (phase completions). */
|
|
123
|
-
notify(text: string, tone?: 'info' | 'warning' | 'error')
|
|
208
|
+
notify: (text: string, tone?: 'info' | 'warning' | 'error') => void
|
|
124
209
|
}): ReactElement {
|
|
125
210
|
const [phase, setPhase] = useState<UpdatePhase>('probe')
|
|
126
211
|
const [status, setStatus] = useState<LauncherUpdateStatus>()
|
|
@@ -132,8 +217,35 @@ export function UpdatePanel({ probe, apply, close, notify }: {
|
|
|
132
217
|
// visible row (up moves away from the tail, down onto it re-follows).
|
|
133
218
|
const [anchor, setAnchor] = useState<'tail' | number>('tail')
|
|
134
219
|
const [epoch, setEpoch] = useState(0)
|
|
220
|
+
const appendLine = (line: string): void => {
|
|
221
|
+
setLines(previous => clipUpdateLines([...previous, line]))
|
|
222
|
+
}
|
|
135
223
|
useEffect(() => {
|
|
136
224
|
let disposed = false
|
|
225
|
+
const existing = currentUpdateApply()
|
|
226
|
+
if (existing !== undefined) {
|
|
227
|
+
// The apply child outlives the panel: closing for an approval or
|
|
228
|
+
// Ctrl+C must reconnect to the same job, not probe-and-confirm again.
|
|
229
|
+
setPhase('apply')
|
|
230
|
+
setLines(existing.lines)
|
|
231
|
+
setExit(undefined)
|
|
232
|
+
setApplyError(undefined)
|
|
233
|
+
setAnchor('tail')
|
|
234
|
+
existing.lineListeners.add(appendLine)
|
|
235
|
+
void existing.promise.then(code => {
|
|
236
|
+
if (disposed) return
|
|
237
|
+
setExit(code)
|
|
238
|
+
setPhase('done')
|
|
239
|
+
}, reason => {
|
|
240
|
+
if (disposed) return
|
|
241
|
+
setApplyError(reason instanceof Error ? reason.message : String(reason))
|
|
242
|
+
setPhase('done')
|
|
243
|
+
})
|
|
244
|
+
return () => {
|
|
245
|
+
disposed = true
|
|
246
|
+
existing.lineListeners.delete(appendLine)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
137
249
|
setPhase('probe')
|
|
138
250
|
setStatus(undefined)
|
|
139
251
|
setProbeError(undefined)
|
|
@@ -147,21 +259,23 @@ export function UpdatePanel({ probe, apply, close, notify }: {
|
|
|
147
259
|
setProbeError(reason instanceof Error ? reason.message : String(reason))
|
|
148
260
|
setPhase('error')
|
|
149
261
|
})
|
|
150
|
-
return () => {
|
|
262
|
+
return () => {
|
|
263
|
+
disposed = true
|
|
264
|
+
currentUpdateApply()?.lineListeners.delete(appendLine)
|
|
265
|
+
}
|
|
151
266
|
}, [epoch, probe])
|
|
152
267
|
const stdout = useStdout().stdout
|
|
153
268
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
154
269
|
const start = (): void => {
|
|
155
270
|
if (phase !== 'plan' || status === undefined) return
|
|
156
271
|
if (!updatePlanView(status).runnable) return
|
|
272
|
+
if (isUpdateApplyRunning()) return
|
|
157
273
|
setPhase('apply')
|
|
158
274
|
setLines([])
|
|
159
275
|
setExit(undefined)
|
|
160
276
|
setApplyError(undefined)
|
|
161
277
|
setAnchor('tail')
|
|
162
|
-
apply(
|
|
163
|
-
setLines(previous => clipUpdateLines([...previous, singleLineText(line)]))
|
|
164
|
-
}).then(code => {
|
|
278
|
+
runUpdateApply(apply, status.plan, appendLine).then(code => {
|
|
165
279
|
setExit(code)
|
|
166
280
|
setPhase('done')
|
|
167
281
|
notify(code === 0 ? 'update installed — restart dsh to activate' : `update failed (exit ${code})`, code === 0 ? 'info' : 'error')
|
|
@@ -212,12 +326,12 @@ export function UpdatePanel({ probe, apply, close, notify }: {
|
|
|
212
326
|
else if (key.downArrow) setAnchor(offset + 1)
|
|
213
327
|
})
|
|
214
328
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
215
|
-
const summary = phase === 'probe' ? 'checking
|
|
216
|
-
: phase === 'error' ? '
|
|
217
|
-
: phase === 'apply' ? singleLineText(lines[lines.length - 1] ?? 'updating
|
|
218
|
-
: phase === 'done' ? (exit === 0 ? 'installed
|
|
219
|
-
: status?.
|
|
220
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(singleLineText(
|
|
329
|
+
const summary = phase === 'probe' ? t('panel.update.checking')
|
|
330
|
+
: phase === 'error' ? t('panel.update.probeFailed')
|
|
331
|
+
: phase === 'apply' ? singleLineText(lines[lines.length - 1] ?? t('panel.update.updating'))
|
|
332
|
+
: phase === 'done' ? (exit === 0 ? t('panel.update.installed') : t('panel.update.failed'))
|
|
333
|
+
: 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')
|
|
334
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(singleLineText(t('panel.update.compact', { summary })), viewport.contentColumns))
|
|
221
335
|
}
|
|
222
336
|
const visible = rows.slice(offset, offset + budget)
|
|
223
337
|
const toneColor = (tone: UpdateRow['tone']): ReturnType<typeof inkColor> | undefined => tone === 'ok'
|
|
@@ -230,17 +344,20 @@ export function UpdatePanel({ probe, apply, close, notify }: {
|
|
|
230
344
|
? inkColor(getPalette().dim)
|
|
231
345
|
: undefined
|
|
232
346
|
const title = status === undefined
|
|
233
|
-
? '
|
|
234
|
-
:
|
|
347
|
+
? t('panel.update.title')
|
|
348
|
+
: status.aheadOfRegistry === true
|
|
349
|
+
? `/update · dsh-code ${status.code.running}`
|
|
350
|
+
: `/update · dsh-code ${status.code.running}${status.code.latest !== null && status.code.latest !== status.code.running ? ` → ${status.code.latest}` : ''}`
|
|
351
|
+
const accent = panelAccent('update', getPalette().dim, getPalette().brandBright)
|
|
235
352
|
return createElement(
|
|
236
353
|
Box,
|
|
237
|
-
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(
|
|
238
|
-
createElement(Text, { color: inkColor(
|
|
354
|
+
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(accent.border), flexDirection: 'column', paddingX: 1 },
|
|
355
|
+
createElement(Text, { color: inkColor(accent.title), wrap: 'truncate-end' }, truncateColumns(singleLineText(title), viewport.contentColumns)),
|
|
239
356
|
...visible.map(row => createElement(Text, {
|
|
240
357
|
key: row.key,
|
|
241
358
|
color: toneColor(row.tone),
|
|
242
359
|
wrap: 'truncate-end',
|
|
243
360
|
}, truncateColumns(` ${singleLineText(row.text)}`, viewport.contentColumns))),
|
|
244
|
-
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(singleLineText(updateFooter(phase, planView?.runnable ?? false, status?.upToDate === true)), viewport.contentColumns)),
|
|
361
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(singleLineText(updateFooter(phase, planView?.runnable ?? false, status?.upToDate === true, status?.aheadOfRegistry === true)), viewport.contentColumns)),
|
|
245
362
|
)
|
|
246
363
|
}
|
package/src/update.ts
CHANGED
|
@@ -20,6 +20,15 @@ export interface LauncherUpdateStatus {
|
|
|
20
20
|
readonly plan: { readonly dshSpec: string; readonly codeSpec: string; readonly pluginSpecs: readonly string[] }
|
|
21
21
|
readonly blockers: { readonly registry: string | null; readonly downgrade: boolean; readonly localCheckout: readonly string[] | null }
|
|
22
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[]
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
/** The launcher entrypoint that ships beside this bundle (lib/../bin). */
|
|
@@ -89,10 +98,17 @@ export async function probeLauncherUpdate(spawnProcess: SpawnLike = spawn as Spa
|
|
|
89
98
|
* exit code (0 success); rejects only when the process could not start.
|
|
90
99
|
* No timeout: an npm install may legitimately take minutes.
|
|
91
100
|
*/
|
|
92
|
-
export function
|
|
93
|
-
const
|
|
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))
|
|
94
110
|
return new Promise((resolve, reject) => {
|
|
95
|
-
const child =
|
|
111
|
+
const child = spawnFn(command.command, command.args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
|
|
96
112
|
const split = createLineSplitter(onLine)
|
|
97
113
|
child.stdout?.on('data', (chunk: Buffer) => { split(chunk.toString()) })
|
|
98
114
|
child.stderr?.on('data', (chunk: Buffer) => { split(chunk.toString()) })
|
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. */
|
|
@@ -16,33 +17,39 @@ function readPackageVersion(manifest = new URL('../package.json', import.meta.ur
|
|
|
16
17
|
/** Version of the installed dsh-code package. */
|
|
17
18
|
export const DSH_CODE_VERSION = readPackageVersion()
|
|
18
19
|
|
|
20
|
+
/** Header brand line: hide a missing-manifest fallback so we never paint v0.0.0. */
|
|
21
|
+
export function headerBrandTitle(version: string = DSH_CODE_VERSION): string {
|
|
22
|
+
return version === '0.0.0' ? 'DeepSeek Harness' : `DeepSeek Harness · v${version}`
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
/** The harness host package: the dsh CLI whose process runs the TUI plugin. */
|
|
20
26
|
const DSH_HOST_PACKAGE_NAME = '@deepseek-ai/dsh'
|
|
21
27
|
|
|
22
28
|
/** Parent levels above the host entry allowed to hold its package manifest. */
|
|
23
29
|
const DSH_HOST_WALK_LIMIT = 4
|
|
24
30
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
/** Read `@deepseek-ai/dsh`'s version from a directory's package.json, or undefined. */
|
|
32
|
+
function readHostVersion(directory: string): string | undefined {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8')) as {
|
|
35
|
+
name?: unknown
|
|
36
|
+
version?: unknown
|
|
37
|
+
}
|
|
38
|
+
if (parsed.name === DSH_HOST_PACKAGE_NAME && typeof parsed.version === 'string' && parsed.version.length > 0) {
|
|
39
|
+
return parsed.version
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// No readable host manifest at this level.
|
|
43
|
+
}
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Climb from an entry file toward a host package.json. */
|
|
48
|
+
function walkFrom(entry: string): string | undefined {
|
|
33
49
|
let directory = dirname(resolve(entry))
|
|
34
50
|
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
|
-
}
|
|
51
|
+
const version = readHostVersion(directory)
|
|
52
|
+
if (version !== undefined) return version
|
|
46
53
|
const parent = dirname(directory)
|
|
47
54
|
if (parent === directory) return undefined
|
|
48
55
|
directory = parent
|
|
@@ -50,6 +57,42 @@ export function resolveDshHostVersion(entry: string | undefined = process.argv[1
|
|
|
50
57
|
return undefined
|
|
51
58
|
}
|
|
52
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the host package via Node's module lookup from an entry file.
|
|
62
|
+
* Covers global `bin/dsh` shims that are not inside the package tree.
|
|
63
|
+
*/
|
|
64
|
+
function resolveFromNode(from: string): string | undefined {
|
|
65
|
+
try {
|
|
66
|
+
const manifest = createRequire(from).resolve(`${DSH_HOST_PACKAGE_NAME}/package.json`)
|
|
67
|
+
return readHostVersion(dirname(manifest))
|
|
68
|
+
} catch {
|
|
69
|
+
return undefined
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the running dsh CLI host's version from its entry file
|
|
75
|
+
* (`process.argv[1]`, e.g. `.../@deepseek-ai/dsh/lib/bin.js` or a PATH
|
|
76
|
+
* symlink `.../bin/dsh` → that file). Only a manifest literally named
|
|
77
|
+
* `@deepseek-ai/dsh` counts, so an unrelated entry (vitest, a plain node
|
|
78
|
+
* script) resolves to undefined instead of faking a kernel version.
|
|
79
|
+
*/
|
|
80
|
+
export function resolveDshHostVersion(entry: string | undefined = process.argv[1]): string | undefined {
|
|
81
|
+
if (entry === undefined || entry === '') return undefined
|
|
82
|
+
const fromWalk = walkFrom(entry)
|
|
83
|
+
if (fromWalk !== undefined) return fromWalk
|
|
84
|
+
try {
|
|
85
|
+
const real = realpathSync(entry)
|
|
86
|
+
if (real !== resolve(entry)) {
|
|
87
|
+
const fromReal = walkFrom(real)
|
|
88
|
+
if (fromReal !== undefined) return fromReal
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Broken or missing symlink: fall through to Node resolution.
|
|
92
|
+
}
|
|
93
|
+
return resolveFromNode(resolve(entry))
|
|
94
|
+
}
|
|
95
|
+
|
|
53
96
|
let cachedDshKernelVersion: string | undefined
|
|
54
97
|
let dshKernelVersionResolved = false
|
|
55
98
|
|
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
|