refactor(cli): keep migrate out of scope
This commit is contained in:
@@ -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
|
||||
+2
-2
@@ -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 <id>` 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:<name>`. 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 <id>` 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:<name>`. 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 <id>` 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 <path>` 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 <path>` 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`).
|
||||
|
||||
@@ -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 <id>`。
|
||||
|
||||
`dsh migrate` 与 `dsh upgrade` 是默认 TUI 界面之上的引导式全新会话入口:各自在调用目录中创建一个全新会话,并以一个内置 skill 播种其首轮(`dsh-migrate` 用于从其他编码 agent 迁移——opencode、pi、Claude Code、Codex;`dsh-upgrade` 用于升级本 checkout),效果等同于用户手动键入 `/skill:<name>`。启动器将 skill 名称提供到启动上下文([`INITIAL_SKILL_KEY`](../../packages/ui/tui/README.md)),TUI 在聊天就绪后自动调用它。两者都不接受任何选项——`--config`、`-p`、`--resume` 都会明确报错——且仅在首次启动时播种,因此之后 `dsh --resume <id>` 恢复该会话时是普通 TUI 会话,不会重复注入。
|
||||
`dsh upgrade` 是默认 TUI 界面之上的引导式全新会话入口:它在调用目录中创建一个全新会话,并以内置 `dsh-upgrade` skill 播种其首轮,效果等同于用户手动键入 `/skill:<name>`。启动器将 skill 名称提供到启动上下文([`INITIAL_SKILL_KEY`](../../packages/ui/tui/README.md)),TUI 在聊天就绪后自动调用它。两者都不接受任何选项——`--config`、`-p`、`--resume` 都会明确报错——且仅在首次启动时播种,因此之后 `dsh --resume <id>` 恢复该会话时是普通 TUI 会话,不会重复注入。
|
||||
|
||||
|
||||
Web 和无头界面启动 `base.cordis.yml` 与 `web.cordis.yml`;`dsh web --config <path>` 会在 Web 界面默认值之后追加一个覆盖。除此之外,两者共享同一套组合:两者都将调用目录视为默认项目和 Workspace 根目录,除非通过 `--workspace-root <path>` 覆盖,否则会在该根目录下创建具名 Workspace;它们会把适用的 `AGENTS.md`/`CLAUDE.md` 指令加载到每个 agent-loop 请求前缀中,渲染预算为 65,536 字节,并选用首条消息模型标题。无头界面唯一的差异是监听操作系统分配的端口(并行 `dsh -p` 运行绝不冲突;stderr 打印的 URL 会在浏览器中打开实时会话)。两者都需要先构建前端 dist 和客户端 bundle(`pnpm run build && pnpm run build:web`)。
|
||||
|
||||
+12
-19
@@ -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
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
+2
-2
@@ -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<void> {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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/<name>/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.
|
||||
Reference in New Issue
Block a user