@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.
@@ -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
- }