@xmoon76/dsh-pi-tui 0.1.8 → 0.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.
@@ -0,0 +1 @@
1
+ import { EventEmitter } from "events";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmoon76/dsh-pi-tui",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
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>",
@@ -33,6 +33,22 @@
33
33
  "types": "./dist/startup.d.mts",
34
34
  "default": "./dist/startup.mjs"
35
35
  },
36
+ "./extensions": {
37
+ "types": "./dist/extensions.d.mts",
38
+ "default": "./dist/extensions.mjs"
39
+ },
40
+ "./extensions/advanced": {
41
+ "types": "./dist/extension/advanced.d.mts",
42
+ "default": "./dist/extension/advanced.mjs"
43
+ },
44
+ "./extensions/unstable": {
45
+ "types": "./dist/extension/unstable.d.mts",
46
+ "default": "./dist/extension/unstable.mjs"
47
+ },
48
+ "./builtins": {
49
+ "types": "./dist/builtins.d.mts",
50
+ "default": "./dist/builtins.mjs"
51
+ },
36
52
  "./cordis.patch.yml": "./cordis.patch.yml",
37
53
  "./package.json": "./package.json"
38
54
  },
@@ -61,67 +77,71 @@
61
77
  "peerDependencies": {
62
78
  "@deepseek-ai/cordis": "^4.0.1",
63
79
  "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
64
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
65
- "@deepseek-ai/dsh-agent-default-model": "^0.1.0-rc.6",
66
- "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6",
67
- "@deepseek-ai/dsh-cmdline": "^0.1.0-rc.6",
68
- "@deepseek-ai/dsh-commands": "^0.1.0-rc.6",
69
- "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
70
- "@deepseek-ai/dsh-goal": "^0.1.0-rc.6",
71
- "@deepseek-ai/dsh-jobs": "^0.1.0-rc.6",
72
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
73
- "@deepseek-ai/dsh-llm-retry": "^0.1.0-rc.6",
74
- "@deepseek-ai/dsh-permission-presets": "^0.1.0-rc.6",
75
- "@deepseek-ai/dsh-plan-mode": "^0.1.0-rc.6",
76
- "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.6",
77
- "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
78
- "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6",
79
- "@deepseek-ai/dsh-session-title": "^0.1.0-rc.6",
80
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
81
- "@deepseek-ai/dsh-shell": "^0.1.0-rc.6",
82
- "@deepseek-ai/dsh-skill": "^0.1.0-rc.6",
83
- "@deepseek-ai/dsh-subagent": "^0.1.0-rc.6",
84
- "@deepseek-ai/dsh-token-meter": "^0.1.0-rc.6",
85
- "@deepseek-ai/dsh-tool-workflow": "^0.1.0-rc.6",
86
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
87
- "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.6",
88
- "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.6",
80
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.8",
81
+ "@deepseek-ai/dsh-agent-default-model": "^0.1.0-rc.8",
82
+ "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.8",
83
+ "@deepseek-ai/dsh-cmdline": "^0.1.0-rc.8",
84
+ "@deepseek-ai/dsh-commands": "^0.1.0-rc.8",
85
+ "@deepseek-ai/dsh-credentials": "^0.1.0-rc.8",
86
+ "@deepseek-ai/dsh-goal": "^0.1.0-rc.8",
87
+ "@deepseek-ai/dsh-jobs": "^0.1.0-rc.8",
88
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
89
+ "@deepseek-ai/dsh-llm-retry": "^0.1.0-rc.8",
90
+ "@deepseek-ai/dsh-permission-presets": "^0.1.0-rc.8",
91
+ "@deepseek-ai/dsh-plan-mode": "^0.1.0-rc.8",
92
+ "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.8",
93
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
94
+ "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8",
95
+ "@deepseek-ai/dsh-session-title": "^0.1.0-rc.8",
96
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
97
+ "@deepseek-ai/dsh-shell": "^0.1.0-rc.8",
98
+ "@deepseek-ai/dsh-skill": "^0.1.0-rc.8",
99
+ "@deepseek-ai/dsh-subagent": "^0.1.0-rc.8",
100
+ "@deepseek-ai/dsh-token-meter": "^0.1.0-rc.8",
101
+ "@deepseek-ai/dsh-tool-workflow": "^0.1.0-rc.8",
102
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.8",
103
+ "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.8",
104
+ "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.8",
89
105
  "@deepseek-ai/schemastery": "^3.18.1"
90
106
  },
91
107
  "devDependencies": {
92
108
  "@xmoon76/pi-tui": "^0.84.3",
93
109
  "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
94
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
95
- "@deepseek-ai/dsh-agent-default-model": "^0.1.0-rc.6",
96
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
97
- "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
98
- "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.6",
110
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.8",
111
+ "@deepseek-ai/dsh-agent-default-model": "^0.1.0-rc.8",
112
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
113
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
114
+ "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.8",
99
115
  "@deepseek-ai/schemastery": "^3.18.1",
100
116
  "@xterm/headless": "5.5.0",
101
117
  "@types/node": "^22.0.0",
102
118
  "typescript": "^5.9.0",
103
119
  "tsdown": "0.22.0",
104
- "@deepseek-ai/dsh-commands": "^0.1.0-rc.6",
105
- "@deepseek-ai/dsh-token-meter": "^0.1.0-rc.6",
106
- "@deepseek-ai/dsh-skill": "^0.1.0-rc.6",
107
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
108
- "@deepseek-ai/dsh-plan-mode": "^0.1.0-rc.6",
109
- "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6",
110
- "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.6",
111
- "@deepseek-ai/dsh-subagent": "^0.1.0-rc.6",
112
- "@deepseek-ai/dsh-goal": "^0.1.0-rc.6",
113
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
114
- "@deepseek-ai/dsh-tool-workflow": "^0.1.0-rc.6",
115
- "@deepseek-ai/dsh-llm-retry": "^0.1.0-rc.6",
116
- "@deepseek-ai/dsh-permission-presets": "^0.1.0-rc.6",
117
- "@deepseek-ai/dsh-session-title": "^0.1.0-rc.6",
118
- "@deepseek-ai/dsh-jobs": "^0.1.0-rc.6",
119
- "@deepseek-ai/dsh-shell": "^0.1.0-rc.6",
120
- "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
121
- "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6",
122
- "@deepseek-ai/dsh-agent-loop": "^0.1.0-rc.6",
123
- "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
124
- "@deepseek-ai/dsh-scope": "^0.1.0-rc.6"
120
+ "@deepseek-ai/dsh-commands": "^0.1.0-rc.8",
121
+ "@deepseek-ai/dsh-token-meter": "^0.1.0-rc.8",
122
+ "@deepseek-ai/dsh-skill": "^0.1.0-rc.8",
123
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
124
+ "@deepseek-ai/dsh-plan-mode": "^0.1.0-rc.8",
125
+ "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8",
126
+ "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.8",
127
+ "@deepseek-ai/dsh-subagent": "^0.1.0-rc.8",
128
+ "@deepseek-ai/dsh-goal": "^0.1.0-rc.8",
129
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.8",
130
+ "@deepseek-ai/dsh-tool-workflow": "^0.1.0-rc.8",
131
+ "@deepseek-ai/dsh-llm-retry": "^0.1.0-rc.8",
132
+ "@deepseek-ai/dsh-permission-presets": "^0.1.0-rc.8",
133
+ "@deepseek-ai/dsh-session-title": "^0.1.0-rc.8",
134
+ "@deepseek-ai/dsh-jobs": "^0.1.0-rc.8",
135
+ "@deepseek-ai/dsh-shell": "^0.1.0-rc.8",
136
+ "@deepseek-ai/dsh-credentials": "^0.1.0-rc.8",
137
+ "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.8",
138
+ "@deepseek-ai/dsh-agent-loop": "^0.1.0-rc.8",
139
+ "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8",
140
+ "@deepseek-ai/dsh-scope": "^0.1.0-rc.8",
141
+ "@deepseek-ai/dsh-attachment": "^0.1.0-rc.8",
142
+ "@deepseek-ai/dsh-brand": "^0.1.0-rc.8",
143
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
144
+ "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8"
125
145
  },
126
146
  "engines": {
127
147
  "node": ">=22.19.0"
@@ -129,9 +149,15 @@
129
149
  "scripts": {
130
150
  "build": "tsdown",
131
151
  "build:package": "pnpm --dir ../pi-tui build && pnpm build",
132
- "typecheck": "tsc -p tsconfig.json --noEmit",
133
- "test": "node --test test/*.test.ts test/*.test.mjs",
152
+ "typecheck": "pnpm build:package && tsc -p tsconfig.json --noEmit",
153
+ "test": "pnpm build:package && node --test test/*.test.ts test/*.test.mjs",
134
154
  "clean": "rm -rf dist",
135
- "smoke": "node scripts/tarball-smoke.mjs"
155
+ "smoke": "node scripts/tarball-smoke.mjs",
156
+ "smoke:fixture": "node scripts/extension-fixture-smoke.mjs",
157
+ "smoke:advanced": "node scripts/advanced-plugin-smoke.mjs",
158
+ "smoke:phase4": "node scripts/phase4-plugin-smoke.mjs",
159
+ "smoke:unstable": "node scripts/unstable-plugin-smoke.mjs",
160
+ "smoke:examples": "node scripts/examples-plugin-smoke.mjs",
161
+ "smoke:vim": "node scripts/vim-plugin-smoke.mjs"
136
162
  }
137
163
  }
@@ -0,0 +1,314 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * advanced-plugin-smoke — verify the PACKED ADVANCED extension surface end
4
+ * to end with a real third-party-shaped plugin (Phase 2, plan §15):
5
+ *
6
+ * 1. the fixture imports ONLY the public `@xmoon76/dsh-pi-tui/
7
+ * extensions/advanced` (and the stable `extensions` entry for the
8
+ * service type) subpaths — importing `@xmoon76/pi-tui`, `src/tui-app`
9
+ * or any repository-relative internal path FAILS the gate (static
10
+ * imports, dynamic import() AND require() are all scanned);
11
+ * 2. the fixture typechecks against the packed `.d.mts`;
12
+ * 3. the fixture COMPILES to JS and RUNTIME-LOADS against the packed
13
+ * bytes (apply() registers through a mock service without throwing);
14
+ * 4. the fixture exercises the ADVANCED surface: ADVANCED_API_LEVEL === 1,
15
+ * normalized input capture (observe + capture), the interactive
16
+ * overlay lease, and the advanced editor controls;
17
+ * 5. the fixture's module shape (name/inject/apply) matches the Loader;
18
+ * 6. no duplicate dsh runtime in the fixture tree.
19
+ *
20
+ * The probe mirrors the host install with SYMLINKS (no npm install — the
21
+ * gate is offline-safe; the packed package's runtime deps resolve from
22
+ * the repo's node_modules exactly like a real profile).
23
+ *
24
+ * Usage: node scripts/advanced-plugin-smoke.mjs [path-to-tgz]
25
+ * @module advanced-plugin-smoke
26
+ */
27
+
28
+ import { spawnSync } from 'node:child_process'
29
+ import {
30
+ existsSync,
31
+ mkdirSync,
32
+ mkdtempSync,
33
+ readdirSync,
34
+ readFileSync,
35
+ realpathSync,
36
+ rmSync,
37
+ statSync,
38
+ lstatSync,
39
+ symlinkSync,
40
+ writeFileSync,
41
+ } from 'node:fs'
42
+ import { tmpdir } from 'node:os'
43
+ import { dirname, join, resolve } from 'node:path'
44
+ import { fileURLToPath } from 'node:url'
45
+
46
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
47
+ const PACKAGE_ROOT = join(SCRIPT_DIR, '..')
48
+ const FIXTURE_ROOT = join(PACKAGE_ROOT, 'test', 'fixtures', 'advanced-plugin')
49
+
50
+ const failures = []
51
+ const checks = []
52
+
53
+ function check(name, ok, detail = '') {
54
+ checks.push(`${ok ? 'ok ' : 'FAIL'} ${name}${detail === '' ? '' : ` — ${detail}`}`)
55
+ if (!ok) failures.push(`${name}${detail === '' ? '' : `: ${detail}`}`)
56
+ }
57
+
58
+ function run(command, args, options = {}) {
59
+ return spawnSync(command, args, { encoding: 'utf8', ...options })
60
+ }
61
+
62
+ function resolveTarball(explicit) {
63
+ if (explicit !== undefined) {
64
+ const absolute = resolve(explicit)
65
+ if (!existsSync(absolute)) throw new Error(`tarball not found: ${explicit}`)
66
+ return absolute
67
+ }
68
+ const dir = PACKAGE_ROOT
69
+ const candidates = readdirSync(dir)
70
+ .filter(name => /xmoon76-dsh-pi-tui-.*\.tgz$/.test(name))
71
+ .map(name => join(dir, name))
72
+ .sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)
73
+ if (candidates.length === 0) {
74
+ throw new Error(`no xmoon76-dsh-pi-tui-*.tgz in ${dir}; run pnpm pack:release first`)
75
+ }
76
+ return candidates[0]
77
+ }
78
+
79
+ function main() {
80
+ const tarball = resolveTarball(process.argv[2])
81
+ const workDir = mkdtempSync(join(tmpdir(), 'advanced-plugin-'))
82
+ try {
83
+ // 1. Extract the packed package.
84
+ const extractedDir = join(workDir, 'pkg')
85
+ mkdirSync(extractedDir)
86
+ const extract = run('tar', ['-xzf', tarball, '-C', extractedDir])
87
+ check('tarball extracts', extract.status === 0, extract.stderr)
88
+ const pkgDir = join(extractedDir, 'package')
89
+ check('packed package has dist/extension/advanced.mjs', existsSync(join(pkgDir, 'dist', 'extension', 'advanced.mjs')))
90
+ check('packed package has dist/extension/advanced.d.mts', existsSync(join(pkgDir, 'dist', 'extension', 'advanced.d.mts')))
91
+
92
+ // 2. IMPORT BAN (plan §15 CI gate): the fixture's IMPORT STATEMENTS
93
+ // (multi-line blocks), dynamic import() and require() specifiers
94
+ // must never reference private internals.
95
+ const fixtureSrc = readFileSync(join(FIXTURE_ROOT, 'src', 'index.ts'), 'utf8')
96
+ const lines = fixtureSrc.split('\n')
97
+ const importStatements = []
98
+ for (let index = 0; index < lines.length; index++) {
99
+ if (/^\s*import\b/.test(lines[index] ?? '')) {
100
+ let statement = lines[index] ?? ''
101
+ while (!statement.includes(';') && index + 1 < lines.length) {
102
+ index += 1
103
+ statement += lines[index]
104
+ }
105
+ importStatements.push(statement)
106
+ }
107
+ }
108
+ const importLines = importStatements.join('\n')
109
+ const dynamicSpecifiers = [...fixtureSrc.matchAll(/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g)]
110
+ .map(match => match[1])
111
+ .concat([...fixtureSrc.matchAll(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g)]
112
+ .map(match => match[1]))
113
+ const banned = [
114
+ // Word-boundary: `@xmoon76/pi-tui` must NOT match
115
+ // `@xmoon76/dsh-pi-tui` (a substring of the public package name).
116
+ ['@xmoon76/pi-tui (the private fork, word-bounded)', /@xmoon76\/pi-tui(?![-a-z])/g],
117
+ ['@moonshot-ai/pi-tui', /@moonshot-ai\/pi-tui/g],
118
+ ['src/tui-app', /src\/tui-app/g],
119
+ ['TuiApp', /\bTuiApp\b/g],
120
+ ['TuiMainScreen', /\bTuiMainScreen\b/g],
121
+ ['TuiAltScreen', /\bTuiAltScreen\b/g],
122
+ ['repository-relative internal path', /\.\.\/\.\.\/src/g],
123
+ ]
124
+ for (const [name, pattern] of banned) {
125
+ const staticMatches = importLines.match(pattern) ?? []
126
+ const dynamicMatches = dynamicSpecifiers.filter(specifier => pattern.test(specifier))
127
+ check(`fixture does not import ${name}`, staticMatches.length === 0 && dynamicMatches.length === 0,
128
+ [...staticMatches, ...dynamicMatches].join(', '))
129
+ }
130
+ // The ONLY @xmoon76/dsh-pi-tui imports are the public subpaths
131
+ // (extensions/advanced + extensions for the service type; cordis is a
132
+ // legitimate peer for the plugin's apply signature).
133
+ const dshImports = importLines.match(/@xmoon76\/dsh-pi-tui[^'"]*/g) ?? []
134
+ check('fixture imports ONLY the public extensions subpaths',
135
+ dshImports.length === 2
136
+ && dshImports.includes('@xmoon76/dsh-pi-tui/extensions/advanced')
137
+ && dshImports.includes('@xmoon76/dsh-pi-tui/extensions'),
138
+ dshImports.join(', '))
139
+
140
+ // 3. ADVANCED-SURFACE EXERCISE (plan §15 acceptance): the fixture
141
+ // exercises the Phase-2 advanced surface through the public entry.
142
+ const surface = [
143
+ ['ADVANCED_API_LEVEL', 'tier level constant'],
144
+ ['advanced(', 'facade builder'],
145
+ ['ui.input.capture', 'normalized input capture'],
146
+ ["mode: 'observe'", 'observe mode'],
147
+ ["mode: 'capture'", 'capture mode'],
148
+ ['showInteractiveOverlay', 'interactive overlay'],
149
+ ['AdvancedInteractiveComponent', 'interactive component type'],
150
+ ['ui.editor', 'advanced editor controls'],
151
+ ]
152
+ for (const [api, label] of surface) {
153
+ check(`fixture exercises ${label} (${api})`, fixtureSrc.includes(api))
154
+ }
155
+ check('fixture never parses raw terminal bytes (no CSI-u / escape matching in the plugin)',
156
+ !/\\x1b\[/.test(fixtureSrc) && !/escape\s*\(/.test(fixtureSrc.replace(/handleInput/g, '')))
157
+
158
+ // 4. The fixture tree: symlink the packed package + the host's
159
+ // node_modules (the offline-safe mirror of a real profile install).
160
+ const fixtureDir = join(workDir, 'fixture')
161
+ mkdirSync(join(fixtureDir, 'node_modules', '@xmoon76'), { recursive: true })
162
+ symlinkSync(pkgDir, join(fixtureDir, 'node_modules', '@xmoon76', 'dsh-pi-tui'), 'dir')
163
+ const hostModules = join(PACKAGE_ROOT, 'node_modules')
164
+ mkdirSync(join(pkgDir, 'node_modules'), { recursive: true })
165
+ for (const entry of readdirSync(hostModules)) {
166
+ const target = join(pkgDir, 'node_modules', entry)
167
+ if (existsSync(target)) continue
168
+ symlinkSync(join(hostModules, entry), target, 'dir')
169
+ }
170
+ mkdirSync(join(fixtureDir, 'node_modules'), { recursive: true })
171
+ for (const entry of readdirSync(hostModules)) {
172
+ const target = join(fixtureDir, 'node_modules', entry)
173
+ if (existsSync(target)) continue
174
+ symlinkSync(join(hostModules, entry), target, 'dir')
175
+ }
176
+ mkdirSync(join(fixtureDir, 'src'), { recursive: true })
177
+ writeFileSync(join(fixtureDir, 'src', 'index.ts'), fixtureSrc)
178
+ writeFileSync(join(fixtureDir, 'tsconfig.json'), JSON.stringify({
179
+ compilerOptions: {
180
+ target: 'ES2022',
181
+ module: 'NodeNext',
182
+ moduleResolution: 'NodeNext',
183
+ strict: true,
184
+ noEmit: true,
185
+ skipLibCheck: true,
186
+ types: [],
187
+ },
188
+ include: ['src'],
189
+ }, null, 2))
190
+ writeFileSync(join(fixtureDir, 'package.json'), JSON.stringify({
191
+ name: 'dsh-pi-advanced-fixture',
192
+ private: true,
193
+ type: 'module',
194
+ }, null, 2))
195
+
196
+ // 5. Typecheck against the packed .d.mts (the repo's tsc — no
197
+ // registry fetch).
198
+ const tsc = run('node', [join(PACKAGE_ROOT, 'node_modules', 'typescript', 'bin', 'tsc'), '-p', join(fixtureDir, 'tsconfig.json')], { cwd: fixtureDir })
199
+ check('fixture typechecks against the packed .d.mts', tsc.status === 0,
200
+ tsc.status === 0 ? '' : (tsc.stdout + tsc.stderr).split('\n').slice(-6).join(' '))
201
+
202
+ // 6. Module shape.
203
+ const moduleShape = /export const name = ['"][^'"]+['"]/s.test(fixtureSrc)
204
+ && /export const inject = \[/s.test(fixtureSrc)
205
+ && /export function apply\(ctx: Context\)/s.test(fixtureSrc)
206
+ check('fixture module shape (name/inject/apply) matches the Loader', moduleShape)
207
+
208
+ // 7. RUNTIME LOAD: compile the fixture to JS and apply() it with a
209
+ // mock service — the registrations must not throw and must resolve
210
+ // the PACKED advanced module.
211
+ const emitDir = join(fixtureDir, 'dist-out')
212
+ const emit = run('node', [join(PACKAGE_ROOT, 'node_modules', 'typescript', 'bin', 'tsc'),
213
+ '-p', join(fixtureDir, 'tsconfig.json'), '--noEmit', 'false', '--outDir', emitDir], { cwd: fixtureDir })
214
+ check('fixture compiles to JS for the runtime probe', emit.status === 0,
215
+ emit.status === 0 ? '' : (emit.stdout + emit.stderr).split('\n').slice(-4).join(' '))
216
+ if (emit.status === 0) {
217
+ writeFileSync(join(fixtureDir, 'runtime-probe.mjs'), `
218
+ const context = {
219
+ get: (name) => name === 'piTuiExtensions' ? mockService() : undefined,
220
+ on: () => {},
221
+ }
222
+ function mockService() {
223
+ const inertLease = {
224
+ id: 'inert', active: false, focused: false,
225
+ focus: () => {}, blur: () => {}, invalidate: () => {},
226
+ close: () => {}, hide: () => {}, show: () => {},
227
+ }
228
+ const inertControls = {
229
+ getEditorState: () => ({ text: '', cursor: 0, focused: false, composing: false }),
230
+ setEditorText: () => {}, setEditorCursor: () => {},
231
+ insertEditorText: () => {}, pasteToEditor: () => {}, requestEditorFocus: () => {},
232
+ }
233
+ return {
234
+ api: () => ({
235
+ apiVersion: 1,
236
+ hostVersion: 'test',
237
+ capabilities: new Set(['advanced.input.capture', 'advanced.ui.interactive', 'advanced.editor.control']),
238
+ deprecations: new Map(),
239
+ }),
240
+ _advancedCaptureInput: (spec) => ({ id: spec.id, dispose: () => {} }),
241
+ _advancedShowInteractiveOverlay: () => inertLease,
242
+ _advancedEditorControls: () => inertControls,
243
+ _advancedUiSelect: () => Promise.resolve(undefined),
244
+ _advancedUiConfirm: () => Promise.resolve(false),
245
+ _advancedUiInput: () => Promise.resolve(undefined),
246
+ _advancedUiNotify: () => {},
247
+ _advancedUiCustom: () => Promise.resolve(undefined),
248
+ _advancedHostState: () => ({
249
+ getTheme: () => 'dark',
250
+ setTheme: () => {},
251
+ setTitle: () => {},
252
+ setWorkingMessage: () => {},
253
+ setToolsExpanded: () => {},
254
+ }),
255
+ }
256
+ }
257
+ const plugin = await import(${JSON.stringify(join(emitDir, 'index.js').replaceAll('\\', '/'))})
258
+ plugin.apply(context)
259
+ if (plugin.name !== 'dsh-pi-advanced-fixture') throw new Error('bad name: ' + plugin.name)
260
+ console.log('fixture-runtime-ok')
261
+ `)
262
+ const runtime = run(process.execPath, [join(fixtureDir, 'runtime-probe.mjs')], { cwd: fixtureDir })
263
+ check('fixture runtime-loads and apply() registers without throwing',
264
+ runtime.status === 0 && runtime.stdout.includes('fixture-runtime-ok'),
265
+ runtime.status === 0 ? '' : (runtime.stderr || runtime.stdout).split('\n').slice(-8).join(' '))
266
+ }
267
+
268
+ // 8. No duplicate dsh runtime: every @deepseek-ai entry in BOTH the
269
+ // packed package tree AND the fixture tree must be a SYMLINK whose
270
+ // realpath resolves into the host module tree (the packed tarball
271
+ // itself carries no node_modules — the scaffolding symlinks them,
272
+ // exactly like a real profile resolves in-box packages from the
273
+ // dsh installation). A REAL directory is a duplicate runtime copy.
274
+ const probeReal = resolve(workDir)
275
+ const auditTrees = [
276
+ ['packed package', join(pkgDir, 'node_modules', '@deepseek-ai')],
277
+ ['fixture tree', join(fixtureDir, 'node_modules', '@deepseek-ai')],
278
+ ]
279
+ const realDirs = []
280
+ for (const [label, nm] of auditTrees) {
281
+ const entries = existsSync(nm) ? readdirSync(nm) : []
282
+ for (const name of entries) {
283
+ const entry = join(nm, name)
284
+ let isSymlink = false
285
+ try { isSymlink = lstatSync(entry).isSymbolicLink() } catch { /* missing */ }
286
+ if (!isSymlink) {
287
+ realDirs.push(`${label}:${name} (real dir)`)
288
+ continue
289
+ }
290
+ try {
291
+ const target = resolve(realpathSync(entry))
292
+ if (target.startsWith(probeReal)) realDirs.push(`${label}:${name}->${target}`)
293
+ } catch {
294
+ realDirs.push(`${label}:${name} (dangling)`)
295
+ }
296
+ }
297
+ }
298
+ check('no duplicate @deepseek-ai dsh-runtime copy (symlinks only, both trees)',
299
+ realDirs.length === 0,
300
+ realDirs.join(', '))
301
+ } finally {
302
+ if (process.env.ADVANCED_SMOKE_KEEP !== '1') rmSync(workDir, { recursive: true, force: true })
303
+ }
304
+
305
+ for (const line of checks) console.log(line)
306
+ if (failures.length > 0) {
307
+ console.error(`\nadvanced-plugin smoke FAILED (${failures.length}):`)
308
+ for (const failure of failures) console.error(` - ${failure}`)
309
+ process.exit(1)
310
+ }
311
+ console.log('\nadvanced-plugin smoke passed')
312
+ }
313
+
314
+ main()
package/scripts/bench.mts CHANGED
@@ -167,7 +167,7 @@ const HEAP_REBUILDS = FAST ? 300 : 2000
167
167
 
168
168
  // --- the benchmark ----------------------------------------------------------
169
169
 
170
- function main(): void {
170
+ async function main(): Promise<void> {
171
171
  const rows: string[] = []
172
172
  const row = (label: string, value: string): void => { rows.push(`${label.padEnd(58)} ${value}`) }
173
173
 
@@ -250,12 +250,47 @@ function main(): void {
250
250
  row('heap (rerun with --expose-gc)', 'skipped')
251
251
  }
252
252
 
253
+ // 5. extension plugin overhead (M11, plan §23): the frame cost with N
254
+ // widget/list contributions — no-plugin baseline vs 10 vs 50 — and a
255
+ // streaming-invalidation case (the batcher must coalesce bursts).
256
+ const { ExtensionLedger } = await import('../src/extension/internal/ledger.ts')
257
+ const { SurfaceHost } = await import('../src/extension/internal/surface-host.ts')
258
+ const { WidgetOutlet } = await import('../src/extension/internal/widget-outlet.ts')
259
+ for (const count of [0, 10, 50]) {
260
+ const ledger = new ExtensionLedger()
261
+ const host = new SurfaceHost(ledger, () => {})
262
+ let renders = 0
263
+ const outlet = new WidgetOutlet(ledger, { requestRender: () => { renders += 1 } }, 'input.widget.below')
264
+ for (let index = 0; index < count; index++) {
265
+ ledger.register('input.widget.below', { id: `w${index}`, order: index }, {
266
+ view: { kind: 'text', spans: [{ text: `widget ${index}` }] },
267
+ importance: index,
268
+ maxHeight: 1,
269
+ }, `owner-${index}`)
270
+ }
271
+ const t0 = process.hrtime.bigint()
272
+ for (let frame = 0; frame < 200; frame += 1) outlet.refresh(0, 120, 4)
273
+ const elapsed = Number(process.hrtime.bigint() - t0) / 200
274
+ row(`widget outlet refresh ×${count} contributions (200 frames)`, `${elapsed.toFixed(0)} ns/frame`)
275
+ void host
276
+ }
277
+ // Invalidation coalescing: 100 replaces in one tick → ONE flush.
278
+ const { InvalidateBatcher } = await import('../src/extension/internal/batcher.ts')
279
+ let flushCount = 0
280
+ const batcher = new InvalidateBatcher({ requestRender: () => { flushCount += 1 } })
281
+ for (let index = 0; index < 100; index += 1) batcher.invalidate()
282
+ await Promise.resolve()
283
+ row('invalidation burst coalescing (100 in one tick)', `${flushCount} flush(es)`)
284
+
253
285
  console.log(rows.join('\n'))
254
286
  }
255
287
 
256
- try {
257
- main()
258
- } catch (error) {
288
+ // Round-1 finding 2 (fixed in round 2 — the previous attempt silently
289
+ // failed to write): main() is async — dynamic imports and awaited stages
290
+ // reject ASYNCHRONOUSLY. A try/catch cannot observe a rejection; catch
291
+ // the PROMISE explicitly so any async failure exits non-zero (never an
292
+ // unhandled rejection with a zero exit code).
293
+ main().catch((error) => {
259
294
  console.error(`bench: ${error instanceof Error ? error.message : String(error)}`)
260
295
  process.exit(1)
261
- }
296
+ })