@xp266/dshtui 0.1.2 → 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 +10 -2
- package/package.json +1 -1
- package/src/chat/bridge.ts +4 -4
- 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 = [];
|
|
@@ -10497,12 +10505,12 @@ async function createChatBridge(ctx) {
|
|
|
10497
10505
|
currentEffort,
|
|
10498
10506
|
effortName,
|
|
10499
10507
|
selectEffort,
|
|
10500
|
-
permissionMode: () => permission()?.current(activeAgent.session
|
|
10508
|
+
permissionMode: () => permission()?.current(activeAgent.session) ?? PERMISSION_PRESETS[0],
|
|
10501
10509
|
cyclePermission: () => {
|
|
10502
10510
|
const service = permission();
|
|
10503
10511
|
if (service === void 0) return;
|
|
10504
10512
|
try {
|
|
10505
|
-
const current = service.current(activeAgent.session
|
|
10513
|
+
const current = service.current(activeAgent.session);
|
|
10506
10514
|
const names = service.names.length > 0 ? service.names : PERMISSION_PRESETS;
|
|
10507
10515
|
const next = names[(names.indexOf(current) + 1) % names.length];
|
|
10508
10516
|
service.set(activeAgent.session, next);
|
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>
|
|
@@ -143,7 +143,7 @@ interface PresetSessionLike {
|
|
|
143
143
|
export const PERMISSION_PRESETS = ['workspace-write', 'danger-full-access', 'read-only'] as const
|
|
144
144
|
|
|
145
145
|
interface PermissionPresetsLike {
|
|
146
|
-
current(
|
|
146
|
+
current(session: Session): string
|
|
147
147
|
set(session: Session, name: string): void
|
|
148
148
|
readonly names: readonly string[]
|
|
149
149
|
readonly defaultPreset: string
|
|
@@ -1040,12 +1040,12 @@ export async function createChatBridge(ctx: Context): Promise<ChatBridge> {
|
|
|
1040
1040
|
currentEffort,
|
|
1041
1041
|
effortName,
|
|
1042
1042
|
selectEffort,
|
|
1043
|
-
permissionMode: () => permission()?.current(activeAgent.session
|
|
1043
|
+
permissionMode: () => permission()?.current(activeAgent.session) ?? PERMISSION_PRESETS[0],
|
|
1044
1044
|
cyclePermission: () => {
|
|
1045
1045
|
const service = permission()
|
|
1046
1046
|
if (service === undefined) return
|
|
1047
1047
|
try {
|
|
1048
|
-
const current = service.current(activeAgent.session
|
|
1048
|
+
const current = service.current(activeAgent.session)
|
|
1049
1049
|
const names = service.names.length > 0 ? service.names : PERMISSION_PRESETS
|
|
1050
1050
|
const index = names.indexOf(current)
|
|
1051
1051
|
const next = names[(index + 1) % names.length]
|
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[] = []
|