@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,318 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * phase4-plugin-smoke — verify the PACKED Phase-4 advanced surface end
4
+ * to end with a real third-party-shaped plugin (Phase 4, plan §17):
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 Phase-4 surface: the imperative UI
15
+ * broker (select/confirm/input/notify), custom interactive UI
16
+ * (ui.custom), and the host-state facade (theme/title/working/
17
+ * tools-expanded);
18
+ * 5. the fixture's module shape (name/inject/apply) matches the Loader;
19
+ * 6. no duplicate dsh runtime in the fixture tree.
20
+ *
21
+ * The probe mirrors the host install with SYMLINKS (no npm install — the
22
+ * gate is offline-safe; the packed package's runtime deps resolve from
23
+ * the repo's node_modules exactly like a real profile).
24
+ *
25
+ * Usage: node scripts/phase4-plugin-smoke.mjs [path-to-tgz]
26
+ * @module phase4-plugin-smoke
27
+ */
28
+
29
+ import { spawnSync } from 'node:child_process'
30
+ import {
31
+ existsSync,
32
+ mkdirSync,
33
+ mkdtempSync,
34
+ readdirSync,
35
+ readFileSync,
36
+ realpathSync,
37
+ rmSync,
38
+ statSync,
39
+ lstatSync,
40
+ symlinkSync,
41
+ writeFileSync,
42
+ } from 'node:fs'
43
+ import { tmpdir } from 'node:os'
44
+ import { dirname, join, resolve } from 'node:path'
45
+ import { fileURLToPath } from 'node:url'
46
+
47
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
48
+ const PACKAGE_ROOT = join(SCRIPT_DIR, '..')
49
+ const FIXTURE_ROOT = join(PACKAGE_ROOT, 'test', 'fixtures', 'phase4-plugin')
50
+
51
+ const failures = []
52
+ const checks = []
53
+
54
+ function check(name, ok, detail = '') {
55
+ checks.push(`${ok ? 'ok ' : 'FAIL'} ${name}${detail === '' ? '' : ` — ${detail}`}`)
56
+ if (!ok) failures.push(`${name}${detail === '' ? '' : `: ${detail}`}`)
57
+ }
58
+
59
+ function run(command, args, options = {}) {
60
+ return spawnSync(command, args, { encoding: 'utf8', ...options })
61
+ }
62
+
63
+ function resolveTarball(explicit) {
64
+ if (explicit !== undefined) {
65
+ const absolute = resolve(explicit)
66
+ if (!existsSync(absolute)) throw new Error(`tarball not found: ${explicit}`)
67
+ return absolute
68
+ }
69
+ const dir = PACKAGE_ROOT
70
+ const candidates = readdirSync(dir)
71
+ .filter(name => /xmoon76-dsh-pi-tui-.*\.tgz$/.test(name))
72
+ .map(name => join(dir, name))
73
+ .sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)
74
+ if (candidates.length === 0) {
75
+ throw new Error(`no xmoon76-dsh-pi-tui-*.tgz in ${dir}; run pnpm pack:release first`)
76
+ }
77
+ return candidates[0]
78
+ }
79
+
80
+ function main() {
81
+ const tarball = resolveTarball(process.argv[2])
82
+ const workDir = mkdtempSync(join(tmpdir(), 'phase4-plugin-'))
83
+ try {
84
+ // 1. Extract the packed package.
85
+ const extractedDir = join(workDir, 'pkg')
86
+ mkdirSync(extractedDir)
87
+ const extract = run('tar', ['-xzf', tarball, '-C', extractedDir])
88
+ check('tarball extracts', extract.status === 0, extract.stderr)
89
+ const pkgDir = join(extractedDir, 'package')
90
+ check('packed package has dist/extension/advanced.mjs', existsSync(join(pkgDir, 'dist', 'extension', 'advanced.mjs')))
91
+ check('packed package has dist/extension/advanced.d.mts', existsSync(join(pkgDir, 'dist', 'extension', 'advanced.d.mts')))
92
+
93
+ // 2. IMPORT BAN (plan §15 CI gate): the fixture's IMPORT STATEMENTS
94
+ // (multi-line blocks), dynamic import() and require() specifiers
95
+ // must never reference private internals.
96
+ const fixtureSrc = readFileSync(join(FIXTURE_ROOT, 'src', 'index.ts'), 'utf8')
97
+ const lines = fixtureSrc.split('\n')
98
+ const importStatements = []
99
+ for (let index = 0; index < lines.length; index++) {
100
+ if (/^\s*import\b/.test(lines[index] ?? '')) {
101
+ let statement = lines[index] ?? ''
102
+ while (!statement.includes(';') && index + 1 < lines.length) {
103
+ index += 1
104
+ statement += lines[index]
105
+ }
106
+ importStatements.push(statement)
107
+ }
108
+ }
109
+ const importLines = importStatements.join('\n')
110
+ const dynamicSpecifiers = [...fixtureSrc.matchAll(/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g)]
111
+ .map(match => match[1])
112
+ .concat([...fixtureSrc.matchAll(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g)]
113
+ .map(match => match[1]))
114
+ const banned = [
115
+ // Word-boundary: `@xmoon76/pi-tui` must NOT match
116
+ // `@xmoon76/dsh-pi-tui` (a substring of the public package name).
117
+ ['@xmoon76/pi-tui (the private fork, word-bounded)', /@xmoon76\/pi-tui(?![-a-z])/g],
118
+ ['@moonshot-ai/pi-tui', /@moonshot-ai\/pi-tui/g],
119
+ ['src/tui-app', /src\/tui-app/g],
120
+ ['TuiApp', /\bTuiApp\b/g],
121
+ ['TuiMainScreen', /\bTuiMainScreen\b/g],
122
+ ['TuiAltScreen', /\bTuiAltScreen\b/g],
123
+ ['repository-relative internal path', /\.\.\/\.\.\/src/g],
124
+ ]
125
+ for (const [name, pattern] of banned) {
126
+ const staticMatches = importLines.match(pattern) ?? []
127
+ const dynamicMatches = dynamicSpecifiers.filter(specifier => pattern.test(specifier))
128
+ check(`fixture does not import ${name}`, staticMatches.length === 0 && dynamicMatches.length === 0,
129
+ [...staticMatches, ...dynamicMatches].join(', '))
130
+ }
131
+ // The ONLY @xmoon76/dsh-pi-tui imports are the public subpaths
132
+ // (extensions/advanced + extensions for the service type; cordis is a
133
+ // legitimate peer for the plugin's apply signature).
134
+ const dshImports = importLines.match(/@xmoon76\/dsh-pi-tui[^'"]*/g) ?? []
135
+ check('fixture imports ONLY the public extensions subpaths',
136
+ dshImports.length === 2
137
+ && dshImports.includes('@xmoon76/dsh-pi-tui/extensions/advanced')
138
+ && dshImports.includes('@xmoon76/dsh-pi-tui/extensions'),
139
+ dshImports.join(', '))
140
+
141
+ // 3. PHASE-4 SURFACE EXERCISE (plan §17 acceptance): the fixture
142
+ // exercises the Phase-4 advanced surface through the public entry.
143
+ const surface = [
144
+ ['ui.ui.select', 'imperative select broker'],
145
+ ['ui.ui.confirm', 'imperative confirm broker'],
146
+ ['ui.ui.input', 'imperative input broker'],
147
+ ['ui.ui.notify', 'imperative notify broker'],
148
+ ['ui.ui.custom', 'custom interactive UI'],
149
+ ['AdvancedCustomHost', 'custom host facade type'],
150
+ ['ui.host.getTheme', 'host-state theme query'],
151
+ ['ui.host.setTheme', 'host-state theme select'],
152
+ ['ui.host.setTitle', 'host-state title override'],
153
+ ['ui.host.setWorkingMessage', 'host-state working override'],
154
+ ['ui.host.setToolsExpanded', 'host-state tool expansion'],
155
+ ]
156
+ for (const [api, label] of surface) {
157
+ check(`fixture exercises ${label} (${api})`, fixtureSrc.includes(api))
158
+ }
159
+ check('fixture never parses raw terminal bytes (no CSI-u / escape matching in the plugin)',
160
+ !/\\x1b\[/.test(fixtureSrc) && !/escape\s*\(/.test(fixtureSrc.replace(/handleInput/g, '')))
161
+
162
+ // 4. The fixture tree: symlink the packed package + the host's
163
+ // node_modules (the offline-safe mirror of a real profile install).
164
+ const fixtureDir = join(workDir, 'fixture')
165
+ mkdirSync(join(fixtureDir, 'node_modules', '@xmoon76'), { recursive: true })
166
+ symlinkSync(pkgDir, join(fixtureDir, 'node_modules', '@xmoon76', 'dsh-pi-tui'), 'dir')
167
+ const hostModules = join(PACKAGE_ROOT, 'node_modules')
168
+ mkdirSync(join(pkgDir, 'node_modules'), { recursive: true })
169
+ for (const entry of readdirSync(hostModules)) {
170
+ const target = join(pkgDir, 'node_modules', entry)
171
+ if (existsSync(target)) continue
172
+ symlinkSync(join(hostModules, entry), target, 'dir')
173
+ }
174
+ mkdirSync(join(fixtureDir, 'node_modules'), { recursive: true })
175
+ for (const entry of readdirSync(hostModules)) {
176
+ const target = join(fixtureDir, 'node_modules', entry)
177
+ if (existsSync(target)) continue
178
+ symlinkSync(join(hostModules, entry), target, 'dir')
179
+ }
180
+ mkdirSync(join(fixtureDir, 'src'), { recursive: true })
181
+ writeFileSync(join(fixtureDir, 'src', 'index.ts'), fixtureSrc)
182
+ writeFileSync(join(fixtureDir, 'tsconfig.json'), JSON.stringify({
183
+ compilerOptions: {
184
+ target: 'ES2022',
185
+ module: 'NodeNext',
186
+ moduleResolution: 'NodeNext',
187
+ strict: true,
188
+ noEmit: true,
189
+ skipLibCheck: true,
190
+ types: [],
191
+ },
192
+ include: ['src'],
193
+ }, null, 2))
194
+ writeFileSync(join(fixtureDir, 'package.json'), JSON.stringify({
195
+ name: 'dsh-pi-phase4-fixture',
196
+ private: true,
197
+ type: 'module',
198
+ }, null, 2))
199
+
200
+ // 5. Typecheck against the packed .d.mts (the repo's tsc — no
201
+ // registry fetch).
202
+ const tsc = run('node', [join(PACKAGE_ROOT, 'node_modules', 'typescript', 'bin', 'tsc'), '-p', join(fixtureDir, 'tsconfig.json')], { cwd: fixtureDir })
203
+ check('fixture typechecks against the packed .d.mts', tsc.status === 0,
204
+ tsc.status === 0 ? '' : (tsc.stdout + tsc.stderr).split('\n').slice(-6).join(' '))
205
+
206
+ // 6. Module shape.
207
+ const moduleShape = /export const name = ['"][^'"]+['"]/s.test(fixtureSrc)
208
+ && /export const inject = \[/s.test(fixtureSrc)
209
+ && /export function apply\(ctx: Context\)/s.test(fixtureSrc)
210
+ check('fixture module shape (name/inject/apply) matches the Loader', moduleShape)
211
+
212
+ // 7. RUNTIME LOAD: compile the fixture to JS and apply() it with a
213
+ // mock service — the registrations must not throw and must resolve
214
+ // the PACKED advanced module.
215
+ const emitDir = join(fixtureDir, 'dist-out')
216
+ const emit = run('node', [join(PACKAGE_ROOT, 'node_modules', 'typescript', 'bin', 'tsc'),
217
+ '-p', join(fixtureDir, 'tsconfig.json'), '--noEmit', 'false', '--outDir', emitDir], { cwd: fixtureDir })
218
+ check('fixture compiles to JS for the runtime probe', emit.status === 0,
219
+ emit.status === 0 ? '' : (emit.stdout + emit.stderr).split('\n').slice(-4).join(' '))
220
+ if (emit.status === 0) {
221
+ writeFileSync(join(fixtureDir, 'runtime-probe.mjs'), `
222
+ const context = {
223
+ get: (name) => name === 'piTuiExtensions' ? mockService() : undefined,
224
+ on: () => {},
225
+ }
226
+ function mockService() {
227
+ const inertLease = {
228
+ id: 'inert', active: false, focused: false,
229
+ focus: () => {}, blur: () => {}, invalidate: () => {},
230
+ close: () => {}, hide: () => {}, show: () => {},
231
+ }
232
+ const inertControls = {
233
+ getEditorState: () => ({ text: '', cursor: 0, focused: false, composing: false }),
234
+ setEditorText: () => {}, setEditorCursor: () => {},
235
+ insertEditorText: () => {}, pasteToEditor: () => {}, requestEditorFocus: () => {},
236
+ }
237
+ return {
238
+ api: () => ({
239
+ apiVersion: 1,
240
+ hostVersion: 'test',
241
+ capabilities: new Set(['advanced.input.capture', 'advanced.ui.interactive', 'advanced.editor.control']),
242
+ deprecations: new Map(),
243
+ }),
244
+ _advancedCaptureInput: (spec) => ({ id: spec.id, dispose: () => {} }),
245
+ _advancedShowInteractiveOverlay: () => inertLease,
246
+ _advancedEditorControls: () => inertControls,
247
+ _advancedUiSelect: () => Promise.resolve(undefined),
248
+ _advancedUiConfirm: () => Promise.resolve(false),
249
+ _advancedUiInput: () => Promise.resolve(undefined),
250
+ _advancedUiNotify: () => {},
251
+ _advancedUiCustom: () => Promise.resolve(undefined),
252
+ _advancedHostState: () => ({
253
+ getTheme: () => 'dark',
254
+ setTheme: () => {},
255
+ setTitle: () => {},
256
+ setWorkingMessage: () => {},
257
+ setToolsExpanded: () => {},
258
+ }),
259
+ }
260
+ }
261
+ const plugin = await import(${JSON.stringify(join(emitDir, 'index.js').replaceAll('\\', '/'))})
262
+ plugin.apply(context)
263
+ if (plugin.name !== 'dsh-pi-phase4-fixture') throw new Error('bad name: ' + plugin.name)
264
+ console.log('fixture-runtime-ok')
265
+ `)
266
+ const runtime = run(process.execPath, [join(fixtureDir, 'runtime-probe.mjs')], { cwd: fixtureDir })
267
+ check('fixture runtime-loads and apply() registers without throwing',
268
+ runtime.status === 0 && runtime.stdout.includes('fixture-runtime-ok'),
269
+ runtime.status === 0 ? '' : (runtime.stderr || runtime.stdout).split('\n').slice(-8).join(' '))
270
+ }
271
+
272
+ // 8. No duplicate dsh runtime: every @deepseek-ai entry in BOTH the
273
+ // packed package tree AND the fixture tree must be a SYMLINK whose
274
+ // realpath resolves into the host module tree (the packed tarball
275
+ // itself carries no node_modules — the scaffolding symlinks them,
276
+ // exactly like a real profile resolves in-box packages from the
277
+ // dsh installation). A REAL directory is a duplicate runtime copy.
278
+ const probeReal = resolve(workDir)
279
+ const auditTrees = [
280
+ ['packed package', join(pkgDir, 'node_modules', '@deepseek-ai')],
281
+ ['fixture tree', join(fixtureDir, 'node_modules', '@deepseek-ai')],
282
+ ]
283
+ const realDirs = []
284
+ for (const [label, nm] of auditTrees) {
285
+ const entries = existsSync(nm) ? readdirSync(nm) : []
286
+ for (const name of entries) {
287
+ const entry = join(nm, name)
288
+ let isSymlink = false
289
+ try { isSymlink = lstatSync(entry).isSymbolicLink() } catch { /* missing */ }
290
+ if (!isSymlink) {
291
+ realDirs.push(`${label}:${name} (real dir)`)
292
+ continue
293
+ }
294
+ try {
295
+ const target = resolve(realpathSync(entry))
296
+ if (target.startsWith(probeReal)) realDirs.push(`${label}:${name}->${target}`)
297
+ } catch {
298
+ realDirs.push(`${label}:${name} (dangling)`)
299
+ }
300
+ }
301
+ }
302
+ check('no duplicate @deepseek-ai dsh-runtime copy (symlinks only, both trees)',
303
+ realDirs.length === 0,
304
+ realDirs.join(', '))
305
+ } finally {
306
+ if (process.env.ADVANCED_SMOKE_KEEP !== '1') rmSync(workDir, { recursive: true, force: true })
307
+ }
308
+
309
+ for (const line of checks) console.log(line)
310
+ if (failures.length > 0) {
311
+ console.error(`\nphase4-plugin smoke FAILED (${failures.length}):`)
312
+ for (const failure of failures) console.error(` - ${failure}`)
313
+ process.exit(1)
314
+ }
315
+ console.log('\nphase4-plugin smoke passed')
316
+ }
317
+
318
+ main()
@@ -149,10 +149,12 @@ function main() {
149
149
  // --- structure ---
150
150
  const files = walkFiles(extracted)
151
151
  const has = (path) => files.includes(path)
152
- check('exports entry dist/index.mjs', has('dist/index.mjs'))
153
- check('exports entry dist/startup.mjs', has('dist/startup.mjs'))
154
- check('types dist/index.d.mts', has('dist/index.d.mts'))
155
- check('types dist/startup.d.mts', has('dist/startup.d.mts'))
152
+ check('exports entry dist/extension/advanced.mjs', has('dist/extension/advanced.mjs'))
153
+ check('exports entry dist/extension/unstable.mjs', has('dist/extension/unstable.mjs'))
154
+ check('types dist/extension/advanced.d.mts', has('dist/extension/advanced.d.mts'))
155
+ check('types dist/extension/unstable.d.mts', has('dist/extension/unstable.d.mts'))
156
+ check('exports entry dist/builtins.mjs', has('dist/builtins.mjs'))
157
+ check('types dist/builtins.d.mts', has('dist/builtins.d.mts'))
156
158
  check('cordis.patch.yml included', has('cordis.patch.yml'))
157
159
  check('config/ included', files.some(name => name.startsWith('config/')))
158
160
  check('repair-session.mjs included', has('scripts/repair-session.mjs'))
@@ -225,6 +227,62 @@ function main() {
225
227
  checks.push(`skip .d.mts parse — typescript unavailable`)
226
228
  }
227
229
 
230
+ // --- public .d.mts leak gate (M3/E, round-1/2 review): the shipped
231
+ // declarations must not reference the vendored fork, TuiApp, private
232
+ // screens, or repository source paths. tsdown INLINES types into the
233
+ // .d.mts with `//#region <path>` markers; a fork leak shows up as a
234
+ // region pointing OUTSIDE the package's own src/ (`../pi-tui/...`,
235
+ // `node_modules/@xmoon76/pi-tui/...`) or as fork module names
236
+ // (`src/tui-main-screen.d.ts`), often inside an auto-generated SHARED
237
+ // CHUNK that the SDK entries only re-export. The gate therefore:
238
+ // (a) scans EVERY dist/*.d.mts (entries AND chunks);
239
+ // (b) treats any region marker whose path is not this package's own
240
+ // src/ as a leak (allowlist by construction);
241
+ // (c) bans fork imports and fork module names anywhere;
242
+ // (d) bans the bare private identifiers (TuiMainScreen/TuiAltScreen/
243
+ // TuiApp) line-scoped over every file, skipping comment lines.
244
+ const dtsAll = files.filter(name => name.startsWith('dist/') && name.endsWith('.d.mts'))
245
+ const sdkIdentifiers = [/\bTuiMainScreen\b/, /\bTuiAltScreen\b/, /\bTuiApp\b/]
246
+ const dtsLeaks = []
247
+ for (const name of dtsAll) {
248
+ const text = readFileSync(join(extracted, name), 'utf8')
249
+ const lines = text.split('\n')
250
+ // (b) region markers: allow only `//#region src/<public module>.d.ts`.
251
+ // The ROOT entry (dist/index.d.mts) is the runner's own surface: it
252
+ // legitimately inlines the TuiApp implementation (src/tui-app.d.ts)
253
+ // for the Loader row — it is NOT a third-party SDK entry. Its region
254
+ // is exempted; everything else (chunks, extensions, builtins) must
255
+ // name only the public modules below.
256
+ const isRoot = name === 'dist/index.d.mts'
257
+ for (const line of lines) {
258
+ const match = /\/\/#region\s+(\S+)/.exec(line)
259
+ if (match) {
260
+ const regionPath = match[1]
261
+ const allowed = /^src\/(builtins|commands|diag|extension\/advanced|extension\/advanced-types|extension\/public-types|extension\/service|extension\/slot-map|extension\/unstable|extension\/unstable-types|extensions|index|skill-catalog|startup|surface-catalog)\.d\.ts$/.test(regionPath)
262
+ const rootAllowed = isRoot && /^src\/tui-app\.d\.ts$/.test(regionPath)
263
+ if (!allowed && !rootAllowed) dtsLeaks.push(`${name}: region ${regionPath}`)
264
+ }
265
+ }
266
+ // (c) fork imports and fork module names anywhere.
267
+ if (/from\s*["']@xmoon76\/pi-tui["']/.test(text)) dtsLeaks.push(`${name}: fork import`)
268
+ if (/packages\/pi-tui/.test(text)) dtsLeaks.push(`${name}: packages/pi-tui path`)
269
+ if (/\b(tui-main-screen|tui-alt-screen)\.d\.ts\b/.test(text)) dtsLeaks.push(`${name}: fork screen module`)
270
+ // (d) bare private identifiers, line-scoped (comments skipped). No
271
+ // exemption: the clean dist has zero TuiApp identifiers anywhere
272
+ // (round-3 finding 1 — the old root exemption was dead code).
273
+ for (const line of lines) {
274
+ const trimmed = line.trim()
275
+ if (trimmed.startsWith('*') || trimmed.startsWith('//') || trimmed.startsWith('/*')) continue
276
+ for (const pattern of sdkIdentifiers) {
277
+ if (pattern.test(trimmed)) dtsLeaks.push(`${name}: bare ${pattern}`)
278
+ }
279
+ }
280
+ }
281
+ check('every public .d.mts leaks no private pi-tui / internal paths', dtsLeaks.length === 0,
282
+ dtsLeaks.length === 0 ? '' : dtsLeaks.join('; '))
283
+ // The .mjs bodies must not import the fork externally either (it is
284
+ // bundled) — the existing dist check below covers every .mjs.
285
+
228
286
  // --- install + runtime ---
229
287
  if (skipInstall) {
230
288
  checks.push('skip npm install (TARBALL_SMOKE_SKIP_INSTALL=1)')
@@ -242,10 +300,14 @@ function main() {
242
300
  const installed = join(probeDir, 'node_modules', '@xmoon76', 'dsh-pi-tui')
243
301
  check('installed package contains dist', existsSync(join(installed, 'dist', 'index.mjs')))
244
302
  const importRun = run(process.execPath, ['--input-type=module', '-e',
245
- "Promise.all([import('@xmoon76/dsh-pi-tui'), import('@xmoon76/dsh-pi-tui/startup')])"
246
- + ".then(m => console.log('imports-ok')).catch(e => { console.error(e.message); process.exit(1) })",
303
+ "Promise.all([import('@xmoon76/dsh-pi-tui'), import('@xmoon76/dsh-pi-tui/startup'),"
304
+ + "import('@xmoon76/dsh-pi-tui/extensions'), import('@xmoon76/dsh-pi-tui/builtins'),"
305
+ + "import('@xmoon76/dsh-pi-tui/extensions/advanced'), import('@xmoon76/dsh-pi-tui/extensions/unstable')])"
306
+ + ".then(m => { if (m[4].ADVANCED_API_LEVEL !== 1) throw new Error('ADVANCED_API_LEVEL');"
307
+ + "if (m[5].UNSTABLE_API_LEVEL !== 1) throw new Error('UNSTABLE_API_LEVEL'); console.log('imports-ok') })"
308
+ + ".catch(e => { console.error(e.message); process.exit(1) })",
247
309
  ], { cwd: probeDir })
248
- check('both exports entries import', importRun.status === 0 && importRun.stdout.includes('imports-ok'),
310
+ check('all exports entries import', importRun.status === 0 && importRun.stdout.includes('imports-ok'),
249
311
  importRun.status === 0 ? '' : importRun.stderr.slice(0, 200))
250
312
  const forkRun = run(process.execPath, ['--input-type=module', '-e',
251
313
  "import('@xmoon76/pi-tui').then(() => process.exit(0)).catch(() => process.exit(3))",