@xmoon76/dsh-pi-tui 0.3.3 → 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,158 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * @xmoon76/dsh-pi-tui/scripts/naming-gate — CI guard for the naming and
4
- * packaging decisions (AGENTS.md "Naming (hard rules)"): nothing here may
5
- * use the `dsh-tui` / `@deepseek-ai/dsh-tui` family, the profile stays
6
- * `pi-tui`, the fork stays private and bundled, and the bundle is the only
7
- * published package with its fork in `deps.onlyBundle`. Fails with a clear
8
- * list when any decision regresses.
9
- * @module naming-gate
10
- */
11
-
12
- import { existsSync, readFileSync, readdirSync } from 'node:fs'
13
- import { spawnSync } from 'node:child_process'
14
- import { join, dirname } from 'node:path'
15
- import { fileURLToPath } from 'node:url'
16
-
17
- // The repository root IS the @xmoon76/dsh-pi-tui package root (root-package
18
- // migration): scripts/ sits directly under it.
19
- const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
20
- const failures = []
21
- const checks = []
22
-
23
- function check(name, ok, detail = '') {
24
- checks.push(`${ok ? 'ok ' : 'FAIL'} ${name}${detail === '' ? '' : ` — ${detail}`}`)
25
- if (!ok) failures.push(`${name}${detail === '' ? '' : `: ${detail}`}`)
26
- }
27
-
28
- function readJson(path) {
29
- return JSON.parse(readFileSync(path, 'utf8'))
30
- }
31
-
32
- const rootPkg = readJson(join(ROOT, 'package.json'))
33
- const forkPkg = readJson(join(ROOT, 'packages', 'pi-tui', 'package.json'))
34
-
35
- // 1. Repository + profile naming: never the reserved `tui` / `dsh-tui` family.
36
- check('root package name is @xmoon76/dsh-pi-tui', rootPkg.name === '@xmoon76/dsh-pi-tui', rootPkg.name)
37
- check('no @deepseek-ai/dsh-tui anywhere', !['@deepseek-ai/dsh-tui', 'dsh-tui'].some(name =>
38
- [rootPkg, forkPkg].some(pkg => pkg.name === name)))
39
- for (const pkg of [rootPkg, forkPkg]) {
40
- const manifest = JSON.stringify(pkg)
41
- if (manifest.includes('@deepseek-ai/dsh-tui') || manifest.includes('"dsh-tui"')) {
42
- check(`no dsh-tui references in ${pkg.name}`, false)
43
- }
44
- }
45
- // The startup service keeps its stable id (internal Loader ids, fine as-is).
46
- const patch = readFileSync(join(ROOT, 'cordis.patch.yml'), 'utf8')
47
- check('patch rows keep tui-startup/tui-app ids', patch.includes('id: tui-startup') && patch.includes('id: tui-app'))
48
- check('startup service keeps TUI_STARTUP_SERVICE',
49
- readFileSync(join(ROOT, 'src', 'startup.ts'), 'utf8').includes("export const TUI_STARTUP_SERVICE = 'tuiStartup'"))
50
-
51
- // 2. Package roles: the fork is private + bundled; the root bundle is public.
52
- check('fork package is private (@xmoon76/pi-tui)', forkPkg.private === true, `private=${String(forkPkg.private)}`)
53
- check('root bundle package is public (@xmoon76/dsh-pi-tui)', rootPkg.private !== true, `private=${String(rootPkg.private)}`)
54
- check('root bundle declares dsh.bundle.patch', rootPkg.dsh?.bundle?.patch !== undefined, rootPkg.dsh?.bundle?.patch)
55
-
56
- // 3. The fork is bundled, never a runtime dependency of the published package.
57
- const tsdown = readFileSync(join(ROOT, 'tsdown.config.ts'), 'utf8')
58
- check('tsdown deps.onlyBundle includes @xmoon76/pi-tui',
59
- tsdown.includes("onlyBundle") && tsdown.includes("'@xmoon76/pi-tui'"))
60
- const runtimeDeps = rootPkg.dependencies ?? {}
61
- check('@xmoon76/pi-tui is NOT a runtime dependency of the bundle',
62
- !(runtimeDeps['@xmoon76/pi-tui'] !== undefined), runtimeDeps['@xmoon76/pi-tui'] ?? 'absent')
63
- const forkDeps = forkPkg.dependencies ?? {}
64
- check('the fork does not depend on the bundle', !(forkDeps['@xmoon76/dsh-pi-tui'] !== undefined))
65
-
66
- // 4. The vendored-version single source of truth stays in the fork manifest.
67
- check('fork repository.note records the vendored commit (single source of truth)',
68
- typeof forkPkg.repository?.note === 'string' && forkPkg.repository.note.length > 20,
69
- forkPkg.repository?.note ?? 'missing')
70
-
71
- // 5. The tarball smoke + pack gate stay wired (stage G).
72
- check('bundle prepack gate exists', (rootPkg.scripts?.prepack ?? '').includes('pnpm build'))
73
- check('bundle postpack smoke exists', (rootPkg.scripts?.postpack ?? '').includes('tarball-smoke'))
74
- check('postpack keeps the pluginization smokes',
75
- ['extension-fixture-smoke', 'vim-plugin-smoke', 'examples-plugin-smoke'].every(name =>
76
- (rootPkg.scripts?.postpack ?? '').includes(name)))
77
-
78
- // 6. The workspace has no stray dist/ or tarballs COMMITTED (dist is
79
- // gitignored by design; the gate checks git tracking, not the tree).
80
- for (const [label, path] of [['root dist', 'dist'], ['packages/pi-tui/dist', 'packages/pi-tui/dist']]) {
81
- const tracked = spawnSync('git', ['ls-files', path], { cwd: ROOT, encoding: 'utf8' }).stdout.trim()
82
- check(`${label} is not committed`, tracked === '', tracked.split('\n').filter(Boolean).slice(0, 3).join(', '))
83
- }
84
- const tgz = spawnSync('git', ['ls-files', '*.tgz'], { cwd: ROOT, encoding: 'utf8' }).stdout.trim()
85
- check('no tarballs committed', tgz === '', tgz.split('\n').filter(Boolean).slice(0, 3).join(', '))
86
-
87
- // 7. Release workflows reference the UN-scoped tarball filename: npm/pnpm
88
- // strip the `@` scope from tgz names (xmoon76-dsh-pi-tui-<v>.tgz), so an
89
- // `@xmoon76-dsh-pi-tui-*.tgz` glob in a workflow matches nothing and the
90
- // gate/upload/publish step fails at runtime. Guard the exact mistake the
91
- // 2026-08-15 pack-gate review found.
92
- for (const workflow of ['ci.yml', 'release.yml']) {
93
- const path = join(ROOT, '.github', 'workflows', workflow)
94
- if (!existsSync(path)) continue // publishing lives inside ci.yml today
95
- const text = readFileSync(path, 'utf8')
96
- check(`workflow ${workflow} uses unscoped tarball globs`,
97
- !text.includes('@xmoon76-dsh-pi-tui-*.tgz') && text.includes('xmoon76-dsh-pi-tui-*.tgz'))
98
- }
99
-
100
- // 8. The bundle's dsh dependency contract is declared in ONE place and the
101
- // gate enforces it against the actual source (AGENTS.md rule 7): every
102
- // `@deepseek-ai/*` package imported by `src/` (value OR type) must be a
103
- // peerDependency, and every peerDependency must be imported by `src/`.
104
- // A peer that only appears in a comment or a test is not a runtime
105
- // contract; an import with no peer breaks the in-box resolution rule and
106
- // can duplicate `@deepseek-ai` copies in the profile (first tool call
107
- // crashes). `dsh-session-query` was removed by this rule: the picker
108
- // types it structurally (src/sessions.ts) and reads the service off the
109
- // live context, so it never needed the package.
110
- const bundleSrc = join(ROOT, 'src')
111
- const srcFiles = (() => {
112
- const out = []
113
- const walk = (dir) => {
114
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
115
- const path = join(dir, entry.name)
116
- if (entry.isDirectory()) walk(path)
117
- else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) out.push(path)
118
- }
119
- }
120
- walk(bundleSrc)
121
- return out
122
- })()
123
- const srcImports = new Set()
124
- for (const file of srcFiles) {
125
- const text = readFileSync(file, 'utf8')
126
- for (const match of text.matchAll(/from\s+['"]@deepseek-ai\/([^'"\/]+)/g)) {
127
- srcImports.add(`@deepseek-ai/${match[1]}`)
128
- }
129
- // Runtime service access via ctx.get('name') must stay structural: a
130
- // package that is ONLY read this way (never imported) has no place in
131
- // the peer list — same rule as dsh-session-query.
132
- }
133
- const peers = new Set(Object.keys(rootPkg.peerDependencies ?? {}))
134
- for (const imported of [...srcImports].sort()) {
135
- check(`peer declared for src import ${imported}`, peers.has(imported), peers.has(imported) ? '' : 'missing from peerDependencies')
136
- }
137
- for (const peer of [...peers].sort()) {
138
- if (!peer.startsWith('@deepseek-ai/')) continue // chalk etc. are regular deps
139
- check(`peer ${peer} is imported by src/`, srcImports.has(peer), srcImports.has(peer) ? '' : 'no import in src/ (dead peer)')
140
- }
141
-
142
- // 9. The M0–M11 pluginization contract survives the root-package migration:
143
- // the public SDK subpaths and their tsdown entries must all stay present.
144
- const exportsMap = rootPkg.exports ?? {}
145
- for (const subpath of ['./extensions', './extensions/advanced', './extensions/unstable', './builtins']) {
146
- check(`exports keep ${subpath}`, exportsMap[subpath] !== undefined)
147
- }
148
- for (const entry of ['src/extensions.ts', 'src/builtins.ts', 'src/extension/advanced.ts', 'src/extension/unstable.ts']) {
149
- check(`tsdown entry keeps ${entry}`, tsdown.includes(entry))
150
- }
151
-
152
- for (const line of checks) console.log(line)
153
- if (failures.length > 0) {
154
- console.error(`\nnaming-gate FAILED (${failures.length}):`)
155
- for (const failure of failures) console.error(` - ${failure}`)
156
- process.exit(1)
157
- }
158
- console.log('\nnaming-gate passed')
@@ -1,318 +0,0 @@
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()
@@ -1,62 +0,0 @@
1
- /**
2
- * Visual preview of the editor `❯` prompt + the transcript user-role
3
- * styling — run inside a real TTY (tmux):
4
- *
5
- * env -u NO_COLOR node --import tsx/esm scripts/preview-prompt.mts
6
- *
7
- * Shows a transcript mixing user / assistant / thinking / system rows and
8
- * alternates the editor between an EMPTY draft (the `❯ ` prompt with the
9
- * cursor block) and a multi-line draft (the prompt leads the first line,
10
- * continuation lines indent under it, exactly like the transcript rows).
11
- * `PREVIEW_HOLD_MS` controls how long it stays up (default 30s); Ctrl+C
12
- * also exits.
13
- * @module @xmoon76/dsh-pi-tui/preview-prompt
14
- */
15
-
16
- import { ProcessTerminal } from '@xmoon76/pi-tui'
17
- import { TuiApp } from '../src/tui-app.ts'
18
-
19
- const holdMs = Number(process.env.PREVIEW_HOLD_MS ?? 30000)
20
-
21
- const terminal = new ProcessTerminal()
22
- const app = new TuiApp(terminal, {
23
- onSubmit: () => {},
24
- onExit: () => {
25
- app.stop()
26
- process.exit(0)
27
- },
28
- })
29
- app.start()
30
-
31
- // Transcript contrast: the user row is a brand-blue bubble (whole-row
32
- // background + blue ❯), everything else (assistant / thinking / system)
33
- // keeps its own distinct style.
34
- app.setTranscript([
35
- {
36
- kind: 'user',
37
- turn: 0,
38
- // Long enough to wrap: the continuation must indent under the ❯.
39
- text: 'The startup fails with ENOTFOUND near line 42 of src/app.ts — I suspect an environment variable is missing, so please check the config load order and compare it against the .env.example keys and the docs.',
40
- },
41
- {
42
- kind: 'assistant',
43
- turn: 0,
44
- text: 'Let me check. **Config load order**:\n\n1. `.env` file\n2. Environment overrides\n3. Default fallbacks\n\n`ENOTFOUND` usually means DNS resolution failed — confirm whether you reach an internal registry or the public one.',
45
- },
46
- { kind: 'thinking', turn: 0, text: 'ENOTFOUND is a DNS issue; verify the config source before concluding.', running: false },
47
- { kind: 'system', turn: 0, label: 'AGENTS.md', text: 'Repo config conventions live in docs/config.md; env keys come from the preset.', summary: 'context rules' },
48
- ])
49
-
50
- // Alternate the draft so both the empty prompt and the multi-line prompt
51
- // (continuation indent) are visible without touching the keyboard.
52
- let draftVisible = false
53
- const flip = setInterval(() => {
54
- draftVisible = !draftVisible
55
- app.setDraft(draftVisible ? 'git status\npnpm test -- --filter @xmoon76/pi-tui' : '')
56
- }, 6000)
57
-
58
- setTimeout(() => {
59
- clearInterval(flip)
60
- app.stop()
61
- process.exit(0)
62
- }, holdMs)
@@ -1,97 +0,0 @@
1
- /**
2
- * Side-by-side preview of user-role visual schemes — repo code untouched,
3
- * pure display. Run in a real TTY (tmux):
4
- *
5
- * env -u NO_COLOR node --import tsx/esm scripts/preview-role-styles.mts
6
- *
7
- * Shows the chosen scheme — background bubble with a brand-blue ❯ (dsh-web
8
- * parity) — in both the dark palette and the light palette, next to the
9
- * old kimi-style amber text for contrast, plus the editor-prompt colours.
10
- * Ctrl+C exits.
11
- * @module @xmoon76/dsh-pi-tui/preview-role-styles
12
- */
13
-
14
- import { Chalk } from 'chalk'
15
- import {
16
- ProcessTerminal, Text, TuiMainScreen, matchesKey,
17
- visibleWidth, wrapTextWithAnsi, type TuiInputListenerResult,
18
- } from '@xmoon76/pi-tui'
19
-
20
- const chalk = new Chalk({ level: 3 })
21
- const dim = (s: string): string => chalk.hex('#888888')(s)
22
- const darkBlue = (s: string): string => chalk.bold.hex('#679EFE')(s)
23
- const darkBubbleBg = (s: string): string => chalk.bgHex('#2C2C2F')(s)
24
- const lightBlue = (s: string): string => chalk.bold.hex('#4177E6')(s)
25
- const lightBubbleBg = (s: string): string => chalk.bgHex('#E4EDFD')(s)
26
- const amber = (s: string): string => chalk.bold.hex('#FFCB6B')(s)
27
- const border = (s: string): string => chalk.hex('#5A5A5A')(s)
28
-
29
- const W = 100
30
- const MSG_ONE = 'Check the config load order — ENOTFOUND at src/app.ts:42, likely a missing env var.'
31
- const MSG_TWO = 'Also verify the .env.example keys against the docs, since the preset loader reads them in sequence.'
32
-
33
- /**
34
- * Wrap `text` to `width - 2` cells. The first row carries the plain `❯ `
35
- * marker, continuation rows indent 2 cells under it; every row is padded
36
- * to the full inner width so a background paint covers the whole row.
37
- */
38
- function wrapped(width: number, text: string): string[] {
39
- const inner = Math.max(1, width - 2)
40
- return wrapTextWithAnsi(text, inner).map((line, index) => {
41
- const prefix = index === 0 ? '❯ ' : ' '
42
- return prefix + line + ' '.repeat(Math.max(0, inner - visibleWidth(line)))
43
- })
44
- }
45
-
46
- /** Bubble rows: full-row background, coloured ❯, body in the terminal default. */
47
- function bubble(width: number, text: string, bg: (s: string) => string, bullet: (s: string) => string): string[] {
48
- return wrapped(width, text).map((row, index) => {
49
- if (index === 0 && row.startsWith('❯ ')) {
50
- return bg(bullet('❯') + ' ' + row.slice(2))
51
- }
52
- return bg(row)
53
- })
54
- }
55
-
56
- /** Old kimi look: the whole row (❯ + body + padding) in one amber paint. */
57
- function amberRows(width: number, text: string): string[] {
58
- return wrapped(width, text).map(amber)
59
- }
60
-
61
- const blocks: string[] = [
62
- dim('dsh-pi-tui · user-role style preview (repo untouched) — Ctrl+C to exit'),
63
- '',
64
- dim('dark · bubble + brand-blue ❯ (chosen scheme)'),
65
- ...bubble(W, MSG_ONE, darkBubbleBg, darkBlue),
66
- ...bubble(W, MSG_TWO, darkBubbleBg, darkBlue),
67
- '',
68
- dim('light · bubble + deep brand-blue ❯ (light palette)'),
69
- ...bubble(W, MSG_ONE, lightBubbleBg, lightBlue),
70
- ...bubble(W, MSG_TWO, lightBubbleBg, lightBlue),
71
- '',
72
- dim('kimi amber text (rejected — kept for contrast)'),
73
- ...amberRows(W, MSG_ONE),
74
- ...amberRows(W, MSG_TWO),
75
- '',
76
- dim('editor prompt candidates:'),
77
- border('─'.repeat(W)),
78
- ` ${darkBlue('❯')} git status ${lightBlue('❯')} git status ${amber('❯')} git status`,
79
- border('─'.repeat(W)),
80
- ]
81
-
82
- // Pad every line to the exact width so nothing wraps.
83
- const preview = blocks
84
- .map(line => line + ' '.repeat(Math.max(0, W - visibleWidth(line))))
85
- .join('\n')
86
-
87
- const terminal = new ProcessTerminal()
88
- const tui = new TuiMainScreen(terminal)
89
- tui.addChild(new Text(preview, 0, 0))
90
- tui.addInputListener((data): TuiInputListenerResult => {
91
- if (matchesKey(data, 'ctrl+c')) {
92
- tui.stop()
93
- process.exit(0)
94
- }
95
- return undefined
96
- })
97
- tui.start()