Address review and cut ceremony: the adapter no longer models help/version/ errors as DshInvocation members. Commander owns those under exitOverride — it prints usage or the diagnostic and one try/catch in parseDshArgs turns the thrown CommanderError into process.exit with the intended code. bin.ts drops its help/version/error cases; the union is the three real modes. Domain checks bail via command.error(print + exit 1): --prompt rejects an empty task or a stray config/--resume, empty --resume= fails loud, and --host/--port are validated. A repeated --resume or a flag captured as a value is Commander's standard behavior, left alone (a bad id fails loud downstream). dsh --help discloses web via addHelpText. Net: args.ts 185 -> 112 lines. Also fixes review nits: built-bin e2e resolves on `close`; the /resume handoff uses `dsh --resume=<id> -- <config>` so a config named `web` stays a positional; and stale prose (cordis.yml comment, app-boot module doc + duplicate JSDoc, ui/README, two feature notes, an agent-loop test name) tracks the shipped state. Removes tui-demo's now-dead plugin-include dep and vendor/loader + app-boot tsconfig references.
56 lines
2.7 KiB
TypeScript
56 lines
2.7 KiB
TypeScript
import { spawn } from 'node:child_process'
|
|
import { existsSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
/**
|
|
* Published-entry smoke for the `dsh` bin: run the built `lib/bin.js` under
|
|
* plain Node (no tsx) with PIPED stdio and assert the TUI refuses to boot.
|
|
* `dsh` is the sole terminal front door; the TUI owns no non-TTY fallback, so a
|
|
* piped launch must exit nonzero with a stderr pointer at the one-shot `-p`
|
|
* mode. The guard fires inside `runTui` BEFORE the Loader resolves the config
|
|
* tree — a compose-time throw inside the tree is logged per-entry, not
|
|
* rethrown, so without this guard a piped launch would settle into an idle
|
|
* UI-less process. The bin resolves its workspace deps through the repo's
|
|
* node_modules, so no external consumer is assembled; missing-config fail-loud
|
|
* and full-boot coverage for the shared dsh-app-boot glue live in cli-demo's
|
|
* built-bin suite, and interactive TTY behavior is PTY-covered by
|
|
* examples/tui-agent. Skips before the bin is built.
|
|
*/
|
|
|
|
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
|
|
const dshBin = join(repoRoot, 'apps/cli/lib/bin.js')
|
|
|
|
/** Run the built bin with PIPED stdio; resolve with output + exit code. */
|
|
function runBuiltBin(): Promise<{ stdout: string; code: number; stderr: string }> {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(process.execPath, [dshBin], { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
let stdout = ''
|
|
let stderr = ''
|
|
child.stdout.setEncoding('utf8')
|
|
child.stdout.on('data', (c: string) => { stdout += c })
|
|
child.stderr.setEncoding('utf8')
|
|
child.stderr.on('data', (c: string) => { stderr += c })
|
|
const timer = setTimeout(() => {
|
|
child.kill('SIGKILL')
|
|
reject(new Error(`dsh built bin did not exit within 25s. stdout:\n${stdout}\nstderr:\n${stderr}`))
|
|
}, 25_000)
|
|
// Resolve on `close` (all stdio drained), not `exit`, so captured output is complete.
|
|
child.on('close', (code) => { clearTimeout(timer); resolve({ stdout, code: code ?? -1, stderr }) })
|
|
child.on('error', (err) => { clearTimeout(timer); reject(err) })
|
|
child.stdin.end()
|
|
})
|
|
}
|
|
|
|
describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', () => {
|
|
it('refuses pipes LOUD (non-zero exit + stderr) before booting the Loader', async () => {
|
|
const { stdout, code, stderr } = await runBuiltBin()
|
|
expect(code).not.toBe(0)
|
|
expect(stderr).toContain('requires stdin and stdout to be interactive TTYs')
|
|
expect(stderr).toContain('dsh -p')
|
|
// The refusal happens before any plugin mounts: stdout stays silent.
|
|
expect(stdout).toBe('')
|
|
}, 30_000)
|
|
})
|