@xp266/dshtui 0.1.3 → 0.1.4
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/bin/dshtui.js +99 -20
- package/lib/index.mjs +8 -0
- package/package.json +1 -1
- package/src/chat/bridge.ts +1 -1
- package/src/chat/session-list.ts +10 -0
package/bin/dshtui.js
CHANGED
|
@@ -4,54 +4,133 @@
|
|
|
4
4
|
* carries this TUI, equivalent to `dsh --profile <profile> [args...]`.
|
|
5
5
|
*
|
|
6
6
|
* The dsh CLI's subcommands are hardcoded upstream, so the command lives in
|
|
7
|
-
* this package's bin
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* this package's bin. Profile resolution: DSH_TUI_PROFILE overrides; else the
|
|
8
|
+
* `dshtui` profile when it exists; else the one profile that already mounts
|
|
9
|
+
* this package (an existing profile carries the user's session history —
|
|
10
|
+
* bootstrapping a fresh one would hide it); else the `dshtui` profile is
|
|
11
|
+
* bootstrapped. Before an install or upgrade, the profile's
|
|
12
|
+
* minimumReleaseAgeExclude gains this package's name so pnpm's
|
|
13
|
+
* supply-chain age policy never rejects freshly published releases.
|
|
11
14
|
*
|
|
12
15
|
* This file must stay free of lib/ imports so it works from a global
|
|
13
16
|
* install, a profile copy, and a dev checkout alike.
|
|
14
17
|
*/
|
|
15
18
|
import { spawn, spawnSync } from 'node:child_process'
|
|
16
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
19
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
17
20
|
import { homedir } from 'node:os'
|
|
18
21
|
import { dirname, join } from 'node:path'
|
|
19
22
|
import { fileURLToPath } from 'node:url'
|
|
20
23
|
|
|
21
24
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
22
25
|
const manifest = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'))
|
|
23
|
-
const
|
|
26
|
+
const packageName = manifest.name
|
|
27
|
+
const windows = process.platform === 'win32'
|
|
24
28
|
const dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
25
|
-
const
|
|
29
|
+
const profilesDir = join(dshHome, 'profiles')
|
|
26
30
|
|
|
27
31
|
function fail(message) {
|
|
28
32
|
console.error(`dshtui: ${message}`)
|
|
29
33
|
process.exit(1)
|
|
30
34
|
}
|
|
31
35
|
|
|
36
|
+
const quote = arg => /^[A-Za-z0-9_@%+=:,./-]+$/.test(arg) ? arg : `"${arg.replaceAll('"', '\\"')}"`
|
|
32
37
|
// Windows resolves `dsh` through a .cmd shim, which only spawns with a
|
|
33
|
-
// shell; the command line is
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
// shell; the command line is one quoted string there because passing an
|
|
39
|
+
// args array alongside shell:true is deprecated (DEP0190).
|
|
40
|
+
function runDsh(args) {
|
|
41
|
+
return windows
|
|
42
|
+
? spawnSync(['dsh', ...args].map(quote).join(' '), { stdio: 'inherit', shell: true })
|
|
43
|
+
: spawnSync('dsh', args, { stdio: 'inherit' })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readJson(path) {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(readFileSync(path, 'utf8'))
|
|
49
|
+
} catch {
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function profileMountsPackage(profileDir) {
|
|
55
|
+
const profileManifest = readJson(join(profileDir, 'package.json'))
|
|
56
|
+
if (profileManifest === undefined) return false
|
|
57
|
+
const bundles = profileManifest?.dsh?.profile?.bundles ?? []
|
|
58
|
+
if (bundles.includes(packageName)) return true
|
|
59
|
+
return packageName in (profileManifest.dependencies ?? {})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function installedVersion(profileDir) {
|
|
63
|
+
return readJson(join(profileDir, 'node_modules', packageName, 'package.json'))?.version
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function versionLessThan(installed, wanted) {
|
|
67
|
+
const core = version => version.split('-')[0].split('.').map(Number)
|
|
68
|
+
const [a, b] = [core(installed), core(wanted)]
|
|
69
|
+
for (let index = 0; index < 3; index++) {
|
|
70
|
+
if ((a[index] ?? 0) !== (b[index] ?? 0)) return (a[index] ?? 0) < (b[index] ?? 0)
|
|
71
|
+
}
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function ensureReleaseAgeExclude(profileDir) {
|
|
76
|
+
// The exclude matches the bare package name, so every version of this
|
|
77
|
+
// package bypasses the age policy while the rest of the tree stays
|
|
78
|
+
// protected by it.
|
|
79
|
+
const workspaceFile = join(profileDir, 'pnpm-workspace.yaml')
|
|
80
|
+
let content = ''
|
|
81
|
+
try {
|
|
82
|
+
content = readFileSync(workspaceFile, 'utf8')
|
|
83
|
+
} catch {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
if (new RegExp(`- ['"]?${packageName.replace('/', '\\/')}['"]?\\s*$`, 'm').test(content)) return
|
|
87
|
+
const line = ` - '${packageName}'`
|
|
88
|
+
content = /^minimumReleaseAgeExclude:\s*$/m.test(content)
|
|
89
|
+
? content.replace(/^minimumReleaseAgeExclude:\s*$/m, `minimumReleaseAgeExclude:\n${line}`)
|
|
90
|
+
: `${content.trimEnd()}\nminimumReleaseAgeExclude:\n${line}\n`
|
|
91
|
+
writeFileSync(workspaceFile, content)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveProfile() {
|
|
95
|
+
const override = process.env.DSH_TUI_PROFILE
|
|
96
|
+
if (override !== undefined) return override
|
|
97
|
+
if (profileMountsPackage(join(profilesDir, 'dshtui'))) return 'dshtui'
|
|
98
|
+
const candidates = existsSync(profilesDir)
|
|
99
|
+
? readdirSync(profilesDir).filter(name => name !== 'node_modules' && profileMountsPackage(join(profilesDir, name)))
|
|
100
|
+
: []
|
|
101
|
+
if (candidates.length === 1) return candidates[0]
|
|
102
|
+
if (candidates.length > 1) {
|
|
103
|
+
fail(`multiple profiles mount ${packageName} (${candidates.join(', ')}); pick one with DSH_TUI_PROFILE=<name>`)
|
|
104
|
+
}
|
|
105
|
+
return 'dshtui'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const profile = resolveProfile()
|
|
109
|
+
const profileDir = join(profilesDir, profile)
|
|
110
|
+
const dshProbe = runDsh(['--version'])
|
|
111
|
+
if (dshProbe.error !== undefined || dshProbe.status !== 0) {
|
|
38
112
|
fail('the dsh CLI was not found on PATH; install it with: npm install -g @deepseek-ai/dsh')
|
|
39
113
|
}
|
|
40
114
|
|
|
41
115
|
if (!existsSync(profileDir)) {
|
|
42
|
-
console.error(`dshtui: profile "${profile}" not found under ${
|
|
43
|
-
const bootstrap =
|
|
44
|
-
'dsh',
|
|
45
|
-
['plugin', '--profile', profile, 'add', `${manifest.name}@${manifest.version}`],
|
|
46
|
-
{ stdio: 'inherit', shell: process.platform === 'win32' },
|
|
47
|
-
)
|
|
116
|
+
console.error(`dshtui: profile "${profile}" not found under ${profilesDir}; installing ${packageName}@${manifest.version} into it`)
|
|
117
|
+
const bootstrap = runDsh(['plugin', '--profile', profile, 'add', `${packageName}@${manifest.version}`])
|
|
48
118
|
if (bootstrap.status !== 0) {
|
|
49
|
-
fail(`bootstrapping the profile failed;
|
|
119
|
+
fail(`bootstrapping the profile failed; run manually: dsh plugin --profile ${profile} add ${packageName}`)
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
const installed = installedVersion(profileDir)
|
|
123
|
+
if (installed !== undefined && versionLessThan(installed, manifest.version)) {
|
|
124
|
+
ensureReleaseAgeExclude(profileDir)
|
|
125
|
+
console.error(`dshtui: upgrading profile "${profile}": ${packageName} ${installed} -> ${manifest.version}`)
|
|
126
|
+
const upgrade = runDsh(['plugin', '--profile', profile, 'add', `${packageName}@${manifest.version}`])
|
|
127
|
+
if (upgrade.status !== 0) {
|
|
128
|
+
fail(`the upgrade failed; run manually: dsh plugin --profile ${profile} add ${packageName}@${manifest.version}`)
|
|
129
|
+
}
|
|
50
130
|
}
|
|
51
131
|
}
|
|
52
132
|
|
|
53
133
|
const argv = process.argv.slice(2)
|
|
54
|
-
const quote = arg => /^[A-Za-z0-9_@%+=:,./-]+$/.test(arg) ? arg : `"${arg.replaceAll('"', '\\"')}"`
|
|
55
134
|
const command = ['dsh', '--profile', profile, ...argv].map(quote).join(' ')
|
|
56
135
|
const child = spawn(windows ? command : 'dsh', windows ? [] : ['--profile', profile, ...argv], {
|
|
57
136
|
stdio: 'inherit',
|
package/lib/index.mjs
CHANGED
|
@@ -7193,6 +7193,14 @@ function sessionTime(session) {
|
|
|
7193
7193
|
* (one log read, then persisted), never a repeated full read.
|
|
7194
7194
|
*/
|
|
7195
7195
|
async function computeSessionList(ctx) {
|
|
7196
|
+
try {
|
|
7197
|
+
return await computeSessionListInner(ctx);
|
|
7198
|
+
} catch (error$2) {
|
|
7199
|
+
error("boot", `session list failed: ${error$2 instanceof Error ? error$2.message : String(error$2)}`);
|
|
7200
|
+
throw error$2;
|
|
7201
|
+
}
|
|
7202
|
+
}
|
|
7203
|
+
async function computeSessionListInner(ctx) {
|
|
7196
7204
|
const workspacePaths = collectWorkspacePaths(ctx);
|
|
7197
7205
|
const archived = collectArchivedIds(ctx);
|
|
7198
7206
|
const summaries = [];
|
package/package.json
CHANGED
package/src/chat/bridge.ts
CHANGED
|
@@ -46,7 +46,7 @@ import { applyTheme } from '../apply-theme.ts'
|
|
|
46
46
|
import { THEME_SETTINGS_NAMESPACE } from '../theme-settings.ts'
|
|
47
47
|
import { themeMode } from '../theme.ts'
|
|
48
48
|
import type { ThemeMode } from '../theme.ts'
|
|
49
|
-
import { warn } from '../log.ts'
|
|
49
|
+
import { error as logError, warn } from '../log.ts'
|
|
50
50
|
|
|
51
51
|
interface AttachmentsServiceLike {
|
|
52
52
|
saveImage(input: { data: Uint8Array; mediaType: string; name?: string }): Promise<ImageAttachmentRef>
|
package/src/chat/session-list.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { stat } from 'node:fs/promises'
|
|
|
2
2
|
import type { Context } from '@deepseek-ai/cordis'
|
|
3
3
|
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
4
4
|
import { textFromBlocks } from './blocks.ts'
|
|
5
|
+
import { error as logError } from '../log.ts'
|
|
5
6
|
import { isBlankSession } from './presets.ts'
|
|
6
7
|
|
|
7
8
|
export interface SessionSummary {
|
|
@@ -27,6 +28,15 @@ export function sessionTime(session: SessionSummary): number {
|
|
|
27
28
|
* (one log read, then persisted), never a repeated full read.
|
|
28
29
|
*/
|
|
29
30
|
export async function computeSessionList(ctx: Context): Promise<SessionSummary[]> {
|
|
31
|
+
try {
|
|
32
|
+
return await computeSessionListInner(ctx)
|
|
33
|
+
} catch (error) {
|
|
34
|
+
logError('boot', `session list failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
35
|
+
throw error
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function computeSessionListInner(ctx: Context): Promise<SessionSummary[]> {
|
|
30
40
|
const workspacePaths = collectWorkspacePaths(ctx)
|
|
31
41
|
const archived = collectArchivedIds(ctx)
|
|
32
42
|
const summaries: SessionSummary[] = []
|