From ff126751d87c08331b3356ecdb063d5c1ebf2e65 Mon Sep 17 00:00:00 2001 From: Turtle Date: Wed, 29 Jul 2026 21:41:51 +0800 Subject: [PATCH] refactor(cli): keep migrate out of scope --- apps/cli/README.i18n.yaml | 4 +-- apps/cli/README.md | 4 +-- apps/cli/README.zh.md | 4 +-- apps/cli/src/args.ts | 31 ++++++++------------ apps/cli/src/bin.ts | 1 - apps/cli/src/tui.ts | 4 +-- apps/cli/tests/args.spec.ts | 7 +---- skills/dsh-migrate/SKILL.md | 57 ------------------------------------- 8 files changed, 21 insertions(+), 91 deletions(-) delete mode 100644 skills/dsh-migrate/SKILL.md diff --git a/apps/cli/README.i18n.yaml b/apps/cli/README.i18n.yaml index 6ef7959159..80c388280b 100644 --- a/apps/cli/README.i18n.yaml +++ b/apps/cli/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/cli/README.md -README.md: 2589462dfb5fc600b0e480a5a41c32860bf6837d -README.zh.md: 3b5e5319b76d9b18b2719cc8e943faaf398c51af +README.md: 7cde0dd8c9c6cf794cf8d1676ed6938e204117cf +README.zh.md: 3b21d563cbd8458810cd05f1ef71bd88074f294f diff --git a/apps/cli/README.md b/apps/cli/README.md index 2589462dfb..7cde0dd8c9 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -3,7 +3,7 @@ English | [中文](README.zh.md) -Argv is parsed once through a [Commander](https://github.com/tj/commander.js) adapter ([`src/args.ts`](src/args.ts)): one program whose default (no subcommand) is the TUI/headless surface (`--config`, `-p`/`--prompt`, `--resume`), whose `meta` subcommand is the same TUI over this checkout, whose `migrate`/`upgrade` subcommands are option-less guided-session entries, and whose `web` subcommand is the browser UI. `src/bin.ts` switches on the resolved mode and dynamic-imports only that mode's module. `dsh --help` lists every mode and `dsh web --help` renders the web usage, `dsh --version` prints this app's version, and an unknown option or a mistyped `--resume` fails loud (stderr, exit 1) instead of misrouting. Every subcommand that shares no option with the default surface — `migrate`, `upgrade`, `web` — rejects a leaked `--config`/`-p`/`--resume` rather than running and dropping it. `dsh web`'s `--host`/`--port` are unvalidated pass-through overrides: the `dsh-host-webserver` schema is the single source of both the default (the shipped `cordis.yml` value when a flag is absent) and validity, and rejects a bad value at boot. `--trusted-host` appends named authorities for the /api browser-trust fence; an all-interfaces bind additionally derives the machine's LAN IP literals itself ([`src/app-cli-entry.ts`](src/app-cli-entry.ts)), so the printed LAN URL works without flags. +Argv is parsed once through a [Commander](https://github.com/tj/commander.js) adapter ([`src/args.ts`](src/args.ts)): one program whose default (no subcommand) is the TUI/headless surface (`--config`, `-p`/`--prompt`, `--resume`), whose `meta` subcommand is the same TUI over this checkout, whose `upgrade` subcommands are option-less guided-session entries, and whose `web` subcommand is the browser UI. `src/bin.ts` switches on the resolved mode and dynamic-imports only that mode's module. `dsh --help` lists every mode and `dsh web --help` renders the web usage, `dsh --version` prints this app's version, and an unknown option or a mistyped `--resume` fails loud (stderr, exit 1) instead of misrouting. Every subcommand that shares no option with the default surface — `migrate`, `upgrade`, `web` — rejects a leaked `--config`/`-p`/`--resume` rather than running and dropping it. `dsh web`'s `--host`/`--port` are unvalidated pass-through overrides: the `dsh-host-webserver` schema is the single source of both the default (the shipped `cordis.yml` value when a flag is absent) and validity, and rejects a bad value at boot. `--trusted-host` appends named authorities for the /api browser-trust fence; an all-interfaces bind additionally derives the machine's LAN IP literals itself ([`src/app-cli-entry.ts`](src/app-cli-entry.ts)), so the printed LAN URL works without flags. The TUI surface: @@ -15,7 +15,7 @@ The TUI surface: `dsh meta` is that same TUI with this harness checkout as the workspace, so working on dsh itself needs no `cd`. It chdirs to the checkout root — resolved from the launcher's real path, the same root the source-path prompt section names — after both `.env` layers are loaded, so environment precedence is unchanged while the session cwd and HMR watch root move together. Meta always starts a fresh session and accepts no default-surface options; use ordinary `dsh --resume ` to resume a persisted session. -`dsh migrate` and `dsh upgrade` are guided fresh-session entries over the default TUI surface: each mints a fresh session in the invoking directory and seeds its first turn with a bundled skill (`dsh-migrate` for migrating from another coding agent — opencode, pi, Claude Code, Codex; `dsh-upgrade` for upgrading this checkout), exactly as if the user typed `/skill:`. The launcher passes the skill name on the boot context ([`INITIAL_SKILL_KEY`](../../packages/ui/tui/README.md)), which the TUI auto-invokes once the chat is live. Both take no options — `--config`, `-p`, and `--resume` fail loud — and seed only on this first launch, so a later `dsh --resume ` of the session is an ordinary TUI session with no re-injection. +`dsh upgrade` is a guided fresh-session entry over the default TUI surface: it mints a fresh session in the invoking directory and seeds its first turn with the bundled `dsh-upgrade` skill, exactly as if the user typed `/skill:`. The launcher passes the skill name on the boot context ([`INITIAL_SKILL_KEY`](../../packages/ui/tui/README.md)), which the TUI auto-invokes once the chat is live. Both take no options — `--config`, `-p`, and `--resume` fail loud — and seed only on this first launch, so a later `dsh --resume ` of the session is an ordinary TUI session with no re-injection. The Web and headless surfaces boot `base.cordis.yml` plus `web.cordis.yml`; `dsh web --config ` adds an overlay after the web surface defaults. Both surfaces otherwise share the same composition: both treat the invoking directory as the default project and Workspace root, create named Workspaces beneath that root unless `--workspace-root ` overrides it, load applicable `AGENTS.md`/`CLAUDE.md` instructions into each agent-loop request prefix with a 65,536-byte render budget, and opt into first-message model titles. Headless differs only in listening on an OS-assigned port (parallel `dsh -p` runs never collide; the stderr-printed URL opens the live session in a browser). Both need the frontend dist and client bundles built (`pnpm run build && pnpm run build:web`). diff --git a/apps/cli/README.zh.md b/apps/cli/README.zh.md index 3b5e5319b7..3b21d563cb 100644 --- a/apps/cli/README.zh.md +++ b/apps/cli/README.zh.md @@ -3,7 +3,7 @@ [English](README.md) | 中文 -Argv 只会通过 [Commander](https://github.com/tj/commander.js) 适配器([`src/args.ts`](src/args.ts))解析一次:同一个程序的默认形式(无子命令)是 TUI/无头界面(`--config`、`-p`/`--prompt`、`--resume`),`meta` 子命令是以本 checkout 为 workspace 的同一个 TUI,`migrate`/`upgrade` 子命令是无选项的引导会话入口,`web` 子命令则是浏览器 UI。`src/bin.ts` 按解析后的 mode 分支,仅动态导入该 mode 的模块。`dsh --help` 列出所有 mode,`dsh web --help` 渲染 Web 用法,`dsh --version` 打印此应用的版本;未知选项或拼错的 `--resume` 会明确报错(stderr,退出码 1),而不会被错路由。凡与默认界面不共享任何选项的子命令(`migrate`、`upgrade`、`web`)都会拒绝泄漏进来的 `--config`/`-p`/`--resume`,而不会照常运行并丢弃它。`dsh web` 的 `--host`/`--port` 是未验证的直通覆盖:`dsh-host-webserver` schema 是默认值(标志缺失时使用已交付的 `cordis.yml` 值)和有效性的唯一真源,并在启动时拒绝错误值。`--trusted-host` 为 /api 浏览器信任栅栏追加具名权威;全接口绑定还会自行推导本机的 LAN IP 字面量([`src/app-cli-entry.ts`](src/app-cli-entry.ts)),因此打印出的 LAN URL 无需任何标志即可使用。 +Argv 只会通过 [Commander](https://github.com/tj/commander.js) 适配器([`src/args.ts`](src/args.ts))解析一次:同一个程序的默认形式(无子命令)是 TUI/无头界面(`--config`、`-p`/`--prompt`、`--resume`),`meta` 子命令是以本 checkout 为 workspace 的同一个 TUI,`upgrade` 子命令是无选项的引导会话入口,`web` 子命令则是浏览器 UI。`src/bin.ts` 按解析后的 mode 分支,仅动态导入该 mode 的模块。`dsh --help` 列出所有 mode,`dsh web --help` 渲染 Web 用法,`dsh --version` 打印此应用的版本;未知选项或拼错的 `--resume` 会明确报错(stderr,退出码 1),而不会被错路由。凡与默认界面不共享任何选项的子命令(`migrate`、`upgrade`、`web`)都会拒绝泄漏进来的 `--config`/`-p`/`--resume`,而不会照常运行并丢弃它。`dsh web` 的 `--host`/`--port` 是未验证的直通覆盖:`dsh-host-webserver` schema 是默认值(标志缺失时使用已交付的 `cordis.yml` 值)和有效性的唯一真源,并在启动时拒绝错误值。`--trusted-host` 为 /api 浏览器信任栅栏追加具名权威;全接口绑定还会自行推导本机的 LAN IP 字面量([`src/app-cli-entry.ts`](src/app-cli-entry.ts)),因此打印出的 LAN URL 无需任何标志即可使用。 TUI 界面: @@ -15,7 +15,7 @@ TUI 界面: `dsh meta` 是以本 harness checkout 为 workspace 的同一个 TUI,因此开发 dsh 自身无需 `cd`。它在两层 `.env` 都加载之后才 chdir 到 checkout 根目录(从启动器的真实路径解析,与源码路径提示词段所指的根目录相同),因此环境优先级不变,而会话 cwd 与 HMR 监视根目录会一并移动。Meta 始终创建新会话,不接受默认界面的任何选项;恢复已持久化会话应使用普通的 `dsh --resume `。 -`dsh migrate` 与 `dsh upgrade` 是默认 TUI 界面之上的引导式全新会话入口:各自在调用目录中创建一个全新会话,并以一个内置 skill 播种其首轮(`dsh-migrate` 用于从其他编码 agent 迁移——opencode、pi、Claude Code、Codex;`dsh-upgrade` 用于升级本 checkout),效果等同于用户手动键入 `/skill:`。启动器将 skill 名称提供到启动上下文([`INITIAL_SKILL_KEY`](../../packages/ui/tui/README.md)),TUI 在聊天就绪后自动调用它。两者都不接受任何选项——`--config`、`-p`、`--resume` 都会明确报错——且仅在首次启动时播种,因此之后 `dsh --resume ` 恢复该会话时是普通 TUI 会话,不会重复注入。 +`dsh upgrade` 是默认 TUI 界面之上的引导式全新会话入口:它在调用目录中创建一个全新会话,并以内置 `dsh-upgrade` skill 播种其首轮,效果等同于用户手动键入 `/skill:`。启动器将 skill 名称提供到启动上下文([`INITIAL_SKILL_KEY`](../../packages/ui/tui/README.md)),TUI 在聊天就绪后自动调用它。两者都不接受任何选项——`--config`、`-p`、`--resume` 都会明确报错——且仅在首次启动时播种,因此之后 `dsh --resume ` 恢复该会话时是普通 TUI 会话,不会重复注入。 Web 和无头界面启动 `base.cordis.yml` 与 `web.cordis.yml`;`dsh web --config ` 会在 Web 界面默认值之后追加一个覆盖。除此之外,两者共享同一套组合:两者都将调用目录视为默认项目和 Workspace 根目录,除非通过 `--workspace-root ` 覆盖,否则会在该根目录下创建具名 Workspace;它们会把适用的 `AGENTS.md`/`CLAUDE.md` 指令加载到每个 agent-loop 请求前缀中,渲染预算为 65,536 字节,并选用首条消息模型标题。无头界面唯一的差异是监听操作系统分配的端口(并行 `dsh -p` 运行绝不冲突;stderr 打印的 URL 会在浏览器中打开实时会话)。两者都需要先构建前端 dist 和客户端 bundle(`pnpm run build && pnpm run build:web`)。 diff --git a/apps/cli/src/args.ts b/apps/cli/src/args.ts index 038ac96af4..ca898b08ea 100644 --- a/apps/cli/src/args.ts +++ b/apps/cli/src/args.ts @@ -35,13 +35,13 @@ interface MetaInvocation { } /** - * Guided fresh-session entries: `dsh migrate` seeds the first turn with the - * `dsh-migrate` skill, `dsh upgrade` with `dsh-upgrade`. Each always mints a + * Guided fresh-session entry: `dsh upgrade` seeds the first turn with the + * `dsh-upgrade` skill. It always mints a * fresh session in the invoking directory and takes no options — `--resume`, * `--config`, and `-p` are rejected as mistyped, so there is nothing to carry. */ interface SkillSessionInvocation { - mode: 'migrate' | 'upgrade' + mode: 'upgrade' } /** @@ -174,22 +174,15 @@ Examples: // Registration order is the rendered help order, so daily use comes first // and the harness-development surfaces (`web --dev`, `meta`) come last. - // `migrate` and `upgrade` are guided fresh-session entries: they take no - // options and always mint a fresh session, so nothing is left to carry. Each - // description names the outcome, not the skill the first turn invokes. - const guided = { - migrate: 'import settings from another coding agent (Claude Code, Codex, opencode)', - upgrade: 'update this dsh installation to the latest version', - } as const - for (const mode of ['migrate', 'upgrade'] as const) { - program - .command(mode) - .description(guided[mode]) - .action(() => { - rejectParentOptions(mode) - resolved = { mode } - }) - } + // `upgrade` is a guided fresh-session entry: it takes no options and always + // mints a fresh session, so nothing is left to carry. + program + .command('upgrade') + .description('update this dsh installation to the latest version') + .action(() => { + rejectParentOptions('upgrade') + resolved = { mode: 'upgrade' } + }) // Host and port name no default: the CLI passes neither through when the flag // is absent, so the shipped `cordis.yml` value stands and restating it here diff --git a/apps/cli/src/bin.ts b/apps/cli/src/bin.ts index 619d0dcd58..6f7ae77c76 100644 --- a/apps/cli/src/bin.ts +++ b/apps/cli/src/bin.ts @@ -48,7 +48,6 @@ switch (invocation.mode) { await runMeta() break } - case 'migrate': case 'upgrade': { const { runSkillSession } = await import('./tui.ts') await runSkillSession(`dsh-${invocation.mode}`) diff --git a/apps/cli/src/tui.ts b/apps/cli/src/tui.ts index 6964ac9753..e96f9b515e 100644 --- a/apps/cli/src/tui.ts +++ b/apps/cli/src/tui.ts @@ -10,7 +10,7 @@ * workspace, and an in-place resume enters the selected session's own directory. * `dsh meta` * ({@link runMeta}) is the one exception — it makes this harness checkout the - * workspace. `dsh migrate`/`dsh upgrade` ({@link runSkillSession}) are fresh + * workspace. `dsh upgrade` ({@link runSkillSession}) are fresh * sessions whose first turn auto-invokes a bundled skill. After boot, the * agent's system prompt is told the path to this harness checkout so it can * find its own source. @@ -91,7 +91,7 @@ export async function runMeta(): Promise { /** * Run the interactive TUI as a guided fresh session whose first turn invokes a - * bundled skill (`dsh migrate` → `dsh-migrate`, `dsh upgrade` → `dsh-upgrade`). + * bundled skill (`dsh upgrade` → `dsh-upgrade`). * Always mints a fresh session in the invoking directory; the skill is seeded * only on this first launch, so a later `--resume` of the session is an ordinary * TUI session with no re-injection. diff --git a/apps/cli/tests/args.spec.ts b/apps/cli/tests/args.spec.ts index 7eea63b68d..ccbd5fa326 100644 --- a/apps/cli/tests/args.spec.ts +++ b/apps/cli/tests/args.spec.ts @@ -40,7 +40,6 @@ describe('parseDshArgs', () => { expect(parse(['web', '--host', '0.0.0.0', '--port', '8080', '--dev', '--workspace-root', '/w'])) .toEqual({ mode: 'web', host: '0.0.0.0', port: 8080, dev: true, workspaceRoot: '/w' }) // Guided fresh-session entries carry nothing: bare mode discriminant only. - expect(parse(['migrate'])).toEqual({ mode: 'migrate' }) expect(parse(['upgrade'])).toEqual({ mode: 'upgrade' }) // --trusted-host is variadic and repeatable; authorities pass through unvalidated. expect(parse(['web', '--trusted-host', 'harness.internal:3080', 'lab.internal', '--trusted-host', '10.0.0.9'])) @@ -73,12 +72,8 @@ describe('parseDshArgs', () => { expect(exitCode(['meta', '--config', 'c.yml'])).toBe(1) expect(exitCode(['meta', '--config-replace', 'tree.yml'])).toBe(1) expect(exitCode(['meta', '-p', 'task'])).toBe(1) - // `migrate`/`upgrade` take no options: any leaked default-surface flag is a + // `upgrade` take no options: any leaked default-surface flag is a // mistyped invocation, not a silently-dropped input. - expect(exitCode(['migrate', '--resume', 's'])).toBe(1) - expect(exitCode(['migrate', '--config', 'c.yml'])).toBe(1) - expect(exitCode(['migrate', '--config-replace', 'tree.yml'])).toBe(1) - expect(exitCode(['migrate', '-p', 'task'])).toBe(1) expect(exitCode(['upgrade', '--resume', 's'])).toBe(1) expect(exitCode(['upgrade', '--config', 'c.yml'])).toBe(1) expect(exitCode(['-p', 'task', 'upgrade'])).toBe(1) diff --git a/skills/dsh-migrate/SKILL.md b/skills/dsh-migrate/SKILL.md deleted file mode 100644 index fbea31f04f..0000000000 --- a/skills/dsh-migrate/SKILL.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -name: dsh-migrate -description: Migrate a user's setup from another coding agent (opencode, pi, Claude Code, Codex) to DSH — porting instruction files, custom commands and skills, hooks, MCP servers, and API/env configuration into their DSH equivalents. Use when the user asks to migrate, switch, or move from another coding agent to DSH. ---- - -# DSH Migrate - -Move a user's existing coding-agent setup onto DSH: instruction files, custom commands, skills, hooks, MCP servers, and API/environment configuration. Port only what has a real DSH equivalent; tell the user plainly when something has none. - -## First: identify the source - -Ask which agent the user is migrating from if they have not said: **opencode**, **pi**, **Claude Code**, or **Codex**. The mapping differs per source. Then locate that agent's config (ask the user, or inspect the obvious locations: `~/.claude/` and `.claude/` for Claude Code, `~/.codex/` and `.codex/` for Codex, the opencode/pi config dir the user names). Read what exists before proposing changes; never invent files the user does not have. - -## DSH targets - -Every migration lands in one of these DSH surfaces. Verify the exact path against the running install rather than assuming. - -- **Workspace instructions**: DSH reads `AGENTS.md` and `CLAUDE.md` (and `AGENTS.local.md` / `CLAUDE.local.md`) from the project, walking up to the project root, plus a user-global `~/.dsh/AGENTS.md`. `CLAUDE.md` is read as-is, so a Claude Code project needs no rename. -- **Personal overlay** (user-global, applies to every DSH session): the Harness home `~/.dsh/` holds `config.yaml` (a top-level YAML array of Loader patch entries that patch the booted plugin tree), `.env` (fills environment gaps only — ambient env and the invoking directory's `.env` win), `AGENTS.md`, and `skills/`. -- **Skills**: directory-bundle or flat-Markdown skills load from `.dsh/skills/` and `.agents/skills/` in the project, and `~/.dsh/skills/` and `~/.agents/skills/` for the user. Personal skills go in `~/.dsh/skills//SKILL.md`. Use the `skill-creator` skill to author them. -- **Hooks**: DSH runs a mapped subset of an existing Claude Code or Codex hook config through compatibility bridges — no rewrite needed for the supported subset. See the per-source sections. -- **MCP servers**: DSH has no native MCP client. Reach MCP servers through the `mcporter` skill / CLI, which can call servers already configured for other tools. -- **API / model config**: DSH uses `DEEPSEEK_API_KEY` (and optional `DEEPSEEK_BASE_URL`) from `.env` (root, invoking directory, or `~/.dsh/.env`). Model and provider are chosen in the booted `cordis.yml` / personal overlay, not per-provider config files. - -## Per-source mapping - -### Claude Code - -- `CLAUDE.md` → read as-is by DSH workspace instructions; keep it, or consolidate into `AGENTS.md`. User-global rules → `~/.dsh/AGENTS.md`. -- `.claude/hooks.json` (or a settings file's `hooks` key) → the `@deepseek-ai/dsh-hooks-claude` bridge runs the mapped command-hook subset on DSH's interception seams, with `${CLAUDE_PLUGIN_ROOT}` / `${CLAUDE_PROJECT_DIR}` substitution. Add it to the booted `cordis.yml` (or personal overlay) pointing `configPath` at the existing file. Anything outside the mapped subset should become a native DSH plugin, not a shimmed hook. -- Slash commands → DSH commands are plugin-provided; there is no drop-in import. Reimplement genuinely needed ones as skills (`~/.dsh/skills/`) or plugins. -- MCP servers in Claude config → use `mcporter` to reach them; DSH has no native MCP. -- `ANTHROPIC_API_KEY` etc. do not transfer; DSH is DeepSeek-backed via `DEEPSEEK_API_KEY`. - -### Codex - -- Codex `AGENTS.md` → DSH already reads `AGENTS.md`; keep it. User-global → `~/.dsh/AGENTS.md`. -- Codex hook config → the `@deepseek-ai/dsh-hooks-codex` bridge runs a deliberate subset (`PreToolUse`, `PostToolUse`, `SessionStart`, `UserPromptSubmit`, `Stop`; regex-only matchers; no plugin env injection; no pre-tool approval/rewrite). Add the bridge to the booted config with `configPath` at the existing Codex hooks file. State the unsupported points to the user rather than implying full parity. -- MCP servers → `mcporter`. -- API/env → `DEEPSEEK_API_KEY` in `.env`. - -### opencode / pi - -- These have no compatibility bridge. Port by concept, not by file: - - Agent/system instructions → `AGENTS.md` (project) and `~/.dsh/AGENTS.md` (user-global). - - Provider/model and any plugin-style tuning → the booted `cordis.yml` or `~/.dsh/config.yaml` overlay patches; API keys → `.env`. - - Reusable prompts/commands → skills under `~/.dsh/skills/`. - - MCP servers → `mcporter`. -- pi has no native MCP by design; the `mcporter` route is the same as for DSH. - -## Do the migration - -1. Confirm the source agent and read its actual config. -2. For each capability (instructions, hooks, commands/skills, MCP, API/env), map it to the DSH target above, or tell the user it has no equivalent. -3. Write the ported files (`AGENTS.md`, `~/.dsh/AGENTS.md`, `~/.dsh/config.yaml`, `~/.dsh/.env`, skills). For hook bridges, add the plugin entry to the booted config. -4. Verify: hooks need the bridge plugin present in the running tree; MCP needs `mcporter` reachable; API needs `DEEPSEEK_API_KEY` set. Test in a real DSH session, not just on paper. -5. Summarize what was ported, what was reimplemented, and what has no DSH equivalent.