@xmoon76/dsh-pi-tui 0.3.3 → 0.3.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/README.md +55 -16
- package/README.zh-CN.md +46 -13
- package/dist/builtins.mjs +1 -1
- package/dist/extensions.mjs +1 -1
- package/dist/index.d.mts +15 -2
- package/dist/index.mjs +1 -1
- package/dist/{src-Txng8gIG.mjs → src-B1uP5j0d.mjs} +4456 -915
- package/package.json +4 -3
- package/scripts/client-boundary-baseline.json +82 -0
- package/scripts/client-boundary-gate.mjs +165 -0
- package/scripts/pre-push-stages.mjs +116 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmoon76/dsh-pi-tui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "The dsh-pi-tui bundle: a TUI surface for DeepSeek Harness profiles (dsh --profile pi-tui)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "XMoon <xmoonox@gmail.com>",
|
|
@@ -172,7 +172,8 @@
|
|
|
172
172
|
"smoke:examples": "node scripts/examples-plugin-smoke.mjs",
|
|
173
173
|
"pack:release": "rm -f xmoon76-dsh-pi-tui-*.tgz && pnpm pack --pack-destination .",
|
|
174
174
|
"smoke:pack": "node scripts/tarball-smoke.mjs",
|
|
175
|
-
"verify:prepush": "pnpm typecheck:fork && pnpm test:fork && pnpm test:docs && node scripts/naming-gate.mjs && pnpm audit --prod --audit-level high && pnpm pack:release",
|
|
176
|
-
"verify:prepush:nofork": "pnpm test:docs && node scripts/naming-gate.mjs && pnpm audit --prod --audit-level high && pnpm pack:release"
|
|
175
|
+
"verify:prepush": "pnpm typecheck:fork && pnpm test:fork && pnpm test:docs && node scripts/naming-gate.mjs && pnpm gate:boundary && pnpm audit --prod --audit-level high && pnpm pack:release",
|
|
176
|
+
"verify:prepush:nofork": "pnpm test:docs && node scripts/naming-gate.mjs && pnpm gate:boundary && pnpm audit --prod --audit-level high && pnpm pack:release",
|
|
177
|
+
"gate:boundary": "node scripts/client-boundary-gate.mjs"
|
|
177
178
|
}
|
|
178
179
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"commands.ts": [
|
|
3
|
+
"import:dsh-agent",
|
|
4
|
+
"import:dsh-session"
|
|
5
|
+
],
|
|
6
|
+
"index.ts": [
|
|
7
|
+
"agentDefaultModel",
|
|
8
|
+
"agentPresets",
|
|
9
|
+
"agents",
|
|
10
|
+
"attachments",
|
|
11
|
+
"authorization",
|
|
12
|
+
"commands",
|
|
13
|
+
"credentials",
|
|
14
|
+
"import:dsh-agent",
|
|
15
|
+
"import:dsh-session",
|
|
16
|
+
"jobs",
|
|
17
|
+
"llm",
|
|
18
|
+
"permissionPresets",
|
|
19
|
+
"sessionPersistence",
|
|
20
|
+
"sessions",
|
|
21
|
+
"settings",
|
|
22
|
+
"shell",
|
|
23
|
+
"subagents",
|
|
24
|
+
"tokenMeter",
|
|
25
|
+
"tools"
|
|
26
|
+
],
|
|
27
|
+
"model-menu.ts": [
|
|
28
|
+
"import:dsh-agent"
|
|
29
|
+
],
|
|
30
|
+
"present.ts": [
|
|
31
|
+
"import:dsh-session"
|
|
32
|
+
],
|
|
33
|
+
"preset-events.ts": [
|
|
34
|
+
"import:dsh-session"
|
|
35
|
+
],
|
|
36
|
+
"rewind.ts": [
|
|
37
|
+
"import:dsh-session"
|
|
38
|
+
],
|
|
39
|
+
"runtime/direct/interaction-direct.ts": [
|
|
40
|
+
"approval",
|
|
41
|
+
"userQuestions"
|
|
42
|
+
],
|
|
43
|
+
"runtime/direct/session-direct.ts": [
|
|
44
|
+
"sessionPersistence",
|
|
45
|
+
"sessionQuery"
|
|
46
|
+
],
|
|
47
|
+
"runtime/direct/session-lifecycle-direct.ts": [
|
|
48
|
+
"agents",
|
|
49
|
+
"import:dsh-agent",
|
|
50
|
+
"import:dsh-session"
|
|
51
|
+
],
|
|
52
|
+
"runtime/direct/session-writer-direct.ts": [
|
|
53
|
+
"sessionTitle"
|
|
54
|
+
],
|
|
55
|
+
"runtime/direct/subagent-direct.ts": [
|
|
56
|
+
"subagents"
|
|
57
|
+
],
|
|
58
|
+
"session-fork.ts": [
|
|
59
|
+
"import:dsh-agent",
|
|
60
|
+
"import:dsh-session"
|
|
61
|
+
],
|
|
62
|
+
"sessions.ts": [
|
|
63
|
+
"import:dsh-session"
|
|
64
|
+
],
|
|
65
|
+
"skill-catalog-refresh.ts": [
|
|
66
|
+
"import:dsh-agent"
|
|
67
|
+
],
|
|
68
|
+
"skill-catalog.ts": [
|
|
69
|
+
"agentPresets",
|
|
70
|
+
"skills"
|
|
71
|
+
],
|
|
72
|
+
"stats.ts": [
|
|
73
|
+
"import:dsh-session"
|
|
74
|
+
],
|
|
75
|
+
"surface-catalog.ts": [
|
|
76
|
+
"commands",
|
|
77
|
+
"import:dsh-agent"
|
|
78
|
+
],
|
|
79
|
+
"transcript.ts": [
|
|
80
|
+
"import:dsh-session"
|
|
81
|
+
]
|
|
82
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @xmoon76/dsh-pi-tui/scripts/client-boundary-gate — CI guard for the
|
|
4
|
+
* server/client migration (AGENTS.md "Server/client migration guardrails
|
|
5
|
+
* (hard rules)"): new feature code must not add direct Host coupling outside
|
|
6
|
+
* the approved boundary. The gate is "baseline allowlist + no-new-debt": the
|
|
7
|
+
* current (file, pattern) pairs are frozen in
|
|
8
|
+
* scripts/client-boundary-baseline.json, and any NEW pair fails. Existing
|
|
9
|
+
* coupling may stay on the allowlist until its owning migration phase moves
|
|
10
|
+
* it (docs/client-server-coupling.md); the baseline is updated deliberately
|
|
11
|
+
* by a maintainer when a phase legitimately relocates coupling, never to
|
|
12
|
+
* absorb new debt.
|
|
13
|
+
*
|
|
14
|
+
* Patterns cover the Host services the migration must isolate (the plan's
|
|
15
|
+
* coupling inventory): `ctx.get('service')`, `ctx.<service>` property
|
|
16
|
+
* access, and `@deepseek-ai/dsh-agent` / `@deepseek-ai/dsh-session` type
|
|
17
|
+
* imports (Agent / AgentHandle / Session). Cordis process services with no
|
|
18
|
+
* Host business state (`loader`, `appExit`) and the TUI's own services
|
|
19
|
+
* (`TUI_STARTUP_SERVICE`, `PI_TUI_EXTENSIONS_SERVICE`) are deliberately NOT
|
|
20
|
+
* patterns — they are not migration coupling.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* node scripts/client-boundary-gate.mjs # check (CI; exit 1 on new debt)
|
|
24
|
+
* node scripts/client-boundary-gate.mjs --report # print the current inventory
|
|
25
|
+
* @module client-boundary-gate
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
29
|
+
import { join, relative, dirname } from 'node:path'
|
|
30
|
+
import { fileURLToPath } from 'node:url'
|
|
31
|
+
|
|
32
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
33
|
+
const BASELINE_PATH = join(ROOT, 'scripts', 'client-boundary-baseline.json')
|
|
34
|
+
|
|
35
|
+
// Host services the migration must isolate. Keep in sync with
|
|
36
|
+
// docs/client-server-coupling.md.
|
|
37
|
+
export const HOST_SERVICES = [
|
|
38
|
+
'agents',
|
|
39
|
+
'sessions',
|
|
40
|
+
'subagents',
|
|
41
|
+
'jobs',
|
|
42
|
+
'credentials',
|
|
43
|
+
'userQuestions',
|
|
44
|
+
'agentPresets',
|
|
45
|
+
'sessionQuery',
|
|
46
|
+
'sessionPersistence',
|
|
47
|
+
'sessionTitle',
|
|
48
|
+
'commands',
|
|
49
|
+
'tools',
|
|
50
|
+
'skills',
|
|
51
|
+
'attachments',
|
|
52
|
+
'llm',
|
|
53
|
+
'settings',
|
|
54
|
+
'approval',
|
|
55
|
+
'authorization',
|
|
56
|
+
'permissionPresets',
|
|
57
|
+
'tokenMeter',
|
|
58
|
+
'agentDefaultModel',
|
|
59
|
+
'workspaceRegistry',
|
|
60
|
+
'directoryPicker',
|
|
61
|
+
'goal',
|
|
62
|
+
'planMode',
|
|
63
|
+
'sandboxPolicy',
|
|
64
|
+
'shell',
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
const SERVICE_ALT = HOST_SERVICES.join('|')
|
|
68
|
+
const PATTERNS = [
|
|
69
|
+
// ctx.get('service') — the dominant access form in this codebase.
|
|
70
|
+
{ key: (m) => m[1], re: new RegExp(`ctx\\.get\\(['"](${SERVICE_ALT})['"]`) },
|
|
71
|
+
// ctx.<service> property access (would throw without inject; still debt).
|
|
72
|
+
{ key: (m) => m[1], re: new RegExp(`ctx\\.(${SERVICE_ALT})\\b`) },
|
|
73
|
+
// Concrete Host object types.
|
|
74
|
+
{ key: () => 'import:dsh-agent', re: /@deepseek-ai\/dsh-agent/ },
|
|
75
|
+
{ key: () => 'import:dsh-session', re: /@deepseek-ai\/dsh-session/ },
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
/** True when the line is (heuristically) inside a comment. */
|
|
79
|
+
function isCommentLine(line) {
|
|
80
|
+
const t = line.trim()
|
|
81
|
+
return t.startsWith('*') || t.startsWith('//') || t.startsWith('/*') || t.startsWith('*/')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Scan a directory tree for Host-coupling patterns in non-comment code
|
|
86
|
+
* lines. Returns a map of `src/...`-style relative path -> Set of matched
|
|
87
|
+
* pattern keys (service names or import markers).
|
|
88
|
+
* @param {string} dir directory to scan
|
|
89
|
+
* @param {string} [base] base for relative paths (defaults to dir)
|
|
90
|
+
* @returns {Record<string, string[]>}
|
|
91
|
+
*/
|
|
92
|
+
export function scanTree(dir, base = dir) {
|
|
93
|
+
const out = {}
|
|
94
|
+
const walk = (d) => {
|
|
95
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
96
|
+
const p = join(d, entry.name)
|
|
97
|
+
if (entry.isDirectory()) {
|
|
98
|
+
if (entry.name === 'node_modules' || entry.name === 'dist') continue
|
|
99
|
+
walk(p)
|
|
100
|
+
} else if (entry.name.endsWith('.ts') || entry.name.endsWith('.mts')) {
|
|
101
|
+
const rel = relative(base, p).split('\\').join('/')
|
|
102
|
+
const hits = new Set()
|
|
103
|
+
const lines = readFileSync(p, 'utf8').split('\n')
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
if (isCommentLine(line)) continue
|
|
106
|
+
for (const { key, re } of PATTERNS) {
|
|
107
|
+
const m = line.match(re)
|
|
108
|
+
if (m) hits.add(key(m))
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (hits.size > 0) out[rel] = [...hits].sort()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
walk(dir)
|
|
116
|
+
return out
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Load the checked-in baseline (missing file = empty baseline). */
|
|
120
|
+
export function loadBaseline() {
|
|
121
|
+
if (!existsSync(BASELINE_PATH)) return {}
|
|
122
|
+
return JSON.parse(readFileSync(BASELINE_PATH, 'utf8'))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Compare a scanned inventory against the baseline. Returns the list of NEW
|
|
127
|
+
* (file, pattern) pairs — the no-new-debt violations.
|
|
128
|
+
* @param {Record<string, string[]>} scanned
|
|
129
|
+
* @param {Record<string, string[]>} baseline
|
|
130
|
+
* @returns {string[]}
|
|
131
|
+
*/
|
|
132
|
+
export function findNewDebt(scanned, baseline) {
|
|
133
|
+
const debt = []
|
|
134
|
+
for (const [file, patterns] of Object.entries(scanned)) {
|
|
135
|
+
const allowed = new Set(baseline[file] ?? [])
|
|
136
|
+
for (const pattern of patterns) {
|
|
137
|
+
if (!allowed.has(pattern)) debt.push(`${file}: ${pattern}`)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return debt
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function main() {
|
|
144
|
+
const scanned = scanTree(join(ROOT, 'src'))
|
|
145
|
+
if (process.argv.includes('--report')) {
|
|
146
|
+
for (const [file, patterns] of Object.entries(scanned).sort()) {
|
|
147
|
+
console.log(`${file}: ${patterns.join(', ')}`)
|
|
148
|
+
}
|
|
149
|
+
console.log(`\n${Object.keys(scanned).length} file(s) with Host coupling.`)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
const baseline = loadBaseline()
|
|
153
|
+
const debt = findNewDebt(scanned, baseline)
|
|
154
|
+
if (debt.length > 0) {
|
|
155
|
+
console.error('client-boundary-gate: NEW Host coupling outside the approved boundary:')
|
|
156
|
+
for (const d of debt) console.error(` ${d}`)
|
|
157
|
+
console.error('\nSee AGENTS.md "Server/client migration guardrails" and docs/client-server-coupling.md.')
|
|
158
|
+
process.exit(1)
|
|
159
|
+
}
|
|
160
|
+
console.log(`client-boundary-gate: ok (${Object.keys(scanned).length} file(s), no new Host coupling)`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (process.argv[1] && relative(ROOT, process.argv[1]).replace(/\\/g, '/') === 'scripts/client-boundary-gate.mjs') {
|
|
164
|
+
main()
|
|
165
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Pre-push gate stage derivation — the single source of truth for what the
|
|
4
|
+
* husky pre-push hook runs stays in package.json's `verify:*` scripts; this
|
|
5
|
+
* module turns one of them into the ordered per-command stage list the hook
|
|
6
|
+
* executes with progress reporting.
|
|
7
|
+
*
|
|
8
|
+
* A script like `a && b && c` yields `['a', 'b', 'c']`. Only top-level `&&`
|
|
9
|
+
* separators are split (a `&&` inside quotes, e.g. inside a node -e snippet,
|
|
10
|
+
* is not a separator). A trailing `&&` (or a `&&` followed by nothing but
|
|
11
|
+
* whitespace) is rejected — `sh -e` treats such a line as a syntax error, so
|
|
12
|
+
* a split would silently drop the intended tail.
|
|
13
|
+
*
|
|
14
|
+
* Supported shell grammar (the parsed script must stay within this subset):
|
|
15
|
+
* - commands chained with `&&` (the split points), each otherwise passed
|
|
16
|
+
* verbatim to `sh -c` for execution — the splitter does NOT interpret
|
|
17
|
+
* the commands themselves;
|
|
18
|
+
* - single- and double-quoted strings anywhere in the script (quotes are
|
|
19
|
+
* tracked so `&&` inside them is not a separator);
|
|
20
|
+
* - backslash escapes inside and outside quotes: an escaped `&&` is NOT
|
|
21
|
+
* treated as a separator (it stays inside the current stage, whose
|
|
22
|
+
* shell semantics are then preserved unchanged by `sh -c`);
|
|
23
|
+
* - anything the command segment may contain (redirects, env assignments,
|
|
24
|
+
* pipes, subshells, `node -e "..."`, ...) is preserved verbatim and
|
|
25
|
+
* executed by `sh -c` unchanged.
|
|
26
|
+
* NOT supported (the splitter rejects them loudly): unterminated quotes,
|
|
27
|
+
* dangling `&&`, empty commands (consecutive `&&`), empty scripts.
|
|
28
|
+
*
|
|
29
|
+
* Usage:
|
|
30
|
+
* node scripts/pre-push-stages.mjs <verify-script-name>
|
|
31
|
+
* # e.g. node scripts/pre-push-stages.mjs verify:prepush
|
|
32
|
+
*
|
|
33
|
+
* @module pre-push-stages
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { readFileSync } from 'node:fs'
|
|
37
|
+
import { join, dirname } from 'node:path'
|
|
38
|
+
import { fileURLToPath } from 'node:url'
|
|
39
|
+
|
|
40
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
41
|
+
|
|
42
|
+
function fail(message) {
|
|
43
|
+
console.error(`pre-push-stages: ${message}`)
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const [name] = process.argv.slice(2)
|
|
48
|
+
if (!name || !name.startsWith('verify:')) {
|
|
49
|
+
fail(`usage: node scripts/pre-push-stages.mjs <verify-script-name> (got: ${name ?? 'none'})`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'))
|
|
53
|
+
const script = pkg.scripts?.[name]
|
|
54
|
+
if (typeof script !== 'string' || script === '') {
|
|
55
|
+
fail(`package.json has no non-empty script "${name}" (update .husky/pre-push)`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Split on top-level `&&` only: walk the string tracking single-quote,
|
|
59
|
+
// double-quote and backslash escapes; a `&&` seen at depth 0 (no open
|
|
60
|
+
// quote) is a command separator. Escapes are honoured OUTSIDE quotes too
|
|
61
|
+
// (`\&&` is an escaped literal `&`, not a separator), and a script with
|
|
62
|
+
// an unterminated quote is rejected — sh would reject it as well.
|
|
63
|
+
const stages = []
|
|
64
|
+
let current = ''
|
|
65
|
+
let quote = null
|
|
66
|
+
for (let i = 0; i < script.length; i++) {
|
|
67
|
+
const ch = script[i]
|
|
68
|
+
if (quote === '"') {
|
|
69
|
+
if (ch === '\\') { current += ch + (script[i + 1] ?? ''); i++; continue }
|
|
70
|
+
if (ch === '"') quote = null
|
|
71
|
+
current += ch
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
if (quote === "'") {
|
|
75
|
+
if (ch === "'") quote = null
|
|
76
|
+
current += ch
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
if (ch === '\\') {
|
|
80
|
+
// Outside quotes: escape the next character (incl. `&`, so `\&&`
|
|
81
|
+
// stays literal). Keep the backslash in the fragment — it is
|
|
82
|
+
// meaningful to the shell that later runs the stage.
|
|
83
|
+
current += ch + (script[i + 1] ?? '')
|
|
84
|
+
i++
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
if (ch === '"' || ch === "'") {
|
|
88
|
+
quote = ch
|
|
89
|
+
current += ch
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
if (ch === '&' && script[i + 1] === '&') {
|
|
93
|
+
stages.push(current)
|
|
94
|
+
current = ''
|
|
95
|
+
i++
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
current += ch
|
|
99
|
+
}
|
|
100
|
+
stages.push(current)
|
|
101
|
+
|
|
102
|
+
if (quote !== null) {
|
|
103
|
+
fail(`script "${name}" has an unterminated ${quote === '"' ? 'double' : 'single'} quote — sh would reject it; fix package.json`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const trimmed = stages.map((s) => s.trim())
|
|
107
|
+
if (trimmed[trimmed.length - 1] === '') {
|
|
108
|
+
fail(`script "${name}" ends with a dangling "&&" — sh -e would reject it; fix package.json`)
|
|
109
|
+
}
|
|
110
|
+
for (const [i, stage] of trimmed.entries()) {
|
|
111
|
+
if (stage === '') {
|
|
112
|
+
fail(`script "${name}" has an empty command at position ${i + 1} (consecutive "&&")`)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
process.stdout.write(trimmed.join('\n') + '\n')
|