@xmoon76/dsh-pi-tui 0.3.4 → 0.3.6
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 +655 -0
- package/README.md +563 -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-Bk66X6NR.mjs} +16568 -2976
- package/package.json +14 -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,300 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* unstable-plugin-smoke — verify the PACKED UNSTABLE extension surface end
|
|
4
|
-
* to end with a real third-party-shaped plugin (Phase 3, plan §17):
|
|
5
|
-
*
|
|
6
|
-
* 1. the fixture imports ONLY the public `@xmoon76/dsh-pi-tui/
|
|
7
|
-
* extensions/unstable` (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 UNSTABLE surface: UNSTABLE_API_LEVEL === 1,
|
|
15
|
-
* raw capture (observe/consume/rewrite), the low-level surface
|
|
16
|
-
* handle and the mount lease;
|
|
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/unstable-plugin-smoke.mjs [path-to-tgz]
|
|
25
|
-
* @module unstable-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', 'unstable-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(), 'unstable-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/unstable.mjs', existsSync(join(pkgDir, 'dist', 'extension', 'unstable.mjs')))
|
|
90
|
-
check('packed package has dist/extension/unstable.d.mts', existsSync(join(pkgDir, 'dist', 'extension', 'unstable.d.mts')))
|
|
91
|
-
|
|
92
|
-
// 2. IMPORT BAN (plan §17 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/unstable + 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/unstable')
|
|
137
|
-
&& dshImports.includes('@xmoon76/dsh-pi-tui/extensions'),
|
|
138
|
-
dshImports.join(', '))
|
|
139
|
-
|
|
140
|
-
// 3. UNSTABLE-SURFACE EXERCISE (plan §17 acceptance): the fixture
|
|
141
|
-
// exercises the Phase-3 unstable surface through the public entry.
|
|
142
|
-
const surface = [
|
|
143
|
-
['UNSTABLE_API_LEVEL', 'tier level constant'],
|
|
144
|
-
['unstable(', 'facade builder'],
|
|
145
|
-
['ui.input.captureRaw', 'raw input capture'],
|
|
146
|
-
["mode: 'observe'", 'observe mode'],
|
|
147
|
-
["mode: 'capture'", 'capture mode'],
|
|
148
|
-
["action: 'consume'", 'consume decision'],
|
|
149
|
-
["action: 'rewrite'", 'rewrite decision'],
|
|
150
|
-
['ui.surface.handle', 'low-level surface handle'],
|
|
151
|
-
['mountComponent', 'low-level mount'],
|
|
152
|
-
]
|
|
153
|
-
for (const [api, label] of surface) {
|
|
154
|
-
check(`fixture exercises ${label} (${api})`, fixtureSrc.includes(api))
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// 4. The fixture tree: symlink the packed package + the host's
|
|
158
|
-
// node_modules (the offline-safe mirror of a real profile install).
|
|
159
|
-
const fixtureDir = join(workDir, 'fixture')
|
|
160
|
-
mkdirSync(join(fixtureDir, 'node_modules', '@xmoon76'), { recursive: true })
|
|
161
|
-
symlinkSync(pkgDir, join(fixtureDir, 'node_modules', '@xmoon76', 'dsh-pi-tui'), 'dir')
|
|
162
|
-
const hostModules = join(PACKAGE_ROOT, 'node_modules')
|
|
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: 'dsh-pi-unstable-fixture',
|
|
191
|
-
private: true,
|
|
192
|
-
type: 'module',
|
|
193
|
-
}, null, 2))
|
|
194
|
-
|
|
195
|
-
// 5. Typecheck against the packed .d.mts (the repo's tsc — no
|
|
196
|
-
// registry fetch).
|
|
197
|
-
const tsc = run('node', [join(PACKAGE_ROOT, 'node_modules', 'typescript', 'bin', 'tsc'), '-p', join(fixtureDir, 'tsconfig.json')], { cwd: fixtureDir })
|
|
198
|
-
check('fixture typechecks against the packed .d.mts', tsc.status === 0,
|
|
199
|
-
tsc.status === 0 ? '' : (tsc.stdout + tsc.stderr).split('\n').slice(-6).join(' '))
|
|
200
|
-
|
|
201
|
-
// 6. Module shape.
|
|
202
|
-
const moduleShape = /export const name = ['"][^'"]+['"]/s.test(fixtureSrc)
|
|
203
|
-
&& /export const inject = \[/s.test(fixtureSrc)
|
|
204
|
-
&& /export function apply\(ctx: Context\)/s.test(fixtureSrc)
|
|
205
|
-
check('fixture module shape (name/inject/apply) matches the Loader', moduleShape)
|
|
206
|
-
|
|
207
|
-
// 7. RUNTIME LOAD: compile the fixture to JS and apply() it with a
|
|
208
|
-
// mock service — the registrations must not throw and must resolve
|
|
209
|
-
// the PACKED unstable module.
|
|
210
|
-
const emitDir = join(fixtureDir, 'dist-out')
|
|
211
|
-
const emit = run('node', [join(PACKAGE_ROOT, 'node_modules', 'typescript', 'bin', 'tsc'),
|
|
212
|
-
'-p', join(fixtureDir, 'tsconfig.json'), '--noEmit', 'false', '--outDir', emitDir], { cwd: fixtureDir })
|
|
213
|
-
check('fixture compiles to JS for the runtime probe', emit.status === 0,
|
|
214
|
-
emit.status === 0 ? '' : (emit.stdout + emit.stderr).split('\n').slice(-4).join(' '))
|
|
215
|
-
if (emit.status === 0) {
|
|
216
|
-
writeFileSync(join(fixtureDir, 'runtime-probe.mjs'), `
|
|
217
|
-
const context = {
|
|
218
|
-
get: (name) => name === 'piTuiExtensions' ? mockService() : undefined,
|
|
219
|
-
on: () => {},
|
|
220
|
-
}
|
|
221
|
-
function mockService() {
|
|
222
|
-
const inertLease = {
|
|
223
|
-
id: 'inert', active: false, focused: false,
|
|
224
|
-
focus: () => {}, blur: () => {}, invalidate: () => {},
|
|
225
|
-
close: () => {}, hide: () => {}, show: () => {},
|
|
226
|
-
}
|
|
227
|
-
const inertHandle = {
|
|
228
|
-
surfaceId: 'inert', generation: 0, width: 0, height: 0,
|
|
229
|
-
requestRender: () => {},
|
|
230
|
-
mountComponent: () => inertLease,
|
|
231
|
-
}
|
|
232
|
-
return {
|
|
233
|
-
api: () => ({
|
|
234
|
-
apiVersion: 1,
|
|
235
|
-
hostVersion: 'test',
|
|
236
|
-
capabilities: new Set(['unstable.input.raw', 'unstable.surface.handle']),
|
|
237
|
-
deprecations: new Map(),
|
|
238
|
-
}),
|
|
239
|
-
_unstableCaptureRaw: (spec) => ({ id: spec.id, dispose: () => {} }),
|
|
240
|
-
_unstableSurfaceHandle: () => inertHandle,
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
const plugin = await import(${JSON.stringify(join(emitDir, 'index.js').replaceAll('\\', '/'))})
|
|
244
|
-
plugin.apply(context)
|
|
245
|
-
if (plugin.name !== 'dsh-pi-unstable-fixture') throw new Error('bad name: ' + plugin.name)
|
|
246
|
-
console.log('fixture-runtime-ok')
|
|
247
|
-
`)
|
|
248
|
-
const runtime = run(process.execPath, [join(fixtureDir, 'runtime-probe.mjs')], { cwd: fixtureDir })
|
|
249
|
-
check('fixture runtime-loads and apply() registers without throwing',
|
|
250
|
-
runtime.status === 0 && runtime.stdout.includes('fixture-runtime-ok'),
|
|
251
|
-
runtime.status === 0 ? '' : (runtime.stderr || runtime.stdout).split('\n').slice(-8).join(' '))
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// 8. No duplicate dsh runtime: every @deepseek-ai entry in BOTH the
|
|
255
|
-
// packed package tree AND the fixture tree must be a SYMLINK whose
|
|
256
|
-
// realpath resolves into the host module tree (the packed tarball
|
|
257
|
-
// itself carries no node_modules — the scaffolding symlinks them,
|
|
258
|
-
// exactly like a real profile resolves in-box packages from the
|
|
259
|
-
// dsh installation). A REAL directory is a duplicate runtime copy.
|
|
260
|
-
const probeReal = resolve(workDir)
|
|
261
|
-
const auditTrees = [
|
|
262
|
-
['packed package', join(pkgDir, 'node_modules', '@deepseek-ai')],
|
|
263
|
-
['fixture tree', join(fixtureDir, 'node_modules', '@deepseek-ai')],
|
|
264
|
-
]
|
|
265
|
-
const realDirs = []
|
|
266
|
-
for (const [label, nm] of auditTrees) {
|
|
267
|
-
const entries = existsSync(nm) ? readdirSync(nm) : []
|
|
268
|
-
for (const name of entries) {
|
|
269
|
-
const entry = join(nm, name)
|
|
270
|
-
let isSymlink = false
|
|
271
|
-
try { isSymlink = lstatSync(entry).isSymbolicLink() } catch { /* missing */ }
|
|
272
|
-
if (!isSymlink) {
|
|
273
|
-
realDirs.push(`${label}:${name} (real dir)`)
|
|
274
|
-
continue
|
|
275
|
-
}
|
|
276
|
-
try {
|
|
277
|
-
const target = resolve(realpathSync(entry))
|
|
278
|
-
if (target.startsWith(probeReal)) realDirs.push(`${label}:${name}->${target}`)
|
|
279
|
-
} catch {
|
|
280
|
-
realDirs.push(`${label}:${name} (dangling)`)
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
check('no duplicate @deepseek-ai dsh-runtime copy (symlinks only, both trees)',
|
|
285
|
-
realDirs.length === 0,
|
|
286
|
-
realDirs.join(', '))
|
|
287
|
-
} finally {
|
|
288
|
-
if (process.env.UNSTABLE_SMOKE_KEEP !== '1') rmSync(workDir, { recursive: true, force: true })
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
for (const line of checks) console.log(line)
|
|
292
|
-
if (failures.length > 0) {
|
|
293
|
-
console.error(`\nunstable-plugin smoke FAILED (${failures.length}):`)
|
|
294
|
-
for (const failure of failures) console.error(` - ${failure}`)
|
|
295
|
-
process.exit(1)
|
|
296
|
-
}
|
|
297
|
-
console.log('\nunstable-plugin smoke passed')
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
main()
|
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* vim-plugin-smoke — verify the M10 vim acceptance plugin against the
|
|
4
|
-
* PACKED `@xmoon76/dsh-pi-tui` tarball (plan §15):
|
|
5
|
-
*
|
|
6
|
-
* 1. the vim fixture imports ONLY the public `@xmoon76/dsh-pi-tui/
|
|
7
|
-
* extensions` subpath — importing `@xmoon76/pi-tui`, `src/tui-app`
|
|
8
|
-
* or any repository-relative internal path FAILS the gate (static
|
|
9
|
-
* imports, dynamic import() AND require() are all scanned);
|
|
10
|
-
* 2. the fixture typechecks against the packed `.d.mts`;
|
|
11
|
-
* 3. the fixture COMPILES to JS and RUNTIME-LOADS against the packed
|
|
12
|
-
* bytes (apply() registers through a mock service without throwing);
|
|
13
|
-
* 4. the fixture exercises the EDITOR-EXTENSION SEAM (replacement
|
|
14
|
-
* registration + semantic EditorInputEvent handling + modal state);
|
|
15
|
-
* the remaining public surfaces (commands, themes, settings,
|
|
16
|
-
* autocomplete, keybindings, renderers, overlays, widgets) are each
|
|
17
|
-
* covered by their own dedicated tests — this gate is NOT a
|
|
18
|
-
* Stable-completeness proof;
|
|
19
|
-
* 5. the fixture's module shape (name/inject/apply) matches the Loader;
|
|
20
|
-
* 6. no duplicate dsh runtime in the fixture tree.
|
|
21
|
-
*
|
|
22
|
-
* The probe mirrors the host install with SYMLINKS (no npm install — the
|
|
23
|
-
* gate is offline-safe; the packed package's runtime deps resolve from
|
|
24
|
-
* the repo's node_modules exactly like a real profile).
|
|
25
|
-
*
|
|
26
|
-
* Usage: node scripts/vim-plugin-smoke.mjs [path-to-tgz]
|
|
27
|
-
* @module vim-plugin-smoke
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import { spawnSync } from 'node:child_process'
|
|
31
|
-
import {
|
|
32
|
-
existsSync,
|
|
33
|
-
mkdirSync,
|
|
34
|
-
mkdtempSync,
|
|
35
|
-
readdirSync,
|
|
36
|
-
readFileSync,
|
|
37
|
-
realpathSync,
|
|
38
|
-
rmSync,
|
|
39
|
-
statSync,
|
|
40
|
-
lstatSync,
|
|
41
|
-
symlinkSync,
|
|
42
|
-
writeFileSync,
|
|
43
|
-
} from 'node:fs'
|
|
44
|
-
import { tmpdir } from 'node:os'
|
|
45
|
-
import { dirname, join, resolve } from 'node:path'
|
|
46
|
-
import { fileURLToPath } from 'node:url'
|
|
47
|
-
|
|
48
|
-
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
|
|
49
|
-
const PACKAGE_ROOT = join(SCRIPT_DIR, '..')
|
|
50
|
-
const FIXTURE_ROOT = join(PACKAGE_ROOT, 'test', 'fixtures', 'vim-plugin')
|
|
51
|
-
|
|
52
|
-
const failures = []
|
|
53
|
-
const checks = []
|
|
54
|
-
|
|
55
|
-
function check(name, ok, detail = '') {
|
|
56
|
-
checks.push(`${ok ? 'ok ' : 'FAIL'} ${name}${detail === '' ? '' : ` — ${detail}`}`)
|
|
57
|
-
if (!ok) failures.push(`${name}${detail === '' ? '' : `: ${detail}`}`)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function run(command, args, options = {}) {
|
|
61
|
-
return spawnSync(command, args, { encoding: 'utf8', ...options })
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function resolveTarball(explicit) {
|
|
65
|
-
if (explicit !== undefined) {
|
|
66
|
-
const absolute = resolve(explicit)
|
|
67
|
-
if (!existsSync(absolute)) throw new Error(`tarball not found: ${explicit}`)
|
|
68
|
-
return absolute
|
|
69
|
-
}
|
|
70
|
-
const dir = PACKAGE_ROOT
|
|
71
|
-
const candidates = readdirSync(dir)
|
|
72
|
-
.filter(name => /xmoon76-dsh-pi-tui-.*\.tgz$/.test(name))
|
|
73
|
-
.map(name => join(dir, name))
|
|
74
|
-
.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)
|
|
75
|
-
if (candidates.length === 0) {
|
|
76
|
-
throw new Error(`no xmoon76-dsh-pi-tui-*.tgz in ${dir}; run pnpm pack:release first`)
|
|
77
|
-
}
|
|
78
|
-
return candidates[0]
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function main() {
|
|
82
|
-
const tarball = resolveTarball(process.argv[2])
|
|
83
|
-
const workDir = mkdtempSync(join(tmpdir(), 'vim-plugin-'))
|
|
84
|
-
try {
|
|
85
|
-
// 1. Extract the packed package.
|
|
86
|
-
const extractedDir = join(workDir, 'pkg')
|
|
87
|
-
mkdirSync(extractedDir)
|
|
88
|
-
const extract = run('tar', ['-xzf', tarball, '-C', extractedDir])
|
|
89
|
-
check('tarball extracts', extract.status === 0, extract.stderr)
|
|
90
|
-
const pkgDir = join(extractedDir, 'package')
|
|
91
|
-
check('packed package has dist/extensions.mjs', existsSync(join(pkgDir, 'dist', 'extensions.mjs')))
|
|
92
|
-
check('packed package has dist/extensions.d.mts', existsSync(join(pkgDir, 'dist', 'extensions.d.mts')))
|
|
93
|
-
|
|
94
|
-
// 2. IMPORT BAN (plan §15 CI gate): the fixture's IMPORT STATEMENTS
|
|
95
|
-
// (multi-line blocks), dynamic import() and require() specifiers
|
|
96
|
-
// must never reference private internals (prose may name them —
|
|
97
|
-
// the gate is about what the plugin can IMPORT).
|
|
98
|
-
const fixtureSrc = readFileSync(join(FIXTURE_ROOT, 'src', 'index.ts'), 'utf8')
|
|
99
|
-
const lines = fixtureSrc.split('\n')
|
|
100
|
-
const importStatements = []
|
|
101
|
-
for (let index = 0; index < lines.length; index++) {
|
|
102
|
-
if (/^\s*import\b/.test(lines[index] ?? '')) {
|
|
103
|
-
let statement = lines[index] ?? ''
|
|
104
|
-
while (!statement.includes(';') && index + 1 < lines.length) {
|
|
105
|
-
index += 1
|
|
106
|
-
statement += lines[index]
|
|
107
|
-
}
|
|
108
|
-
importStatements.push(statement)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
const importLines = importStatements.join('\n')
|
|
112
|
-
const dynamicSpecifiers = [...fixtureSrc.matchAll(/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g)]
|
|
113
|
-
.map(match => match[1])
|
|
114
|
-
.concat([...fixtureSrc.matchAll(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g)]
|
|
115
|
-
.map(match => match[1]))
|
|
116
|
-
const banned = [
|
|
117
|
-
// Word-boundary: `@xmoon76/pi-tui` must NOT match
|
|
118
|
-
// `@xmoon76/dsh-pi-tui` (a substring of the public package name).
|
|
119
|
-
['@xmoon76/pi-tui (the private fork, word-bounded)', /@xmoon76\/pi-tui(?![-a-z])/g],
|
|
120
|
-
['@moonshot-ai/pi-tui', /@moonshot-ai\/pi-tui/g],
|
|
121
|
-
['src/tui-app', /src\/tui-app/g],
|
|
122
|
-
['TuiApp', /\bTuiApp\b/g],
|
|
123
|
-
['TuiMainScreen', /\bTuiMainScreen\b/g],
|
|
124
|
-
['TuiAltScreen', /\bTuiAltScreen\b/g],
|
|
125
|
-
['repository-relative internal path', /\.\.\/\.\.\/src/g],
|
|
126
|
-
]
|
|
127
|
-
for (const [name, pattern] of banned) {
|
|
128
|
-
const staticMatches = importLines.match(pattern) ?? []
|
|
129
|
-
const dynamicMatches = dynamicSpecifiers.filter(specifier => pattern.test(specifier))
|
|
130
|
-
check(`fixture does not import ${name}`, staticMatches.length === 0 && dynamicMatches.length === 0,
|
|
131
|
-
[...staticMatches, ...dynamicMatches].join(', '))
|
|
132
|
-
}
|
|
133
|
-
// The ONLY @xmoon76/dsh-pi-tui imports are the extensions subpath
|
|
134
|
-
// (cordis is a legitimate peer for the plugin's apply signature).
|
|
135
|
-
const dshImports = importLines.match(/@xmoon76\/dsh-pi-tui[^'"]*/g) ?? []
|
|
136
|
-
check('fixture imports ONLY the public extensions subpath',
|
|
137
|
-
dshImports.length === 1 && dshImports[0] === '@xmoon76/dsh-pi-tui/extensions',
|
|
138
|
-
dshImports.join(', '))
|
|
139
|
-
|
|
140
|
-
// 3. EDITOR-SEAM SURFACE (plan §15 acceptance): the editor seam
|
|
141
|
-
// exercised through the vim fixture. The checks below validate the
|
|
142
|
-
// editor-extension seam, NOT a Stable-completeness proof: each
|
|
143
|
-
// remaining public surface (commands, themes, settings,
|
|
144
|
-
// autocomplete, keybindings, renderers, overlays, widgets) has its
|
|
145
|
-
// own dedicated tests (plan §8) — this gate must not grow into a
|
|
146
|
-
// FULL-SDK proof.
|
|
147
|
-
const surface = [
|
|
148
|
-
['registerEditor', 'editor replacement'],
|
|
149
|
-
['handleInput', 'semantic editor input channel'],
|
|
150
|
-
['EditorInputEvent', 'host-normalized event shape'],
|
|
151
|
-
['create:', 'editor create() protocol member'],
|
|
152
|
-
['dispose:', 'editor dispose() protocol member'],
|
|
153
|
-
]
|
|
154
|
-
for (const [api, label] of surface) {
|
|
155
|
-
check(`fixture exercises ${label} (${api})`, fixtureSrc.includes(api))
|
|
156
|
-
}
|
|
157
|
-
// The fixture must consume the semantic handleInput(event) channel
|
|
158
|
-
// (EditorInputEvent) — the editor-extension seam. Modal-mode behavior
|
|
159
|
-
// (insert/normal) is NOT part of the Stable contract (plan §7): the
|
|
160
|
-
// fixture keeps its mini modal editor, but CI must not define Vim
|
|
161
|
-
// behavior as a Stable API contract.
|
|
162
|
-
check('fixture never parses raw terminal bytes (no CSI-u / escape matching in the plugin)',
|
|
163
|
-
!/\\x1b\[/.test(fixtureSrc) && !/escape\s*\(/.test(fixtureSrc.replace(/handleInput/g, '')))
|
|
164
|
-
|
|
165
|
-
// 4. The fixture tree: symlink the packed package + the host's
|
|
166
|
-
// node_modules (the offline-safe mirror of a real profile install —
|
|
167
|
-
// the packed tarball itself carries no node_modules).
|
|
168
|
-
const fixtureDir = join(workDir, 'fixture')
|
|
169
|
-
mkdirSync(join(fixtureDir, 'node_modules', '@xmoon76'), { recursive: true })
|
|
170
|
-
symlinkSync(pkgDir, join(fixtureDir, 'node_modules', '@xmoon76', 'dsh-pi-tui'), 'dir')
|
|
171
|
-
const hostModules = join(PACKAGE_ROOT, 'node_modules')
|
|
172
|
-
mkdirSync(join(pkgDir, 'node_modules'), { recursive: true })
|
|
173
|
-
for (const entry of readdirSync(hostModules)) {
|
|
174
|
-
const target = join(pkgDir, 'node_modules', entry)
|
|
175
|
-
if (existsSync(target)) continue
|
|
176
|
-
symlinkSync(join(hostModules, entry), target, 'dir')
|
|
177
|
-
}
|
|
178
|
-
// The fixture ALSO imports @deepseek-ai/cordis directly (the apply
|
|
179
|
-
// Context type) — mirror the host modules into the fixture tree too.
|
|
180
|
-
mkdirSync(join(fixtureDir, 'node_modules'), { recursive: true })
|
|
181
|
-
for (const entry of readdirSync(hostModules)) {
|
|
182
|
-
const target = join(fixtureDir, 'node_modules', entry)
|
|
183
|
-
if (existsSync(target)) continue
|
|
184
|
-
symlinkSync(join(hostModules, entry), target, 'dir')
|
|
185
|
-
}
|
|
186
|
-
mkdirSync(join(fixtureDir, 'src'), { recursive: true })
|
|
187
|
-
writeFileSync(join(fixtureDir, 'src', 'index.ts'), fixtureSrc)
|
|
188
|
-
writeFileSync(join(fixtureDir, 'tsconfig.json'), JSON.stringify({
|
|
189
|
-
compilerOptions: {
|
|
190
|
-
target: 'ES2022',
|
|
191
|
-
module: 'NodeNext',
|
|
192
|
-
moduleResolution: 'NodeNext',
|
|
193
|
-
strict: true,
|
|
194
|
-
noEmit: true,
|
|
195
|
-
skipLibCheck: true,
|
|
196
|
-
types: [],
|
|
197
|
-
},
|
|
198
|
-
include: ['src'],
|
|
199
|
-
}, null, 2))
|
|
200
|
-
writeFileSync(join(fixtureDir, 'package.json'), JSON.stringify({
|
|
201
|
-
name: 'dsh-pi-vim-fixture',
|
|
202
|
-
private: true,
|
|
203
|
-
type: 'module',
|
|
204
|
-
}, null, 2))
|
|
205
|
-
|
|
206
|
-
// 5. Typecheck against the packed .d.mts (the repo's tsc — no
|
|
207
|
-
// registry fetch).
|
|
208
|
-
const tsc = run('node', [join(PACKAGE_ROOT, 'node_modules', 'typescript', 'bin', 'tsc'), '-p', join(fixtureDir, 'tsconfig.json')], { cwd: fixtureDir })
|
|
209
|
-
check('fixture typechecks against the packed .d.mts', tsc.status === 0,
|
|
210
|
-
tsc.status === 0 ? '' : (tsc.stdout + tsc.stderr).split('\n').slice(-6).join(' '))
|
|
211
|
-
|
|
212
|
-
// 6. Module shape.
|
|
213
|
-
const moduleShape = /export const name = ['"][^'"]+['"]/s.test(fixtureSrc)
|
|
214
|
-
&& /export const inject = \[/s.test(fixtureSrc)
|
|
215
|
-
&& /export function apply\(ctx: Context\)/s.test(fixtureSrc)
|
|
216
|
-
check('fixture module shape (name/inject/apply) matches the Loader', moduleShape)
|
|
217
|
-
|
|
218
|
-
// 7. RUNTIME LOAD (round-1 finding 2): compile the fixture to JS and
|
|
219
|
-
// apply() it with a mock service — the registrations must not
|
|
220
|
-
// throw and must resolve the PACKED extensions module.
|
|
221
|
-
const emitDir = join(fixtureDir, 'dist-out')
|
|
222
|
-
const emit = run('node', [join(PACKAGE_ROOT, 'node_modules', 'typescript', 'bin', 'tsc'),
|
|
223
|
-
'-p', join(fixtureDir, 'tsconfig.json'), '--noEmit', 'false', '--outDir', emitDir], { cwd: fixtureDir })
|
|
224
|
-
check('fixture compiles to JS for the runtime probe', emit.status === 0,
|
|
225
|
-
emit.status === 0 ? '' : (emit.stdout + emit.stderr).split('\n').slice(-4).join(' '))
|
|
226
|
-
if (emit.status === 0) {
|
|
227
|
-
writeFileSync(join(fixtureDir, 'runtime-probe.mjs'), `
|
|
228
|
-
const context = {
|
|
229
|
-
get: (name) => name === 'piTuiExtensions' ? mockService() : undefined,
|
|
230
|
-
on: () => {},
|
|
231
|
-
}
|
|
232
|
-
function mockService() {
|
|
233
|
-
const makeHandle = () => ({ id: 'x', invalidate: () => {}, replace: () => {}, dispose: () => {} })
|
|
234
|
-
return {
|
|
235
|
-
api: () => ({ apiVersion: 1, hostVersion: 'test', capabilities: new Set(['slot.input.widget', 'slot.input.dock.item', 'slot.chrome.header.badge', 'slot.chrome.footer.status']) }),
|
|
236
|
-
register: () => makeHandle(),
|
|
237
|
-
registerCommand: () => makeHandle(),
|
|
238
|
-
registerTheme: () => makeHandle(),
|
|
239
|
-
registerAutocomplete: () => makeHandle(),
|
|
240
|
-
registerSetting: () => makeHandle(),
|
|
241
|
-
registerKeybinding: () => makeHandle(),
|
|
242
|
-
registerMessageRenderer: () => makeHandle(),
|
|
243
|
-
registerToolRenderer: () => makeHandle(),
|
|
244
|
-
registerEditor: () => makeHandle(),
|
|
245
|
-
showOverlay: () => ({ close: () => {}, hide: () => {}, show: () => {} }),
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
const plugin = await import(${JSON.stringify(join(emitDir, 'index.js').replaceAll('\\', '/'))})
|
|
249
|
-
plugin.apply(context)
|
|
250
|
-
if (plugin.name !== 'dsh-pi-vim-fixture') throw new Error('bad name: ' + plugin.name)
|
|
251
|
-
console.log('fixture-runtime-ok')
|
|
252
|
-
`)
|
|
253
|
-
const runtime = run(process.execPath, [join(fixtureDir, 'runtime-probe.mjs')], { cwd: fixtureDir })
|
|
254
|
-
check('fixture runtime-loads and apply() registers without throwing',
|
|
255
|
-
runtime.status === 0 && runtime.stdout.includes('fixture-runtime-ok'),
|
|
256
|
-
runtime.status === 0 ? '' : (runtime.stderr || runtime.stdout).split('\n').slice(-8).join(' '))
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// 8. No duplicate dsh runtime: every @deepseek-ai entry in BOTH the
|
|
260
|
-
// packed package tree AND the fixture tree must be a SYMLINK whose
|
|
261
|
-
// realpath resolves into the host module tree (the packed tarball
|
|
262
|
-
// itself carries no node_modules — the scaffolding symlinks them,
|
|
263
|
-
// exactly like a real profile resolves in-box packages from the
|
|
264
|
-
// dsh installation). A REAL directory is a duplicate runtime copy.
|
|
265
|
-
// The symlinks point into the HOST's modules (pnpm's virtual store
|
|
266
|
-
// under node_modules/.pnpm, itself linking to the pnpm store — a
|
|
267
|
-
// REAL host install). The defect class is a COPY: a real directory,
|
|
268
|
-
// or a symlink resolving back INTO the packed/fixture tree (a
|
|
269
|
-
// self-contained duplicate). A symlink resolving OUTSIDE the probe
|
|
270
|
-
// (host node_modules / pnpm store) is exactly the host resolution a
|
|
271
|
-
// real profile uses.
|
|
272
|
-
const probeReal = resolve(workDir)
|
|
273
|
-
const auditTrees = [
|
|
274
|
-
['packed package', join(pkgDir, 'node_modules', '@deepseek-ai')],
|
|
275
|
-
['fixture tree', join(fixtureDir, 'node_modules', '@deepseek-ai')],
|
|
276
|
-
]
|
|
277
|
-
const realDirs = []
|
|
278
|
-
for (const [label, nm] of auditTrees) {
|
|
279
|
-
const entries = existsSync(nm) ? readdirSync(nm) : []
|
|
280
|
-
for (const name of entries) {
|
|
281
|
-
const entry = join(nm, name)
|
|
282
|
-
let isSymlink = false
|
|
283
|
-
try { isSymlink = lstatSync(entry).isSymbolicLink() } catch { /* missing */ }
|
|
284
|
-
if (!isSymlink) {
|
|
285
|
-
realDirs.push(`${label}:${name} (real dir)`)
|
|
286
|
-
continue
|
|
287
|
-
}
|
|
288
|
-
// A symlink whose target resolves INSIDE the probe is a duplicate
|
|
289
|
-
// copy; a dangling link is a defect. Anything else (host modules,
|
|
290
|
-
// the pnpm store) is the intended host resolution.
|
|
291
|
-
try {
|
|
292
|
-
const target = resolve(realpathSync(entry))
|
|
293
|
-
if (target.startsWith(probeReal)) realDirs.push(`${label}:${name}->${target}`)
|
|
294
|
-
} catch {
|
|
295
|
-
realDirs.push(`${label}:${name} (dangling)`)
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
check('no duplicate @deepseek-ai dsh-runtime copy (symlinks only, both trees)',
|
|
300
|
-
realDirs.length === 0,
|
|
301
|
-
realDirs.join(', '))
|
|
302
|
-
} finally {
|
|
303
|
-
if (process.env.VIM_SMOKE_KEEP !== '1') rmSync(workDir, { recursive: true, force: true })
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
for (const line of checks) console.log(line)
|
|
307
|
-
if (failures.length > 0) {
|
|
308
|
-
console.error(`\nvim-plugin smoke FAILED (${failures.length}):`)
|
|
309
|
-
for (const failure of failures) console.error(` - ${failure}`)
|
|
310
|
-
process.exit(1)
|
|
311
|
-
}
|
|
312
|
-
console.log('\nvim-plugin smoke passed')
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
main()
|
|
File without changes
|