@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
package/scripts/naming-gate.mjs
DELETED
|
@@ -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,116 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Pre-push gate stage derivation — the single source of truth for what the
|
|
4
|
-
* husky pre-push hook runs stays in package.json's `verify:*` scripts; this
|
|
5
|
-
* module turns one of them into the ordered per-command stage list the hook
|
|
6
|
-
* executes with progress reporting.
|
|
7
|
-
*
|
|
8
|
-
* A script like `a && b && c` yields `['a', 'b', 'c']`. Only top-level `&&`
|
|
9
|
-
* separators are split (a `&&` inside quotes, e.g. inside a node -e snippet,
|
|
10
|
-
* is not a separator). A trailing `&&` (or a `&&` followed by nothing but
|
|
11
|
-
* whitespace) is rejected — `sh -e` treats such a line as a syntax error, so
|
|
12
|
-
* a split would silently drop the intended tail.
|
|
13
|
-
*
|
|
14
|
-
* Supported shell grammar (the parsed script must stay within this subset):
|
|
15
|
-
* - commands chained with `&&` (the split points), each otherwise passed
|
|
16
|
-
* verbatim to `sh -c` for execution — the splitter does NOT interpret
|
|
17
|
-
* the commands themselves;
|
|
18
|
-
* - single- and double-quoted strings anywhere in the script (quotes are
|
|
19
|
-
* tracked so `&&` inside them is not a separator);
|
|
20
|
-
* - backslash escapes inside and outside quotes: an escaped `&&` is NOT
|
|
21
|
-
* treated as a separator (it stays inside the current stage, whose
|
|
22
|
-
* shell semantics are then preserved unchanged by `sh -c`);
|
|
23
|
-
* - anything the command segment may contain (redirects, env assignments,
|
|
24
|
-
* pipes, subshells, `node -e "..."`, ...) is preserved verbatim and
|
|
25
|
-
* executed by `sh -c` unchanged.
|
|
26
|
-
* NOT supported (the splitter rejects them loudly): unterminated quotes,
|
|
27
|
-
* dangling `&&`, empty commands (consecutive `&&`), empty scripts.
|
|
28
|
-
*
|
|
29
|
-
* Usage:
|
|
30
|
-
* node scripts/pre-push-stages.mjs <verify-script-name>
|
|
31
|
-
* # e.g. node scripts/pre-push-stages.mjs verify:prepush
|
|
32
|
-
*
|
|
33
|
-
* @module pre-push-stages
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
import { readFileSync } from 'node:fs'
|
|
37
|
-
import { join, dirname } from 'node:path'
|
|
38
|
-
import { fileURLToPath } from 'node:url'
|
|
39
|
-
|
|
40
|
-
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
41
|
-
|
|
42
|
-
function fail(message) {
|
|
43
|
-
console.error(`pre-push-stages: ${message}`)
|
|
44
|
-
process.exit(1)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const [name] = process.argv.slice(2)
|
|
48
|
-
if (!name || !name.startsWith('verify:')) {
|
|
49
|
-
fail(`usage: node scripts/pre-push-stages.mjs <verify-script-name> (got: ${name ?? 'none'})`)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'))
|
|
53
|
-
const script = pkg.scripts?.[name]
|
|
54
|
-
if (typeof script !== 'string' || script === '') {
|
|
55
|
-
fail(`package.json has no non-empty script "${name}" (update .husky/pre-push)`)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Split on top-level `&&` only: walk the string tracking single-quote,
|
|
59
|
-
// double-quote and backslash escapes; a `&&` seen at depth 0 (no open
|
|
60
|
-
// quote) is a command separator. Escapes are honoured OUTSIDE quotes too
|
|
61
|
-
// (`\&&` is an escaped literal `&`, not a separator), and a script with
|
|
62
|
-
// an unterminated quote is rejected — sh would reject it as well.
|
|
63
|
-
const stages = []
|
|
64
|
-
let current = ''
|
|
65
|
-
let quote = null
|
|
66
|
-
for (let i = 0; i < script.length; i++) {
|
|
67
|
-
const ch = script[i]
|
|
68
|
-
if (quote === '"') {
|
|
69
|
-
if (ch === '\\') { current += ch + (script[i + 1] ?? ''); i++; continue }
|
|
70
|
-
if (ch === '"') quote = null
|
|
71
|
-
current += ch
|
|
72
|
-
continue
|
|
73
|
-
}
|
|
74
|
-
if (quote === "'") {
|
|
75
|
-
if (ch === "'") quote = null
|
|
76
|
-
current += ch
|
|
77
|
-
continue
|
|
78
|
-
}
|
|
79
|
-
if (ch === '\\') {
|
|
80
|
-
// Outside quotes: escape the next character (incl. `&`, so `\&&`
|
|
81
|
-
// stays literal). Keep the backslash in the fragment — it is
|
|
82
|
-
// meaningful to the shell that later runs the stage.
|
|
83
|
-
current += ch + (script[i + 1] ?? '')
|
|
84
|
-
i++
|
|
85
|
-
continue
|
|
86
|
-
}
|
|
87
|
-
if (ch === '"' || ch === "'") {
|
|
88
|
-
quote = ch
|
|
89
|
-
current += ch
|
|
90
|
-
continue
|
|
91
|
-
}
|
|
92
|
-
if (ch === '&' && script[i + 1] === '&') {
|
|
93
|
-
stages.push(current)
|
|
94
|
-
current = ''
|
|
95
|
-
i++
|
|
96
|
-
continue
|
|
97
|
-
}
|
|
98
|
-
current += ch
|
|
99
|
-
}
|
|
100
|
-
stages.push(current)
|
|
101
|
-
|
|
102
|
-
if (quote !== null) {
|
|
103
|
-
fail(`script "${name}" has an unterminated ${quote === '"' ? 'double' : 'single'} quote — sh would reject it; fix package.json`)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const trimmed = stages.map((s) => s.trim())
|
|
107
|
-
if (trimmed[trimmed.length - 1] === '') {
|
|
108
|
-
fail(`script "${name}" ends with a dangling "&&" — sh -e would reject it; fix package.json`)
|
|
109
|
-
}
|
|
110
|
-
for (const [i, stage] of trimmed.entries()) {
|
|
111
|
-
if (stage === '') {
|
|
112
|
-
fail(`script "${name}" has an empty command at position ${i + 1} (consecutive "&&")`)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
process.stdout.write(trimmed.join('\n') + '\n')
|
|
@@ -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)
|