@xmoon76/dsh-pi-tui 0.3.4 → 0.3.5
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 +648 -0
- package/README.md +557 -466
- package/dist/builtins.mjs +1 -13
- package/dist/extension/advanced.d.mts +1 -1
- package/dist/extension/unstable.d.mts +1 -1
- package/dist/extensions.d.mts +9 -6
- package/dist/extensions.mjs +1 -1
- package/dist/index.d.mts +2 -12
- package/dist/index.mjs +2 -2
- package/dist/{public-types-CZz5_arj.d.mts → public-types-_pe9ZH00.d.mts} +74 -8
- package/dist/{src-B1uP5j0d.mjs → src-B93JMbsv.mjs} +13497 -2012
- package/package.json +11 -6
- package/README.zh-CN.md +0 -469
- package/scripts/advanced-plugin-smoke.mjs +0 -314
- package/scripts/bench.mts +0 -302
- package/scripts/client-boundary-baseline.json +0 -82
- package/scripts/client-boundary-gate.mjs +0 -165
- package/scripts/e2e-session-lease.sh +0 -388
- package/scripts/examples-plugin-smoke.mjs +0 -320
- package/scripts/extension-fixture-smoke.mjs +0 -219
- package/scripts/naming-gate.mjs +0 -158
- package/scripts/phase4-plugin-smoke.mjs +0 -318
- package/scripts/pre-push-stages.mjs +0 -116
- package/scripts/preview-prompt.mts +0 -62
- package/scripts/preview-role-styles.mts +0 -97
- package/scripts/tarball-smoke.mjs +0 -393
- package/scripts/unstable-plugin-smoke.mjs +0 -300
- package/scripts/vim-plugin-smoke.mjs +0 -315
- /package/dist/{theme-registry-CjbtRmhd.d.mts → keybinding-registry-CjbtRmhd.d.mts} +0 -0
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* examples-plugin-smoke — verify the Phase-5 EXAMPLE plugins (vim,
|
|
4
|
-
* questionnaire, interactive-shell) against the PACKED
|
|
5
|
-
* `@xmoon76/dsh-pi-tui` tarball (plan §14/§18):
|
|
6
|
-
*
|
|
7
|
-
* 1. each example imports ONLY the public subpaths (extensions /
|
|
8
|
-
* extensions/advanced / extensions/unstable + cordis) — importing
|
|
9
|
-
* `@xmoon76/pi-tui`, `src/tui-app` or any repository-relative
|
|
10
|
-
* internal path FAILS the gate (static imports, dynamic import()
|
|
11
|
-
* AND require() are all scanned);
|
|
12
|
-
* 2. each example typechecks against the packed `.d.mts`;
|
|
13
|
-
* 3. each example COMPILES to JS and RUNTIME-LOADS against the packed
|
|
14
|
-
* bytes (apply() registers through a mock service without throwing);
|
|
15
|
-
* 4. the examples exercise their tier surfaces: vim (editor SDK +
|
|
16
|
-
* modal state machine), questionnaire (imperative UI broker),
|
|
17
|
-
* interactive-shell (unstable raw + low-level mount);
|
|
18
|
-
* 5. no duplicate dsh runtime in the example trees.
|
|
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/examples-plugin-smoke.mjs [path-to-tgz]
|
|
25
|
-
* @module examples-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 EXAMPLES_ROOT = join(PACKAGE_ROOT, 'examples', 'plugins')
|
|
49
|
-
|
|
50
|
-
/** The example plugins under gate (name → expected module name). */
|
|
51
|
-
const EXAMPLES = [
|
|
52
|
-
{ dir: 'vim', moduleName: 'dsh-pi-example-vim', surface: ['registerEditor', 'handleInput', 'EditorInputEvent', "'insert'", "'normal'"] },
|
|
53
|
-
{ dir: 'questionnaire', moduleName: 'dsh-pi-example-questionnaire', surface: ['ui.ui.select', 'ui.ui.input', 'ui.ui.confirm', 'ui.ui.notify'] },
|
|
54
|
-
{ dir: 'interactive-shell', moduleName: 'dsh-pi-example-interactive-shell', surface: ['ui.input.captureRaw', "mode: 'exclusive'", 'mountComponent'] },
|
|
55
|
-
]
|
|
56
|
-
|
|
57
|
-
const failures = []
|
|
58
|
-
const checks = []
|
|
59
|
-
|
|
60
|
-
function check(name, ok, detail = '') {
|
|
61
|
-
checks.push(`${ok ? 'ok ' : 'FAIL'} ${name}${detail === '' ? '' : ` — ${detail}`}`)
|
|
62
|
-
if (!ok) failures.push(`${name}${detail === '' ? '' : `: ${detail}`}`)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function run(command, args, options = {}) {
|
|
66
|
-
return spawnSync(command, args, { encoding: 'utf8', ...options })
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function resolveTarball(explicit) {
|
|
70
|
-
if (explicit !== undefined) {
|
|
71
|
-
const absolute = resolve(explicit)
|
|
72
|
-
if (!existsSync(absolute)) throw new Error(`tarball not found: ${explicit}`)
|
|
73
|
-
return absolute
|
|
74
|
-
}
|
|
75
|
-
const dir = PACKAGE_ROOT
|
|
76
|
-
const candidates = readdirSync(dir)
|
|
77
|
-
.filter(name => /xmoon76-dsh-pi-tui-.*\.tgz$/.test(name))
|
|
78
|
-
.map(name => join(dir, name))
|
|
79
|
-
.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)
|
|
80
|
-
if (candidates.length === 0) {
|
|
81
|
-
throw new Error(`no xmoon76-dsh-pi-tui-*.tgz in ${dir}; run pnpm pack:release first`)
|
|
82
|
-
}
|
|
83
|
-
return candidates[0]
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** The import statements of one source file (multi-line blocks). */
|
|
87
|
-
function importStatementsOf(source) {
|
|
88
|
-
const lines = source.split('\n')
|
|
89
|
-
const statements = []
|
|
90
|
-
for (let index = 0; index < lines.length; index++) {
|
|
91
|
-
if (/^\s*import\b/.test(lines[index] ?? '')) {
|
|
92
|
-
let statement = lines[index] ?? ''
|
|
93
|
-
while (!statement.includes(';') && index + 1 < lines.length) {
|
|
94
|
-
index += 1
|
|
95
|
-
statement += lines[index]
|
|
96
|
-
}
|
|
97
|
-
statements.push(statement)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return statements.join('\n')
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function main() {
|
|
104
|
-
const tarball = resolveTarball(process.argv[2])
|
|
105
|
-
const workDir = mkdtempSync(join(tmpdir(), 'examples-plugin-'))
|
|
106
|
-
try {
|
|
107
|
-
// 1. Extract the packed package.
|
|
108
|
-
const extractedDir = join(workDir, 'pkg')
|
|
109
|
-
mkdirSync(extractedDir)
|
|
110
|
-
const extract = run('tar', ['-xzf', tarball, '-C', extractedDir])
|
|
111
|
-
check('tarball extracts', extract.status === 0, extract.stderr)
|
|
112
|
-
const pkgDir = join(extractedDir, 'package')
|
|
113
|
-
check('packed package has dist/extensions.mjs', existsSync(join(pkgDir, 'dist', 'extensions.mjs')))
|
|
114
|
-
check('packed package has dist/extension/advanced.mjs', existsSync(join(pkgDir, 'dist', 'extension', 'advanced.mjs')))
|
|
115
|
-
check('packed package has dist/extension/unstable.mjs', existsSync(join(pkgDir, 'dist', 'extension', 'unstable.mjs')))
|
|
116
|
-
|
|
117
|
-
// The host modules (symlinked into the probe trees — the offline-safe
|
|
118
|
-
// mirror of a real profile install).
|
|
119
|
-
const hostModules = join(PACKAGE_ROOT, 'node_modules')
|
|
120
|
-
|
|
121
|
-
for (const example of EXAMPLES) {
|
|
122
|
-
const exampleRoot = join(EXAMPLES_ROOT, example.dir)
|
|
123
|
-
const fixtureSrc = readFileSync(join(exampleRoot, 'src', 'index.ts'), 'utf8')
|
|
124
|
-
const importLines = importStatementsOf(fixtureSrc)
|
|
125
|
-
const dynamicSpecifiers = [...fixtureSrc.matchAll(/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g)]
|
|
126
|
-
.map(match => match[1])
|
|
127
|
-
.concat([...fixtureSrc.matchAll(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g)]
|
|
128
|
-
.map(match => match[1]))
|
|
129
|
-
|
|
130
|
-
// 2. IMPORT BAN (plan §14 CI gate).
|
|
131
|
-
const banned = [
|
|
132
|
-
['@xmoon76/pi-tui (the private fork, word-bounded)', /@xmoon76\/pi-tui(?![-a-z])/g],
|
|
133
|
-
['@moonshot-ai/pi-tui', /@moonshot-ai\/pi-tui/g],
|
|
134
|
-
['src/tui-app', /src\/tui-app/g],
|
|
135
|
-
['TuiApp', /\bTuiApp\b/g],
|
|
136
|
-
['TuiMainScreen', /\bTuiMainScreen\b/g],
|
|
137
|
-
['TuiAltScreen', /\bTuiAltScreen\b/g],
|
|
138
|
-
['repository-relative internal path', /\.\.\/\.\.\/src/g],
|
|
139
|
-
]
|
|
140
|
-
for (const [name, pattern] of banned) {
|
|
141
|
-
const staticMatches = importLines.match(pattern) ?? []
|
|
142
|
-
const dynamicMatches = dynamicSpecifiers.filter(specifier => pattern.test(specifier))
|
|
143
|
-
check(`${example.dir}: does not import ${name}`, staticMatches.length === 0 && dynamicMatches.length === 0,
|
|
144
|
-
[...staticMatches, ...dynamicMatches].join(', '))
|
|
145
|
-
}
|
|
146
|
-
const dshImports = importLines.match(/@xmoon76\/dsh-pi-tui[^'"]*/g) ?? []
|
|
147
|
-
const allowed = ['@xmoon76/dsh-pi-tui/extensions', '@xmoon76/dsh-pi-tui/extensions/advanced', '@xmoon76/dsh-pi-tui/extensions/unstable']
|
|
148
|
-
check(`${example.dir}: imports ONLY the public extensions subpaths`,
|
|
149
|
-
dshImports.length > 0 && dshImports.every(specifier => allowed.includes(specifier)),
|
|
150
|
-
dshImports.join(', '))
|
|
151
|
-
|
|
152
|
-
// 3. SURFACE EXERCISE (plan §18 acceptance).
|
|
153
|
-
for (const api of example.surface) {
|
|
154
|
-
check(`${example.dir}: exercises ${api}`, fixtureSrc.includes(api))
|
|
155
|
-
}
|
|
156
|
-
check(`${example.dir}: never parses raw terminal bytes (no CSI-u / escape matching in the plugin)`,
|
|
157
|
-
!/\\x1b\[/.test(fixtureSrc) && !/escape\s*\(/.test(fixtureSrc.replace(/handleInput/g, '')))
|
|
158
|
-
|
|
159
|
-
// 4. The probe tree: symlink the packed package + the host modules.
|
|
160
|
-
const fixtureDir = join(workDir, example.dir)
|
|
161
|
-
mkdirSync(join(fixtureDir, 'node_modules', '@xmoon76'), { recursive: true })
|
|
162
|
-
symlinkSync(pkgDir, join(fixtureDir, 'node_modules', '@xmoon76', 'dsh-pi-tui'), 'dir')
|
|
163
|
-
mkdirSync(join(pkgDir, 'node_modules'), { recursive: true })
|
|
164
|
-
for (const entry of readdirSync(hostModules)) {
|
|
165
|
-
const target = join(pkgDir, 'node_modules', entry)
|
|
166
|
-
if (existsSync(target)) continue
|
|
167
|
-
symlinkSync(join(hostModules, entry), target, 'dir')
|
|
168
|
-
}
|
|
169
|
-
mkdirSync(join(fixtureDir, 'node_modules'), { recursive: true })
|
|
170
|
-
for (const entry of readdirSync(hostModules)) {
|
|
171
|
-
const target = join(fixtureDir, 'node_modules', entry)
|
|
172
|
-
if (existsSync(target)) continue
|
|
173
|
-
symlinkSync(join(hostModules, entry), target, 'dir')
|
|
174
|
-
}
|
|
175
|
-
mkdirSync(join(fixtureDir, 'src'), { recursive: true })
|
|
176
|
-
writeFileSync(join(fixtureDir, 'src', 'index.ts'), fixtureSrc)
|
|
177
|
-
writeFileSync(join(fixtureDir, 'tsconfig.json'), JSON.stringify({
|
|
178
|
-
compilerOptions: {
|
|
179
|
-
target: 'ES2022',
|
|
180
|
-
module: 'NodeNext',
|
|
181
|
-
moduleResolution: 'NodeNext',
|
|
182
|
-
strict: true,
|
|
183
|
-
noEmit: true,
|
|
184
|
-
skipLibCheck: true,
|
|
185
|
-
types: [],
|
|
186
|
-
},
|
|
187
|
-
include: ['src'],
|
|
188
|
-
}, null, 2))
|
|
189
|
-
writeFileSync(join(fixtureDir, 'package.json'), JSON.stringify({
|
|
190
|
-
name: example.moduleName,
|
|
191
|
-
private: true,
|
|
192
|
-
type: 'module',
|
|
193
|
-
}, null, 2))
|
|
194
|
-
|
|
195
|
-
// 5. Typecheck against the packed .d.mts.
|
|
196
|
-
const tsc = run('node', [join(PACKAGE_ROOT, 'node_modules', 'typescript', 'bin', 'tsc'), '-p', join(fixtureDir, 'tsconfig.json')], { cwd: fixtureDir })
|
|
197
|
-
check(`${example.dir}: typechecks against the packed .d.mts`, tsc.status === 0,
|
|
198
|
-
tsc.status === 0 ? '' : (tsc.stdout + tsc.stderr).split('\n').slice(-6).join(' '))
|
|
199
|
-
|
|
200
|
-
// 6. Module shape.
|
|
201
|
-
const moduleShape = /export const name = ['"][^'"]+['"]/s.test(fixtureSrc)
|
|
202
|
-
&& /export const inject = \[/s.test(fixtureSrc)
|
|
203
|
-
&& /export function apply\(ctx: Context\)/s.test(fixtureSrc)
|
|
204
|
-
check(`${example.dir}: module shape (name/inject/apply) matches the Loader`, moduleShape)
|
|
205
|
-
|
|
206
|
-
// 7. RUNTIME LOAD: compile to JS and apply() with a mock service.
|
|
207
|
-
const emitDir = join(fixtureDir, 'dist-out')
|
|
208
|
-
const emit = run('node', [join(PACKAGE_ROOT, 'node_modules', 'typescript', 'bin', 'tsc'),
|
|
209
|
-
'-p', join(fixtureDir, 'tsconfig.json'), '--noEmit', 'false', '--outDir', emitDir], { cwd: fixtureDir })
|
|
210
|
-
check(`${example.dir}: compiles to JS for the runtime probe`, emit.status === 0,
|
|
211
|
-
emit.status === 0 ? '' : (emit.stdout + emit.stderr).split('\n').slice(-4).join(' '))
|
|
212
|
-
if (emit.status === 0) {
|
|
213
|
-
writeFileSync(join(fixtureDir, 'runtime-probe.mjs'), `
|
|
214
|
-
const context = {
|
|
215
|
-
get: (name) => name === 'piTuiExtensions' ? mockService() : undefined,
|
|
216
|
-
on: () => {},
|
|
217
|
-
}
|
|
218
|
-
function mockService() {
|
|
219
|
-
const inertLease = {
|
|
220
|
-
id: 'inert', active: false, focused: false,
|
|
221
|
-
focus: () => {}, blur: () => {}, invalidate: () => {},
|
|
222
|
-
close: () => {}, hide: () => {}, show: () => {},
|
|
223
|
-
}
|
|
224
|
-
const inertControls = {
|
|
225
|
-
getEditorState: () => ({ text: '', cursor: 0, focused: false, composing: false }),
|
|
226
|
-
setEditorText: () => {}, setEditorCursor: () => {},
|
|
227
|
-
insertEditorText: () => {}, pasteToEditor: () => {}, requestEditorFocus: () => {},
|
|
228
|
-
}
|
|
229
|
-
const inertHandle = {
|
|
230
|
-
surfaceId: 'inert', generation: 0, width: 0, height: 0,
|
|
231
|
-
requestRender: () => {},
|
|
232
|
-
mountComponent: () => inertLease,
|
|
233
|
-
}
|
|
234
|
-
const makeHandle = () => ({ id: 'x', invalidate: () => {}, replace: () => {}, dispose: () => {} })
|
|
235
|
-
return {
|
|
236
|
-
api: () => ({
|
|
237
|
-
apiVersion: 1,
|
|
238
|
-
hostVersion: 'test',
|
|
239
|
-
capabilities: new Set([
|
|
240
|
-
'slot.chrome.header.badge', 'slot.input.dock.item', 'slot.chrome.footer.status',
|
|
241
|
-
'slot.input.widget', 'advanced.input.capture', 'advanced.ui.interactive',
|
|
242
|
-
'advanced.editor.control', 'unstable.input.raw', 'unstable.surface.handle',
|
|
243
|
-
]),
|
|
244
|
-
deprecations: new Map(),
|
|
245
|
-
}),
|
|
246
|
-
register: () => makeHandle(),
|
|
247
|
-
registerCommand: () => makeHandle(),
|
|
248
|
-
registerTheme: () => makeHandle(),
|
|
249
|
-
registerAutocomplete: () => makeHandle(),
|
|
250
|
-
registerSetting: () => makeHandle(),
|
|
251
|
-
registerKeybinding: () => makeHandle(),
|
|
252
|
-
registerMessageRenderer: () => makeHandle(),
|
|
253
|
-
registerToolRenderer: () => makeHandle(),
|
|
254
|
-
registerEditor: () => makeHandle(),
|
|
255
|
-
showOverlay: () => ({ close: () => {}, hide: () => {}, show: () => {} }),
|
|
256
|
-
_advancedCaptureInput: (spec) => ({ id: spec.id, dispose: () => {} }),
|
|
257
|
-
_advancedShowInteractiveOverlay: () => inertLease,
|
|
258
|
-
_advancedEditorControls: () => inertControls,
|
|
259
|
-
_advancedUiSelect: () => Promise.resolve(undefined),
|
|
260
|
-
_advancedUiConfirm: () => Promise.resolve(false),
|
|
261
|
-
_advancedUiInput: () => Promise.resolve(undefined),
|
|
262
|
-
_advancedUiNotify: () => {},
|
|
263
|
-
_advancedUiCustom: () => Promise.resolve(undefined),
|
|
264
|
-
_advancedHostState: () => ({
|
|
265
|
-
getTheme: () => 'dark', setTheme: () => {}, setTitle: () => {},
|
|
266
|
-
setWorkingMessage: () => {}, setToolsExpanded: () => {},
|
|
267
|
-
}),
|
|
268
|
-
_unstableCaptureRaw: (spec) => ({ id: spec.id, dispose: () => {} }),
|
|
269
|
-
_unstableSurfaceHandle: () => inertHandle,
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
const plugin = await import(${JSON.stringify(join(emitDir, 'index.js').replaceAll('\\', '/'))})
|
|
273
|
-
plugin.apply(context)
|
|
274
|
-
if (plugin.name !== ${JSON.stringify(example.moduleName)}) throw new Error('bad name: ' + plugin.name)
|
|
275
|
-
console.log('fixture-runtime-ok')
|
|
276
|
-
`)
|
|
277
|
-
const runtime = run(process.execPath, [join(fixtureDir, 'runtime-probe.mjs')], { cwd: fixtureDir })
|
|
278
|
-
check(`${example.dir}: runtime-loads and apply() registers without throwing`,
|
|
279
|
-
runtime.status === 0 && runtime.stdout.includes('fixture-runtime-ok'),
|
|
280
|
-
runtime.status === 0 ? '' : (runtime.stderr || runtime.stdout).split('\n').slice(-8).join(' '))
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// 8. No duplicate dsh runtime in the example tree.
|
|
284
|
-
const probeReal = resolve(workDir)
|
|
285
|
-
const nm = join(fixtureDir, 'node_modules', '@deepseek-ai')
|
|
286
|
-
const realDirs = []
|
|
287
|
-
const entries = existsSync(nm) ? readdirSync(nm) : []
|
|
288
|
-
for (const name of entries) {
|
|
289
|
-
const entry = join(nm, name)
|
|
290
|
-
let isSymlink = false
|
|
291
|
-
try { isSymlink = lstatSync(entry).isSymbolicLink() } catch { /* missing */ }
|
|
292
|
-
if (!isSymlink) {
|
|
293
|
-
realDirs.push(`${name} (real dir)`)
|
|
294
|
-
continue
|
|
295
|
-
}
|
|
296
|
-
try {
|
|
297
|
-
const target = resolve(realpathSync(entry))
|
|
298
|
-
if (target.startsWith(probeReal)) realDirs.push(`${name}->${target}`)
|
|
299
|
-
} catch {
|
|
300
|
-
realDirs.push(`${name} (dangling)`)
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
check(`${example.dir}: no duplicate @deepseek-ai dsh-runtime copy (symlinks only)`,
|
|
304
|
-
realDirs.length === 0,
|
|
305
|
-
realDirs.join(', '))
|
|
306
|
-
}
|
|
307
|
-
} finally {
|
|
308
|
-
if (process.env.EXAMPLES_SMOKE_KEEP !== '1') rmSync(workDir, { recursive: true, force: true })
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
for (const line of checks) console.log(line)
|
|
312
|
-
if (failures.length > 0) {
|
|
313
|
-
console.error(`\nexamples-plugin smoke FAILED (${failures.length}):`)
|
|
314
|
-
for (const failure of failures) console.error(` - ${failure}`)
|
|
315
|
-
process.exit(1)
|
|
316
|
-
}
|
|
317
|
-
console.log('\nexamples-plugin smoke passed')
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
main()
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* extension-fixture-smoke — verify the PACKED extension surface end to end
|
|
4
|
-
* with a real third-party-shaped plugin (plan §16 M3 packed fixture):
|
|
5
|
-
*
|
|
6
|
-
* 1. the fixture typechecks against the packed `@xmoon76/dsh-pi-tui`
|
|
7
|
-
* tarball's `./extensions` subpath (its `.d.mts`);
|
|
8
|
-
* 2. the fixture's runtime import resolves ONLY the packed bytes (the
|
|
9
|
-
* tarball, not the workspace source);
|
|
10
|
-
* 3. the fixture registers through the public service API and its module
|
|
11
|
-
* shape (name/inject/apply) matches what the Loader mounts;
|
|
12
|
-
* 4. the fixture's node_modules contains NO `@deepseek-ai` entry (no
|
|
13
|
-
* duplicate dsh runtime — peer deps resolve from the host install).
|
|
14
|
-
*
|
|
15
|
-
* Usage: node scripts/extension-fixture-smoke.mjs [path-to-tgz]
|
|
16
|
-
* The tarball path defaults to the newest `xmoon76-dsh-pi-tui-*.tgz` in the
|
|
17
|
-
* package directory (the `pnpm pack:release` output location).
|
|
18
|
-
* @module extension-fixture-smoke
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { spawnSync } from 'node:child_process'
|
|
22
|
-
import {
|
|
23
|
-
existsSync,
|
|
24
|
-
mkdirSync,
|
|
25
|
-
mkdtempSync,
|
|
26
|
-
readdirSync,
|
|
27
|
-
readFileSync,
|
|
28
|
-
rmSync,
|
|
29
|
-
statSync,
|
|
30
|
-
symlinkSync,
|
|
31
|
-
writeFileSync,
|
|
32
|
-
} from 'node:fs'
|
|
33
|
-
import { tmpdir } from 'node:os'
|
|
34
|
-
import { basename, dirname, join, resolve } from 'node:path'
|
|
35
|
-
import { fileURLToPath } from 'node:url'
|
|
36
|
-
|
|
37
|
-
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
|
|
38
|
-
const PACKAGE_ROOT = join(SCRIPT_DIR, '..')
|
|
39
|
-
const FIXTURE_ROOT = join(PACKAGE_ROOT, 'test', 'fixtures', 'extension-plugin')
|
|
40
|
-
|
|
41
|
-
const failures = []
|
|
42
|
-
const checks = []
|
|
43
|
-
|
|
44
|
-
function check(name, ok, detail = '') {
|
|
45
|
-
checks.push(`${ok ? 'ok ' : 'FAIL'} ${name}${detail === '' ? '' : ` — ${detail}`}`)
|
|
46
|
-
if (!ok) failures.push(`${name}${detail === '' ? '' : `: ${detail}`}`)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function run(command, args, options = {}) {
|
|
50
|
-
return spawnSync(command, args, { encoding: 'utf8', ...options })
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function resolveTarball(explicit) {
|
|
54
|
-
if (explicit !== undefined) {
|
|
55
|
-
const absolute = resolve(explicit)
|
|
56
|
-
if (!existsSync(absolute)) throw new Error(`tarball not found: ${explicit}`)
|
|
57
|
-
return absolute
|
|
58
|
-
}
|
|
59
|
-
const dir = PACKAGE_ROOT
|
|
60
|
-
const candidates = readdirSync(dir)
|
|
61
|
-
.filter(name => /xmoon76-dsh-pi-tui-.*\.tgz$/.test(name))
|
|
62
|
-
.map(name => join(dir, name))
|
|
63
|
-
.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)
|
|
64
|
-
if (candidates.length === 0) {
|
|
65
|
-
throw new Error(`no xmoon76-dsh-pi-tui-*.tgz in ${dir}; run pnpm pack:release first`)
|
|
66
|
-
}
|
|
67
|
-
return candidates[0]
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function main() {
|
|
71
|
-
const tarball = resolveTarball(process.argv[2])
|
|
72
|
-
const workDir = mkdtempSync(join(tmpdir(), 'extension-fixture-'))
|
|
73
|
-
try {
|
|
74
|
-
// 1. Extract the packed package.
|
|
75
|
-
const extractedDir = join(workDir, 'pkg')
|
|
76
|
-
mkdirSync(extractedDir)
|
|
77
|
-
const extract = run('tar', ['-xzf', tarball, '-C', extractedDir])
|
|
78
|
-
check('tarball extracts', extract.status === 0, extract.stderr)
|
|
79
|
-
const pkgDir = join(extractedDir, 'package')
|
|
80
|
-
check('packed package has dist/extensions.mjs', existsSync(join(pkgDir, 'dist', 'extensions.mjs')))
|
|
81
|
-
check('packed package has dist/extensions.d.mts', existsSync(join(pkgDir, 'dist', 'extensions.d.mts')))
|
|
82
|
-
|
|
83
|
-
// 2. Fixture workspace: node_modules/@xmoon76/dsh-pi-tui -> packed pkg.
|
|
84
|
-
const fixtureDir = join(workDir, 'fixture')
|
|
85
|
-
mkdirSync(join(fixtureDir, 'node_modules', '@xmoon76'), { recursive: true })
|
|
86
|
-
symlinkSync(pkgDir, join(fixtureDir, 'node_modules', '@xmoon76', 'dsh-pi-tui'), 'dir')
|
|
87
|
-
// Peer deps (cordis, commander, @deepseek-ai/*) resolve from the host
|
|
88
|
-
// install in a real deployment. ESM resolution walks UP from the
|
|
89
|
-
// importing file, so the deps must sit in the PACKED package's own
|
|
90
|
-
// node_modules (a fixture-level node_modules would never be reached);
|
|
91
|
-
// the smoke mirrors the host install by symlinking the main package's
|
|
92
|
-
// modules there. The packed tarball itself carries NO node_modules
|
|
93
|
-
// (check 6 asserts that) — this is the smoke fixture's own scaffolding.
|
|
94
|
-
// The fixture's own source also imports @deepseek-ai/cordis (the peer
|
|
95
|
-
// Context type), so the fixture tree sees the same host modules.
|
|
96
|
-
const hostModules = join(PACKAGE_ROOT, 'node_modules')
|
|
97
|
-
mkdirSync(join(pkgDir, 'node_modules'), { recursive: true })
|
|
98
|
-
for (const entry of readdirSync(hostModules)) {
|
|
99
|
-
const target = join(pkgDir, 'node_modules', entry)
|
|
100
|
-
if (existsSync(target)) continue
|
|
101
|
-
symlinkSync(join(hostModules, entry), target, 'dir')
|
|
102
|
-
const fixtureTarget = join(fixtureDir, 'node_modules', entry)
|
|
103
|
-
if (!existsSync(fixtureTarget)) {
|
|
104
|
-
symlinkSync(join(hostModules, entry), fixtureTarget, 'dir')
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
mkdirSync(join(fixtureDir, 'src'), { recursive: true })
|
|
108
|
-
// Copy the fixture sources explicitly into the fixture's src/ (keep
|
|
109
|
-
// the repo fixture as the source of truth). Copying each file avoids
|
|
110
|
-
// the `cp -r src dst` behavior differences across platforms when dst
|
|
111
|
-
// already contains src/ (GNU cp merges into dst/src, BSD may nest).
|
|
112
|
-
const sourceDir = join(FIXTURE_ROOT, 'src')
|
|
113
|
-
for (const file of readdirSync(sourceDir)) {
|
|
114
|
-
const from = join(sourceDir, file)
|
|
115
|
-
const to = join(fixtureDir, 'src', file)
|
|
116
|
-
if (statSync(from).isDirectory()) {
|
|
117
|
-
run('cp', ['-r', from, to])
|
|
118
|
-
} else {
|
|
119
|
-
writeFileSync(to, readFileSync(from))
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
check('fixture sources copied', existsSync(join(fixtureDir, 'src', 'index.ts')))
|
|
123
|
-
writeFileSync(join(fixtureDir, 'tsconfig.json'), readFileSync(join(FIXTURE_ROOT, 'tsconfig.json')))
|
|
124
|
-
writeFileSync(join(fixtureDir, 'package.json'), JSON.stringify({
|
|
125
|
-
name: 'dsh-pi-extension-fixture',
|
|
126
|
-
private: true,
|
|
127
|
-
type: 'module',
|
|
128
|
-
}, null, 2))
|
|
129
|
-
|
|
130
|
-
// 3. Typecheck the fixture against the PACKED .d.mts.
|
|
131
|
-
const tsc = run('node', [join(PACKAGE_ROOT, 'node_modules', 'typescript', 'bin', 'tsc'), '-p', join(fixtureDir, 'tsconfig.json')], {
|
|
132
|
-
cwd: fixtureDir,
|
|
133
|
-
})
|
|
134
|
-
check('fixture typechecks against the packed .d.mts', tsc.status === 0,
|
|
135
|
-
tsc.status === 0 ? '' : tsc.stdout.slice(-600) + tsc.stderr.slice(-400))
|
|
136
|
-
|
|
137
|
-
// 4. Runtime load: the fixture module must resolve the packed bytes.
|
|
138
|
-
const load = run(process.execPath, ['--input-type=module', '-e', `
|
|
139
|
-
import { name, inject, apply } from '${join(fixtureDir, 'src', 'index.ts').replaceAll('\\', '/')}'
|
|
140
|
-
if (name !== 'pi-extension-fixture') throw new Error('bad name')
|
|
141
|
-
if (typeof apply !== 'function') throw new Error('bad apply')
|
|
142
|
-
console.log('fixture-module-ok')
|
|
143
|
-
`], { cwd: fixtureDir })
|
|
144
|
-
check('fixture module loads from the packed package', load.status === 0 && load.stdout.includes('fixture-module-ok'),
|
|
145
|
-
load.status === 0 ? '' : load.stderr.slice(0, 400))
|
|
146
|
-
|
|
147
|
-
// 5. The fixture resolves @xmoon76/dsh-pi-tui/extensions through the
|
|
148
|
-
// packed subpath (import resolution check).
|
|
149
|
-
const subpath = run(process.execPath, ['--input-type=module', '-e', `
|
|
150
|
-
const m = await import('@xmoon76/dsh-pi-tui/extensions')
|
|
151
|
-
if (typeof m.PI_TUI_EXTENSIONS_SERVICE !== 'string') throw new Error('bad export')
|
|
152
|
-
console.log('subpath-ok')
|
|
153
|
-
`], { cwd: fixtureDir })
|
|
154
|
-
check('@xmoon76/dsh-pi-tui/extensions subpath imports from the packed package',
|
|
155
|
-
subpath.status === 0 && subpath.stdout.includes('subpath-ok'),
|
|
156
|
-
subpath.status === 0 ? '' : subpath.stderr.slice(0, 400))
|
|
157
|
-
|
|
158
|
-
// 6. No duplicate @deepseek-ai runtime: the packed package declares
|
|
159
|
-
// @deepseek-ai/* as PEER dependencies only (resolved from the dsh
|
|
160
|
-
// install), the TARBALL carries no nested node_modules, the fixture
|
|
161
|
-
// imports no dsh runtime path itself, and the smoke scaffolding's
|
|
162
|
-
// host-module links in the extracted package are SYMLINKS only —
|
|
163
|
-
// never real copies (P2-3: an explicit recursive audit).
|
|
164
|
-
const packedPkg = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'))
|
|
165
|
-
const deepseekDeps = Object.keys(packedPkg.dependencies ?? {})
|
|
166
|
-
.filter(name => name.startsWith('@deepseek-ai'))
|
|
167
|
-
check('packed package has no @deepseek-ai dependency (peers only)', deepseekDeps.length === 0,
|
|
168
|
-
deepseekDeps.length === 0 ? '' : deepseekDeps.join(', '))
|
|
169
|
-
const tarballEntries = run('tar', ['-tzf', tarball]).stdout.split('\n')
|
|
170
|
-
check('tarball carries no nested node_modules',
|
|
171
|
-
!tarballEntries.some(entry => /(^|\/)node_modules\//.test(entry)))
|
|
172
|
-
const fixtureSource = readFileSync(join(fixtureDir, 'src', 'index.ts'), 'utf8')
|
|
173
|
-
// A Cordis plugin imports the cordis Context type (peer); the banned
|
|
174
|
-
// set is the dsh RUNTIME packages (agent/session/...) that would
|
|
175
|
-
// duplicate the host install.
|
|
176
|
-
check('fixture imports no dsh runtime @deepseek-ai path',
|
|
177
|
-
!/@deepseek-ai\/(?!cordis)/.test(fixtureSource))
|
|
178
|
-
// Recursive nested-package audit (P2-3): every @deepseek-ai entry in
|
|
179
|
-
// the EXTRACTED package's scaffolding node_modules must be a symlink
|
|
180
|
-
// to the host install (a real directory would be a duplicate copy).
|
|
181
|
-
const scaffoldingModules = join(pkgDir, 'node_modules')
|
|
182
|
-
let realDeepseekDirs = 0
|
|
183
|
-
const audit = (dir, depth) => {
|
|
184
|
-
if (depth > 4) return
|
|
185
|
-
let entries = []
|
|
186
|
-
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
187
|
-
for (const entry of entries) {
|
|
188
|
-
const full = join(dir, entry.name)
|
|
189
|
-
if (entry.isSymbolicLink()) continue // host links are fine
|
|
190
|
-
if (entry.isDirectory()) {
|
|
191
|
-
if (entry.name.startsWith('@deepseek-ai') || /deepseek/.test(entry.name)) {
|
|
192
|
-
realDeepseekDirs += 1
|
|
193
|
-
}
|
|
194
|
-
if (entry.name !== '.bin') audit(full, depth + 1)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
audit(scaffoldingModules, 0)
|
|
199
|
-
check('extracted-package scaffolding has no REAL @deepseek-ai directory (symlinks only)', realDeepseekDirs === 0,
|
|
200
|
-
realDeepseekDirs === 0 ? '' : `${realDeepseekDirs} real @deepseek-ai director(ies)`)
|
|
201
|
-
} finally {
|
|
202
|
-
rmSync(workDir, { recursive: true, force: true })
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
for (const line of checks) console.log(line)
|
|
206
|
-
if (failures.length > 0) {
|
|
207
|
-
console.error(`\nextension fixture smoke FAILED (${failures.length}):`)
|
|
208
|
-
for (const failure of failures) console.error(` - ${failure}`)
|
|
209
|
-
process.exit(1)
|
|
210
|
-
}
|
|
211
|
-
console.log('\nextension fixture smoke passed')
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
try {
|
|
215
|
-
main()
|
|
216
|
-
} catch (error) {
|
|
217
|
-
console.error(`extension-fixture-smoke: ${error instanceof Error ? error.message : String(error)}`)
|
|
218
|
-
process.exit(1)
|
|
219
|
-
}
|