Files
deepseek-harness/apps/cli/reference/README.md
T
Turtle 37cbd155f5 refactor(cli)!: the launcher parses only its own flags
Launcher flags come first and end at the first token dsh does not
recognize; everything after reaches the booted app verbatim, so
dsh --profile tui --resume <id> works with no launcher change and
dsh --profile web --help prints the web app's help. A bare dsh -h, which
has no app to hand the flag to, still prints the launcher's own.

src/web.ts is deleted: the Web flag family, its LAN-trust sampling, and the
one-shot task positional now live in their bundles, and runProfile no
longer knows any row id. What the startup row decides comes back as a
launcher-owned patch layer above every layer a user can edit, so a live
config edit recomposes the tree without resetting a served port.

dsh web and dsh --profile web finally boot through one path, which also
gives --profile web the harness-source prompt section that only the alias
used to add.
2026-08-10 23:45:04 +08:00

11 KiB

dsh CLI behavior reference

English | 中文

This reference defines the profile, web-alias, plugin-management, and config-dump command modes. Argv is parsed once through src/args.ts, and src/bin.ts dynamically imports only the selected runner.

Profile boot

dsh --profile <name> boots the profile at $DSH_HOME/profiles/<name>. The effective tree is composed over an empty root by applying, in order: each bundle patch named in the profile manifest's dsh.profile.bundles list, the profile's own cordis.patch.yml, the home-level $DSH_HOME/cordis.patch.yml (machine-local preferences shared by every profile, so it outranks the per-profile layer), and each --patch <path> overlay in argv order. Later layers win per row; a patch replaces the targeted row's complete config value rather than deep-merging keys, and may insert new rows. A parse, schema, resolution, or plugin boot failure is reported and exits nonzero. SIGINT and SIGTERM dispose the mounted root before exit.

Bundle names resolve from the dsh installation first, then from the profile directory. In-box bundles (@deepseek-ai/dsh-base, @deepseek-ai/dsh-web-app, @deepseek-ai/dsh-headless) therefore always come from the same installation as the running dsh; out-of-tree bundles come from the profile's pnpm-managed node_modules. A bare plugin name in any patch row resolves through the profile directory's Node parent-walk, which reaches the maintained installation fallback $DSH_HOME/profiles/node_modules (one symlink per package the installation's app and bundles depend on, healed on every launch).

The web and headless profiles auto-initialize from shipped templates on first use (web: base + web-app; headless: base + headless). Any other missing profile fails loud with a hint to run dsh plugin --profile <name> add <package>.

App arguments

The launcher's flags come first and end at the first token it does not recognize; everything from there on is handed to the booted profile verbatim through ctx.cmdlineArgs, where that app's own startup row parses it (dsh-cmdline). dsh --profile web --port 8080 therefore reaches the web app's --port, dsh --profile web --help prints that app's help and boots nothing, and dsh --help (no profile to hand it to) prints the launcher's own. -V/--version prints the launcher's version when it appears before the app-argument boundary.

A composition mounts once. A Loader row that injects cmdlineArgs parses this app's arguments and provides what it resolved as a service; each row configured from flags injects that service, and Loader waits for it before evaluating the row's config (port: !!js ctx.webStartup.port ?? 3080). A flag therefore beats the value written beside it. This precedence requires the row to retain that expression; a user patch that replaces the whole config with literals removes the runtime read. Help and rejected arguments request exit — nonzero for a rejection, 0 for help — without activating rows that depend on the startup service. A live cordis.patch.yml edit re-evaluates expressions against services that are still up, so it cannot reset a served port.

Launcher flags must come before app arguments, and the launcher's parser consumes one --: an app argument that must arrive as a literal -- needs -- --. A first app argument equal to web or plugin selects that subcommand instead. A profile with no active row injecting cmdlineArgs accepts no app arguments; it rejects them before mounting any row instead of silently ignoring them.

The shipped apps own these command lines:

Profile Arguments
web --host, --port, --dev, --workspace-root, repeatable --trusted-host
headless the task text, as the positional argument

A one-shot task (dsh --profile headless "run the tests") creates one fresh persisted Agent through the core registry, submits the task, waits for quiescence, and flushes the Session before deriving the last non-empty assistant text and final turn/end reason from its durable interval. It prints the text on stdout and exits 0 for completed, else 1. An invocation with no task is a usage error from that app. The shipped headless profile mounts no ApiProxy, Host, HTTP server, Web runtime, or browser client; a successful run writes nothing to stderr and opens no listening port.

Inspect the composed tree without booting it:

dsh --profile web --dump-default-config
dsh --profile web --patch ./extra.yml --dump-config

--dump-default-config prints only the bundle layers; --dump-config adds the profile's cordis.patch.yml, the home-level $DSH_HOME/cordis.patch.yml, and --patch overlays. Both print comments naming the file that supplied each row and every overlay that changed it; !!js expressions remain unevaluated, and unmatched patch targets are reported on stderr. A dump never runs an app's startup row, so it shows the composed tree before any app argument is resolved and rejects an invocation that carries app arguments.

Plugin management

dsh plugin --profile <name> <args...> initializes the profile when missing (shipped template, or @deepseek-ai/dsh-base alone for other names), then forwards <args...> to pnpm with the profile directory as working directory — add, remove, why, update, and every other pnpm verb work unchanged; pnpm must be on PATH. Relative path specs (., ../plugin, and their file:/link: forms) are anchored to the invoking directory first, so add . from a plugin checkout installs that checkout, not the profile. After every successful run, dsh.profile.bundles is reconciled against the installed state: each dependency resolving to a package whose manifest declares "dsh": { "bundle": { "patch": "./cordis.patch.yml" } } joins the layer stack (so an update that gains the declaration activates it), a bundle-less dependency stays plain with a one-time warning, and a removed dependency leaves the stack.

dsh plugin --profile tui add github:deepseek-harness/turtle-ui
dsh plugin --profile tui remove turtle-ui
dsh --profile tui

Git-hosted plugins that ship sources build during install through their prepare script, which pnpm ≥10 blocks until the consumer allows it: the first add fails with pnpm's allowBuilds hint (and a dsh pointer at the profile's pnpm-workspace.yaml); copy the printed key there and re-run. Installing a built tarball or a local checkout needs no allowance.

Web alias

dsh web is a hardcoded alias for --profile web; the flags after it belong to the web app, which owns them in its bundle's startup row. --host, --port, and --workspace-root override the composed values of the rows that carry them, repeatable --trusted-host adds authorities over the composed fence configuration, and --dev switches the web-runtime row to development mode and enables the client-plugin HMR receiver the bundle ships disabled; it expects a separate pnpm run dev:web watcher for no-refresh client bundle updates.

dsh web
dsh web --patch ./extra.cordis.yml
dsh web --dump-config
dsh web --help

The production Web runner needs built package and frontend artifacts (pnpm run build). It serves http://127.0.0.1:3080 by default. Binding all interfaces also trusts the machine's discovered LAN IP literals; --trusted-host adds named authorities accepted by the /api browser-trust fence.

Process shutdown gives the plugin tree up to five seconds to dispose. The first SIGINT/SIGTERM starts that graceful drain; a second signal forces immediate exit. If one-shot normal completion is already stuck in disposal, the first Ctrl+C is the escalation and exits immediately instead of being swallowed.

All modes treat the invoking directory as the default workspace root, load applicable AGENTS.md or CLAUDE.md instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. Long-lived surfaces watch valid edits of both cordis.patch.yml layers (profile and home) and reapply them transactionally; one-shot runs read the files once at startup.

New sessions default to the workspace-write permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads, network access, and process visibility are not confined. DSH_PERMISSION_MODE changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one.

DSH_TOOLS_MODE selects native, code, or both for the process; another value fails at boot. The shipped minimal agent preset keeps that deployment presentation, fixes the complete system prompt to You are a helpful software engineer assistant., and composes only persistent bash plus str_replace_editor. Select 极简模式 when creating a Web session; every other prompt section and model-facing plugin remains absent from that agent while the shared browser, workspace, persistence, sandbox, and permission host stays in place.

Shared deployment behavior

The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable web_search, and session telemetry. Provider credentials resolve from the inherited environment, $DSH_HOME/.credentials.yaml, the invoking directory's .env, then $DSH_HOME/.env; the managed document is never materialized into process.env, while both .env files are ordinary launch environment layers. Search uses DEEPSEEK_API_KEY and accepts DEEPSEEK_SEARCH_BASE_URL; web_fetch is disabled unless a patch layer inserts a provider and enables it.

Session events stream as OTLP/HTTP logs by default. DSH_TELEMETRY_OTLP_URL selects another collector. Any non-empty DSH_TELEMETRY_DISABLED disables the telemetry row before boot. The shipped base has no telemetry redaction rule, so exported records can contain message text, tool arguments and results, and workspace paths; the telemetry Agent Note owns that deployment decision.

Install external plugin bundles through dsh plugin --profile <name> add <package-or-git-spec>. The installed package owns its dependencies and contributes its declared cordis.patch.yml layer. The CLI also ships @deepseek-ai/dsh-mcp-client as a dependency for patch layers, but no MCP server is enabled by default because each server command is trusted executable code outside the agent sandbox.

Source launcher

Link the source-running launcher onto PATH:

ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh

It resolves the checkout through its real path and launches apps/cli/src/bin.ts with node --import tsx/esm. TSX_TSCONFIG_PATH is pinned to the checkout root, so workspace package resolution is independent of the invoking directory. pnpm run dsh uses the same entry and forwards arguments. The built form is apps/cli/lib/bin.js after pnpm run build.