refactor(cli): make dsh the sole terminal front door, drop RESUME_SESSION_ID

Remove the redundant dsh-tui-demo bin and the RESUME_SESSION_ID environment
variable, leaving dsh as the one terminal entrypoint.

The dsh-tui-demo package was a plugin (the TUI app bundle mounted by dsh's
config) plus a bin that booted a leaf cordis.yml — the same job `dsh [config]`
does. The bin, its ./bin export, its built-bin.e2e.ts, the tsdown bin entry,
and the now-unused dsh-app-boot dependency are removed; the package keeps its
plugin and invariant. demo:cordis, demo:code-mode, and the tui-agent and
cordis-agent keyless PTY smokes now launch through apps/cli/src/bin.ts with the
config as the positional argument. cli-demo/acp-demo/jsonrpc-demo keep their
bins (distinct surfaces).

RESUME_SESSION_ID was the only bridge from --resume into the shipped config;
--resume now provides the id on the boot context via ctx.provide(
RESUME_SESSION_ID_KEY, id), and the four configs read it as a bare identifier
through a quoted typeof-guarded !!js expression. The TUI resumeCommand fixtures
and docs move to `dsh --resume {session}`.

Agent Note and its Chinese pair updated; config-catalog regenerated.
This commit is contained in:
Turtle
2026-07-25 12:43:59 +08:00
parent 6cd139a25b
commit 870fb1cafa
28 changed files with 76 additions and 189 deletions

View File

@@ -5,12 +5,12 @@ Pre-composed plugin bundles a thin leaf `cordis.yml` loads instead of assembling
| Package | npm name | Role |
|---|---|---|
| `agent-spine-demo/` | `@deepseek-ai/dsh-agent-spine-demo` | The executor-less/UI-less agent spine as one bundle plugin, with fallback session titles and an opt-in persisted-goal stack |
| `tui-demo/` | `@deepseek-ai/dsh-tui-demo` | Full-screen terminal app: the spine + persisted goals + `/goal` command + JSONL persistence + `dsh-tui` + a pre-created `main` agent, with a boot `bin` |
| `tui-demo/` | `@deepseek-ai/dsh-tui-demo` | Full-screen terminal app bundle: the spine + persisted goals + `/goal` command + JSONL persistence + `dsh-tui` + a pre-created `main` agent; no bin, booted by the [`dsh`](../../apps/cli/README.md) CLI |
| `cli-demo/` | `@deepseek-ai/dsh-cli-demo` | Headless one-shot app: the spine + JSONL persistence + a pre-created `main` agent, with text and DSH-native JSON output |
| `acp-demo/` | `@deepseek-ai/dsh-acp-demo` | ACP server app: the spine + persisted goals + `/goal` command + JSONL persistence + the [`acp`](../ui/acp/README.md) bridge (no stdout logger), with a boot `bin` |
| `jsonrpc-demo/` | `@deepseek-ai/dsh-jsonrpc-demo` | Bin-only runtime that boots an external `cordis.yml` for the stdio JSON-RPC SDK client |
`agent-spine-demo` is the shared bundle; `tui-demo`, `cli-demo`, and `acp-demo` compose it with full-screen terminal, headless one-shot, and ACP front doors and own their boot bins. `jsonrpc-demo` mounts no composition of its own — it boots whatever tree the deployment's `cordis.yml` names, and is what the Python SDK runtime launches.
`agent-spine-demo` is the shared bundle; `tui-demo`, `cli-demo`, and `acp-demo` compose it with full-screen terminal, headless one-shot, and ACP front doors. `cli-demo` and `acp-demo` own their boot bins; `tui-demo` ships only the bundle plugin, and the product [`dsh`](../../apps/cli/README.md) CLI is its terminal front door. `jsonrpc-demo` mounts no composition of its own — it boots whatever tree the deployment's `cordis.yml` names, and is what the Python SDK runtime launches.
These are **not** product API. The spine pieces they bundle live in [`core/`](../core/README.md), the bridges/channels/boot-glue in [`ui/`](../ui/README.md), and the swappable backends (LLM adapter, bash executor) in their capability groups; a demo bundle just picks one concrete composition of them. Swap or fork one freely.

View File

@@ -1,8 +1,8 @@
# @deepseek-ai/dsh-tui-demo
The full-screen terminal app: a Cordis plugin that composes [`@deepseek-ai/dsh-agent-spine-demo`](../agent-spine-demo/README.md), persisted same-session goals, the human-command registry and `/goal` producer, JSONL persistence, keyboard-backed user interaction, a pre-created `main` agent, and [`@deepseek-ai/dsh-tui`](../../ui/tui/README.md). Its `bin` boots a leaf `cordis.yml`.
The full-screen terminal app bundle: a Cordis plugin that composes [`@deepseek-ai/dsh-agent-spine-demo`](../agent-spine-demo/README.md), persisted same-session goals, the human-command registry and `/goal` producer, JSONL persistence, keyboard-backed user interaction, a pre-created `main` agent, and [`@deepseek-ai/dsh-tui`](../../ui/tui/README.md). A `cordis.yml` mounts it as one entry; the [`dsh`](../../../apps/cli/README.md) CLI is the front door that boots such a config.
Use [`@deepseek-ai/dsh-cli-demo`](../cli-demo/README.md) for pipes, scripts, and other non-interactive runs. This package requires a TTY pair and has no line-oriented fallback.
Use [`@deepseek-ai/dsh-cli-demo`](../cli-demo/README.md) for pipes, scripts, and other non-interactive runs. This bundle requires a TTY pair and has no line-oriented fallback.
## What it bakes in
@@ -47,9 +47,9 @@ Swappable LLM, bash, filesystem, and other capability providers remain in the le
Fresh runs mint a `main-session-<uuid>` session id and pass it to both the TUI and configured agent. Resumed runs bind both components to `resumeSessionId`. The TUI mounts before the spine so it can render a matching config-start failure instead of leaving a blank terminal. The app composes persistence and session query for `/resume`; an embedding host may additionally provide `tuiResumeHost` for in-place process handoff.
## The bin
## Front door
`dsh-tui-demo [path-to-cordis.yml]` defaults to `./cordis.yml`, loads the optional cwd `.env`, boots the Cordis Loader, and waits for the full plugin tree. The repository installs Loader's optional native helper, so bare package specifiers resolve under plain Node.
This package ships no bin. The [`dsh`](../../../apps/cli/README.md) CLI is the terminal front door: `dsh [path-to-cordis.yml]` boots a leaf config that mounts this bundle (defaulting to the shipped `examples/tui-agent/cordis.yml`), loads the optional cwd `.env`, drives the Cordis Loader, and waits for the full plugin tree. The repository installs Loader's optional native helper, so bare package specifiers resolve under plain Node.
## Example leaf

View File

@@ -1,14 +1,11 @@
{
"name": "@deepseek-ai/dsh-tui-demo",
"description": "Full-screen terminal app: agent spine + persisted goals + human commands + JSONL persistence + pi-tui front door + pre-created main agent",
"description": "Full-screen TUI app bundle plugin: agent spine + persisted goals + human commands + JSONL persistence + pi-tui front door + pre-created main agent (mounted by the dsh CLI's config)",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"bin": {
"dsh-tui-demo": "lib/bin.js"
},
"exports": {
".": {
"types": "./lib/types/index.d.ts",
@@ -18,17 +15,12 @@
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./bin": {
"types": "./lib/types/bin.d.ts",
"default": "./lib/bin.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/bin.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"src"
@@ -37,7 +29,6 @@
"peerDependencies": {
"@cordisjs/plugin-include": "^1.0.4",
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
"@deepseek-ai/dsh-app-boot": "^0.0.1",
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-agent-loop": "^0.0.1",
"@deepseek-ai/dsh-commands": "^0.0.1",
@@ -62,7 +53,6 @@
"devDependencies": {
"@cordisjs/plugin-include": "workspace:^",
"@cordisjs/plugin-loader": "workspace:^",
"@deepseek-ai/dsh-app-boot": "workspace:^",
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-loop": "workspace:^",
"@deepseek-ai/dsh-commands": "workspace:^",

View File

@@ -1,27 +0,0 @@
#!/usr/bin/env node
/**
* Boot a TUI app from a leaf `cordis.yml`; usage is `dsh-tui-demo [config]`, defaulting to the
* cwd file. Shared `.env` loading, fail-loud Loader guards, and settled-tree boot live in
* dsh-app-boot. The tui-agent and cordis-agent demos invoke this bin with their own leaf configs.
* @module @deepseek-ai/dsh-tui-demo/bin
*/
import { boot, installFailLoud, loadEnv, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
const NAME = 'dsh-tui-demo'
/* v8 ignore start -- thin self-executing composition over the unit-tested
dsh-app-boot helpers; exercised end-to-end by the tui-agent PTY smoke and
the built-bin fail-loud smoke */
// Refuse pipes BEFORE booting: a compose-time throw inside the Loader tree is
// logged per-entry rather than rethrown, so a piped launch would otherwise
// settle into an idle UI-less process instead of exiting nonzero.
if (!process.stdin.isTTY || !process.stdout.isTTY) {
process.stderr.write(`${NAME}: the TUI requires stdin and stdout to be interactive TTYs; `
+ 'use the one-shot dsh-cli-demo bin for pipes and automation\n')
process.exit(1)
}
installFailLoud(NAME)
loadEnv(NAME)
await boot(NAME, resolveConfigPath(process.argv[2] ?? './cordis.yml', undefined))
/* v8 ignore stop */

View File

@@ -64,8 +64,8 @@ export interface Config {
/**
* Shell command template the TUI prints on exit and lists under `/resume`,
* with `{session}` replaced by the live session id (forwarded to the front
* door). Set it to a command that resumes via this app's env var, e.g.
* `RESUME_SESSION_ID={session} dsh`.
* door). Set it to a command that resumes the session, e.g.
* `dsh --resume {session}`.
*/
resumeCommand?: string
/** Full-screen TUI presentation settings. */

View File

@@ -1,98 +0,0 @@
import { spawn } from 'node:child_process'
import { existsSync } from 'node:fs'
import { mkdtemp, mkdir, rm, symlink, readFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { afterEach, describe, expect, it } from 'vitest'
/**
* Published-entry smoke: run `lib/bin.js` under plain Node in a symlinked external consumer.
* The TUI app owns no non-TTY fallback, so the piped subprocess must refuse to boot with a
* nonzero exit and a stderr pointer at the one-shot CLI — the bin guards BEFORE the Loader
* because a compose-time throw inside the tree is logged per-entry, not rethrown. The consumer
* links only the bin's import chain (dsh-app-boot and its vendored Loader stack): the refusal
* fires before any config is read, so no plugin tree is needed. Missing-config fail-loud and
* full-boot coverage for the shared dsh-app-boot glue live in cli-demo's built-bin suite; it
* skips before build, and interactive TTY behavior is PTY-covered by examples/tui-agent (the
* one sanctioned PTY surface).
*/
const repoRoot = fileURLToPath(new URL('../../../../', import.meta.url))
const tuiBin = join(repoRoot, 'packages/examples/tui-demo/lib/bin.js')
// Symlink each package the bin imports at module load by package name so plain
// Node resolves its built `main`, matching an installed dependency rather than
// tsconfig paths.
const dshPackages = ['examples/tui-demo', 'ui/app-boot']
const vendorPackages = ['cordis', 'loader', 'include', 'schemastery', 'cosmokit']
async function pkgName(absDir: string): Promise<string> {
const json = JSON.parse(await readFile(join(absDir, 'package.json'), 'utf8')) as { name: string }
return json.name
}
/** Build a temporary external consumer with built workspace/vendor links. */
async function makeConsumer(): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), 'tui-built-bin-'))
const nm = join(dir, 'node_modules')
for (const rel of dshPackages) {
const abs = join(repoRoot, 'packages', rel)
const target = join(nm, await pkgName(abs))
await mkdir(dirname(target), { recursive: true })
await symlink(abs, target)
}
for (const v of vendorPackages) {
const abs = join(repoRoot, 'vendor', v)
const target = join(nm, await pkgName(abs))
await mkdir(dirname(target), { recursive: true })
await symlink(abs, target)
}
return dir
}
/** Run the built bin in `cwd` with PIPED stdio; resolve with output + exit code. */
function runBuiltBin(cwd: string): Promise<{ stdout: string; code: number; stderr: string }> {
return new Promise((resolve, reject) => {
// NO tsx — this is the published `node lib/bin.js` path; the guard fires
// before the Loader resolves the config tree.
const child = spawn(process.execPath, [tuiBin, './cordis.yml'], {
cwd,
env: { ...process.env, DSH_HOME: join(cwd, '.dsh'), DSH_AGENTS_HOME: join(cwd, '.agents') },
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(`built bin did not exit within 25s. stdout:\n${stdout}\nstderr:\n${stderr}`))
}, 25_000)
child.on('exit', (code) => { clearTimeout(timer); resolve({ stdout, code: code ?? -1, stderr }) })
child.on('error', (err) => { clearTimeout(timer); reject(err) })
child.stdin.end()
})
}
let consumer: string | undefined
afterEach(async () => {
// Windows can briefly retain released handles after exit; retry removal.
if (consumer !== undefined) await rm(consumer, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 })
consumer = undefined
})
describe.skipIf(!existsSync(tuiBin))('dsh-tui-demo BUILT bin (node lib/bin.js, no tsx)', () => {
it('refuses pipes LOUD (non-zero exit + stderr) before booting the Loader', async () => {
consumer = await makeConsumer()
const { stdout, code, stderr } = await runBuiltBin(consumer)
expect(code).not.toBe(0)
expect(stderr).toContain('requires stdin and stdout to be interactive TTYs')
expect(stderr).toContain('dsh-cli-demo')
// The refusal happens before any plugin mounts: stdout stays silent.
expect(stdout).toBe('')
}, 30_000)
})

View File

@@ -1,14 +1,14 @@
import { defineConfig } from 'tsdown'
/**
* tui-demo ships two entries: the plugin (`index`) and the CLI `bin`
* (`bin`), the latter referenced by package.json `bin`/`exports["./bin"]`.
* The root tsdown builds only `lib/types/index.js`, so this override adds
* `lib/types/bin.js`. Declarations come from `tsc -b` (dts: false),
* matching every package.
* tui-demo ships the plugin (`index`) and its invariant companion; the CLI
* front door is `dsh` (apps/cli), which mounts this bundle through its config.
* The root tsdown builds only `lib/types/index.js`, so this override adds the
* invariant entry. Declarations come from `tsc -b` (dts: false), matching
* every package.
*/
export default defineConfig({
entry: ['lib/types/index.js', 'lib/types/invariant.js', 'lib/types/bin.js'],
entry: ['lib/types/index.js', 'lib/types/invariant.js'],
outDir: 'lib',
format: ['esm'],
platform: 'node',