From 2464d6169f945ced4f2c6187afc7aaba8d3b3408 Mon Sep 17 00:00:00 2001 From: Turtle Date: Wed, 22 Jul 2026 10:55:20 +0800 Subject: [PATCH] feat(app-boot): move personal config to the Harness home (~/.dsh) Squashes feat/personal-config-dsh-home: personal config.yaml and .env move from ~/.config/dsh to the Harness home (~/.dsh), plus the module-graph, lockfile, and i18n pairing regeneration that followed. --- ...26-07-20-dsh-cli-personal-config.i18n.yaml | 4 +-- .../2026-07-20-dsh-cli-personal-config.md | 10 +++--- .../2026-07-20-dsh-cli-personal-config.zh.md | 10 +++--- ...2026-07-21-tui-todo-write-opt-in.i18n.yaml | 4 +-- .../2026-07-21-tui-todo-write-opt-in.md | 2 +- .../2026-07-21-tui-todo-write-opt-in.zh.md | 2 +- apps/cli/README.md | 2 +- apps/cli/package.json | 5 +-- apps/cli/src/tui.ts | 8 ++--- apps/cli/tsconfig.json | 3 ++ .../ui/app-boot/tests/personal-config.spec.ts | 35 ++++--------------- scripts/install.sh | 16 ++++----- 12 files changed, 41 insertions(+), 60 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml index 8dd2bc2fdf..7addc991d2 100644 --- a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.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 -2026-07-20-dsh-cli-personal-config.md: 7849f6317f290a677bae45219012edd2ad9e7211 -2026-07-20-dsh-cli-personal-config.zh.md: 77b3c319da975a2d52e3d50fda361edd4a1bd23c +2026-07-20-dsh-cli-personal-config.md: 514bb5b12a3e04c7deaad1e8616472eed1c920e1 +2026-07-20-dsh-cli-personal-config.zh.md: 16fada82c59c8a356e6df112234e6b7565aae1bf diff --git a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md index 7849f6317f..514bb5b12a 100644 --- a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md +++ b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md @@ -1,4 +1,4 @@ -# Agent Note: The dsh CLI and personal config overlays from ~/.config/dsh +# Agent Note: The dsh CLI and personal config overlays from the Harness home Status: implemented @@ -14,13 +14,13 @@ Two coupled pieces, aligned with the `apps/` assembly tier proposed by the `dsh **The `dsh` CLI (`apps/cli`, npm name `@deepseek-ai/dsh`).** `apps/*` joins the workspaces as the product-assembly tier over `packages/*` libraries. The bin's dispatch reserves `web` and `-p`/`--prompt` for PR #443 (they exit with a pointer) so the two branches merge as a near-union; everything else runs the default surface: the interactive TUI, booting the shipped `examples/tui-agent/cordis.yml` (or an explicit config argument) with the invoking directory as the workspace. The committed `bin/dsh` launcher resolves the checkout through its own real path and runs the bin **from source** via the repo's tsx (with `--expose-internals` for the config's HMR entry), so `ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh` installs a command that always executes the current working tree. `pnpm run demo:tui` runs the same entry. -**Personal config (`dsh-app-boot`).** The personal config directory resolves as `$DSH_CONFIG_HOME`, else `$XDG_CONFIG_HOME/dsh`, else `~/.config/dsh` (`resolvePersonalConfigDir`; empty variables read as unset). The dsh TUI surface consumes its two optional files; the demo bins boot their committed trees verbatim: +**Personal config (`dsh-app-boot`).** The personal overlay lives in the Harness home — `$DSH_HOME`, else `~/.dsh` — resolved by the shared [`resolveDshHome`](../architecture/2026-07-24-single-harness-home-resolver.md) (`@deepseek-ai/dsh-paths`), the same single root skills and AGENTS.md resolve against. The dsh TUI surface consumes its two optional files; the demo bins boot their committed trees verbatim: - `.env` — loaded after the invoking directory's `.env`; `process.loadEnvFile` never overrides, so precedence is ambient > project `.env` > personal `.env`. - `config.yaml` — a top-level YAML array of `@cordisjs/plugin-include` `PatchOptions`, parsed with the include's own `!!js` dialect (`loadPersonalPatches`) and passed to `boot()`, which forwards it as the root include's `patches`. Patch semantics are exactly the committed overlay semantics (the Code Mode overlay is the template): an id-targeted patch replaces the named entry's whole `config`, `insert` appends entries, an unmatched id warns and is skipped. - A missing file means no overlay; a present-but-unreadable, unparsable, or non-array file throws at boot (misconfiguration fails loud, never a silent skip). -The PTY smoke's launcher isolates `DSH_CONFIG_HOME` to a per-test directory, exactly as it already isolates `DSH_HOME`/`DSH_AGENTS_HOME`, so a developer's real personal overlay cannot leak into fixtures; only the dsh CLI reads personal config, so no other test launcher needed changes. +The PTY smoke's launcher isolates `$DSH_HOME` to a per-test directory, exactly as it already isolates `DSH_AGENTS_HOME`, so a developer's real personal overlay cannot leak into fixtures; only the dsh CLI reads personal config, so no other test launcher needed changes. Hot-reload interplay: the include re-applies its `patches` on every config re-read (the [config hot-reload resilience Agent Note](../bug-fix/2026-07-20-config-hot-reload-resilience.md)), so a live `cordis.yml` edit keeps the personal overlay applied. @@ -41,9 +41,9 @@ Hot-reload interplay: the include re-applies its `patches` on every config re-re - `dsh` from any directory (and `pnpm run demo:tui`) boots the personal provider/model with zero repo changes; verified end-to-end against a personal Anthropic proxy with Opus 4.8, including a bash tool round trip. - Because an id-targeted patch replaces the whole `config`, a personal override restates the base fields it keeps and can drift when the base entry changes shape; the loader's entry-not-found/name-mismatch warnings are the only diagnostics. - Personal patches resolve ids against the booted file's own tree, so nested-include overlays (Code Mode) are not personalized; live-run parity for those leaves is deferred. -- `dsh-app-boot` gains a real dependency (`js-yaml`) and a load-only copy of the include's `!!js` YAML type. +- `dsh-app-boot` depends on `js-yaml` (plus a load-only copy of the include's `!!js` YAML type) and, like `apps/cli`, on `@deepseek-ai/dsh-paths` for `resolveDshHome`. - When PR #443 lands, `apps/cli/src/bin.ts`'s dispatch chain and `apps/cli/package.json`'s dependency list conflict textually; both resolve as unions (their `web`/`-p` branches plus our default-TUI branch). ## Testing -`packages/ui/app-boot/tests/personal-config.spec.ts` pins directory precedence (including empty-variable fallback), `!!js` preservation and end-to-end interpolation through a booted tree, insert entries, the absent/empty no-op paths, and the three fail-loud shapes (unreadable, unparsable, non-array). `examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` boots the dsh bin in a PTY three ways: default config with no overlay, a personal `.env` + `config.yaml` chain whose patched welcome renders in the banner, and an invalid personal file failing the boot loudly. The pre-existing smokes and snapshot suites pass on a machine whose real `~/.config/dsh` overlay would change the booted model — the isolation, not luck. +`packages/ui/app-boot/tests/personal-config.spec.ts` pins `!!js` preservation and end-to-end interpolation through a booted tree, insert entries, the default directory resolving from `$DSH_HOME`, the absent/empty no-op paths, and the three fail-loud shapes (unreadable, unparsable, non-array). `examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` boots the dsh bin in a PTY three ways: default config with no overlay, a personal `.env` + `config.yaml` chain whose patched welcome renders in the banner, and an invalid personal file failing the boot loudly. The pre-existing smokes and snapshot suites pass on a machine whose real `~/.dsh` overlay would change the booted model — the isolation, not luck. diff --git a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md index 77b3c319da..16fada82c5 100644 --- a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md +++ b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md @@ -1,4 +1,4 @@ -# Agent Note: dsh CLI 与来自 ~/.config/dsh 的个人配置 overlay +# Agent Note: dsh CLI 与来自 Harness home 的个人配置 overlay Status: implemented @@ -14,13 +14,13 @@ Status: implemented **`dsh` CLI(`apps/cli`,npm 名 `@deepseek-ai/dsh`)。** `apps/*` 作为 `packages/*` 库之上的产品装配层加入 workspaces。bin 的分发把 `web` 和 `-p`/`--prompt` 保留给 PR #443(它们以指引退出),使两个分支能以接近并集的方式合并;其余一切都运行默认表面:交互式 TUI,加载随仓库提供的 `examples/tui-agent/cordis.yml`(或显式的配置参数),并以调用目录为工作区。已提交的 `bin/dsh` 启动器通过自身真实路径解析 checkout,用仓库的 tsx **从源码**运行该 bin(带 `--expose-internals`,供配置里的 HMR 配置项使用),因此 `ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh` 安装的命令永远执行当前工作树。`pnpm run demo:tui` 运行同一入口。 -**个人配置(`dsh-app-boot`)。** 个人配置目录按 `$DSH_CONFIG_HOME`、其次 `$XDG_CONFIG_HOME/dsh`、最后 `~/.config/dsh` 解析(`resolvePersonalConfigDir`;空变量视为未设置)。dsh 的 TUI 表面消费其中两个可选文件;各示例 bin 仍然逐字节按已提交的配置树启动: +**个人配置(`dsh-app-boot`)。** 个人 overlay 存放在 Harness home——`$DSH_HOME`,否则 `~/.dsh`——由共享的 [`resolveDshHome`](../architecture/2026-07-24-single-harness-home-resolver.md)(`@deepseek-ai/dsh-paths`)解析,与 skills、AGENTS.md 解析所依据的单一根目录相同。dsh 的 TUI 表面消费其中两个可选文件;各示例 bin 仍然逐字节按已提交的配置树启动: - `.env`——在调用目录的 `.env` 之后加载;`process.loadEnvFile` 从不覆盖已有值,因此优先级为环境变量 > 项目 `.env` > 个人 `.env`。 - `config.yaml`——顶层 YAML 数组,元素为 `@cordisjs/plugin-include` 的 `PatchOptions`,用 include 自己的 `!!js` 方言解析(`loadPersonalPatches`)并传给 `boot()`,由它作为根 include 的 `patches` 转发。补丁语义与已提交 overlay 完全一致(Code Mode overlay 是模板):按 id 定位的补丁替换该配置项的整个 `config`,`insert` 追加配置项,未匹配的 id 记录警告并跳过。 - 文件缺失即无 overlay;文件存在但不可读、不可解析或非数组则在启动时抛出(配置错误响亮失败,绝不静默跳过)。 -PTY 冒烟测试的启动器把 `DSH_CONFIG_HOME` 隔离到每个测试自己的目录,与它已有的 `DSH_HOME`/`DSH_AGENTS_HOME` 隔离方式完全一致,开发者真实的个人 overlay 不可能泄漏进 fixture;只有 dsh CLI 读取个人配置,因此其他测试启动器无需改动。 +PTY 冒烟测试的启动器把 `$DSH_HOME` 隔离到每个测试自己的目录,与它已有的 `DSH_AGENTS_HOME` 隔离方式完全一致,开发者真实的个人 overlay 不可能泄漏进 fixture;只有 dsh CLI 读取个人配置,因此其他测试启动器无需改动。 与热重载的交互:include 在每次配置重读时重新应用其 `patches`(见[配置热重载韧性 Agent Note](../bug-fix/2026-07-20-config-hot-reload-resilience.md)),因此运行中编辑 `cordis.yml` 后个人 overlay 仍保持生效。 @@ -41,9 +41,9 @@ PTY 冒烟测试的启动器把 `DSH_CONFIG_HOME` 隔离到每个测试自己的 - 在任意目录运行 `dsh`(以及 `pnpm run demo:tui`)即可零仓库改动地使用个人提供方/模型;已针对个人 Anthropic 代理与 Opus 4.8 端到端验证,包括一次 bash 工具往返。 - 由于按 id 定位的补丁替换整个 `config`,个人覆盖必须复述它保留的基础字段,并可能随基础配置项形态变化而漂移;loader 的「配置项未找到/名称不匹配」警告是仅有的诊断。 - 个人补丁只在被启动文件自身的树里解析 id,因此嵌套 include 的 overlay(Code Mode)不会被个性化;这些叶子的实际运行等价性暂缓。 -- `dsh-app-boot` 新增一个真实依赖(`js-yaml`)和一份只用于加载的 include `!!js` YAML 类型副本。 +- `dsh-app-boot` 依赖 `js-yaml`(外加一份只用于加载的 include `!!js` YAML 类型副本),并与 `apps/cli` 一样依赖 `@deepseek-ai/dsh-paths` 以获取 `resolveDshHome`。 - PR #443 落地时,`apps/cli/src/bin.ts` 的分发链与 `apps/cli/package.json` 的依赖列表会产生文本冲突;两者都按并集解决(他们的 `web`/`-p` 分支加上我们的默认 TUI 分支)。 ## Testing -`packages/ui/app-boot/tests/personal-config.spec.ts` 固定目录优先级(含空变量回退)、`!!js` 的保留与经真实启动树的端到端插值、insert 配置项、缺失/为空的无操作路径,以及三种响亮失败形态(不可读、不可解析、非数组)。`examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` 在 PTY 里以三种方式启动 dsh bin:无 overlay 的默认配置、个人 `.env` + `config.yaml` 链条(打补丁的欢迎语渲染进横幅)、以及无效个人文件导致的响亮启动失败。既有冒烟与快照套件在一台真实 `~/.config/dsh` overlay 会改变启动模型的机器上通过——靠隔离,不靠运气。 +`packages/ui/app-boot/tests/personal-config.spec.ts` 固定 `!!js` 的保留与经真实启动树的端到端插值、insert 配置项、默认目录从 `$DSH_HOME` 解析、缺失/为空的无操作路径,以及三种响亮失败形态(不可读、不可解析、非数组)。`examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` 在 PTY 里以三种方式启动 dsh bin:无 overlay 的默认配置、个人 `.env` + `config.yaml` 链条(打补丁的欢迎语渲染进横幅)、以及无效个人文件导致的响亮启动失败。既有冒烟与快照套件在一台真实 `~/.dsh` overlay 会改变启动模型的机器上通过——靠隔离,不靠运气。 diff --git a/.agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.i18n.yaml index 3d91d63789..4e0393bede 100644 --- a/.agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.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 -2026-07-21-tui-todo-write-opt-in.md: 30797568f2b37180f8a9c837cb4ae7f10b6125eb -2026-07-21-tui-todo-write-opt-in.zh.md: 29a779bbd765a42a12e8561c87375d742dd8bd4b +2026-07-21-tui-todo-write-opt-in.md: f89f76a462f4d30960254833ab71973f6a4f7655 +2026-07-21-tui-todo-write-opt-in.zh.md: f80d2639612819975f03aea9771019cd5237a2ee diff --git a/.agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.md b/.agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.md index 30797568f2..f89f76a462 100644 --- a/.agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.md +++ b/.agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.md @@ -10,7 +10,7 @@ The shipped tui-agent `cordis.yml` loaded `@deepseek-ai/dsh-tool-todo`, exposing ## Decision -The tui-agent `cordis.yml` no longer loads `tool-todo`; `todo_write` is opt-in. The `code-mode.cordis.yml` overlay inherits the base composition, so its generated SDK drops `todo_write` too. Enabling it is one entry — add `@deepseek-ai/dsh-tool-todo` to `cordis.yml` (or a `~/.config/dsh` personal overlay) — after which the model logs the whole-list `todo/write` snapshot and the TUI renders the plan, unchanged. The `TodoItem` type and the `todo/write` event stay in `@deepseek-ai/dsh-session` and the TUI's plan rendering stays wired, so both the default (disabled) and opt-in (enabled) paths are first-class. The sibling acp-agent, headless-agent, and jsonrpc-agent examples still ship the tool. +The tui-agent `cordis.yml` no longer loads `tool-todo`; `todo_write` is opt-in. The `code-mode.cordis.yml` overlay inherits the base composition, so its generated SDK drops `todo_write` too. Enabling it is one entry — add `@deepseek-ai/dsh-tool-todo` to `cordis.yml` (or a `~/.dsh` personal overlay) — after which the model logs the whole-list `todo/write` snapshot and the TUI renders the plan, unchanged. The `TodoItem` type and the `todo/write` event stay in `@deepseek-ai/dsh-session` and the TUI's plan rendering stays wired, so both the default (disabled) and opt-in (enabled) paths are first-class. The sibling acp-agent, headless-agent, and jsonrpc-agent examples still ship the tool. ## Alternatives considered diff --git a/.agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.zh.md b/.agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.zh.md index 29a779bbd7..f80d263961 100644 --- a/.agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.zh.md +++ b/.agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.zh.md @@ -10,7 +10,7 @@ Status: implemented ## Decision -tui-agent `cordis.yml` 不再加载 `tool-todo`;`todo_write` 改为可选启用。`code-mode.cordis.yml` 覆盖配置继承基础组合,因此它生成的 SDK 同样不再包含 `todo_write`。启用它只需一条配置项——把 `@deepseek-ai/dsh-tool-todo` 加入 `cordis.yml`(或 `~/.config/dsh` 的个人覆盖配置)——此后模型照旧记录整份清单的 `todo/write` 快照,TUI 照旧渲染该计划。`TodoItem` 类型与 `todo/write` 事件仍留在 `@deepseek-ai/dsh-session`,TUI 的计划渲染也保持接线,因此默认(禁用)与可选启用(启用)两条路径都是一等公民。同类的 acp-agent、headless-agent、jsonrpc-agent 示例仍然出厂携带该工具。 +tui-agent `cordis.yml` 不再加载 `tool-todo`;`todo_write` 改为可选启用。`code-mode.cordis.yml` 覆盖配置继承基础组合,因此它生成的 SDK 同样不再包含 `todo_write`。启用它只需一条配置项——把 `@deepseek-ai/dsh-tool-todo` 加入 `cordis.yml`(或 `~/.dsh` 的个人覆盖配置)——此后模型照旧记录整份清单的 `todo/write` 快照,TUI 照旧渲染该计划。`TodoItem` 类型与 `todo/write` 事件仍留在 `@deepseek-ai/dsh-session`,TUI 的计划渲染也保持接线,因此默认(禁用)与可选启用(启用)两条路径都是一等公民。同类的 acp-agent、headless-agent、jsonrpc-agent 示例仍然出厂携带该工具。 ## Alternatives considered diff --git a/apps/cli/README.md b/apps/cli/README.md index 935b9804d9..b8ff616d59 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -8,7 +8,7 @@ The TUI surface: - resumes a persisted session with `dsh --resume ` — the form the TUI prints on exit and lists under `/resume`; the flag sets `RESUME_SESSION_ID` before boot so the shipped config rehydrates that session, and a missing or unreadable id fails loud and exits nonzero; - treats the **invoking directory** as the workspace — sessions, relative paths, and workspace instructions resolve from the cwd; - tells the agent where its own source lives: after boot it adds a prompt section naming this harness checkout, resolved from the launcher's real path so it holds under a PATH symlink and an arbitrary cwd, so the self-referential `cordis` toolset can read and modify it; -- applies the personal overlay from `~/.config/dsh` (see [app-boot's Personal config](../../packages/ui/app-boot/README.md#personal-config)): `.env` fills environment gaps (ambient > project `.env` > personal `.env`), `config.yaml` patches the booted tree. +- applies the personal overlay from `~/.dsh` (see [app-boot's Personal config](../../packages/ui/app-boot/README.md#personal-config)): `.env` fills environment gaps (ambient > project `.env` > personal `.env`), `config.yaml` patches the booted tree. ## Install (developer machine) diff --git a/apps/cli/package.json b/apps/cli/package.json index bae172303a..a8fa488a98 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh", - "description": "dsh CLI: the interactive TUI coding agent, booting the shipped default config with the personal overlay from ~/.config/dsh", + "description": "dsh CLI: the interactive TUI coding agent, booting the shipped default config with the personal overlay from ~/.dsh", "version": "0.0.1", "private": true, "type": "module", @@ -13,6 +13,7 @@ ], "license": "BSD-3-Clause", "dependencies": { - "@deepseek-ai/dsh-app-boot": "workspace:^" + "@deepseek-ai/dsh-app-boot": "workspace:^", + "@deepseek-ai/dsh-paths": "workspace:^" } } diff --git a/apps/cli/src/tui.ts b/apps/cli/src/tui.ts index befe954806..6f97a68ad3 100644 --- a/apps/cli/src/tui.ts +++ b/apps/cli/src/tui.ts @@ -1,8 +1,8 @@ /** * `dsh` default surface — the interactive TUI coding agent. Boots the shipped * tui-agent config (or an explicit config argument) with the personal overlay - * from `~/.config/dsh`: its `.env` fills environment gaps (precedence: ambient - * environment, then the invoking directory's `.env`, then the personal one) + * from the Harness home (`~/.dsh`): its `.env` fills environment gaps (precedence: + * ambient environment, then the invoking directory's `.env`, then the personal one) * and its `config.yaml` patches the booted tree. The workspace is the invoking * directory: sessions, relative paths, and workspace instructions resolve from * the cwd, so `dsh` acts on whatever project it is launched in. After boot, the @@ -20,8 +20,8 @@ import { loadPersonalPatches, parseResumeArg, resolveConfigPath, - resolvePersonalConfigDir, } from '@deepseek-ai/dsh-app-boot' +import { resolveDshHome } from '@deepseek-ai/dsh-paths' const NAME = 'dsh' @@ -60,7 +60,7 @@ export async function runTui(argv: string[]): Promise { installFailLoud(NAME) // The bin already loaded the invoking directory's .env; the personal .env // only fills what is still unset (process.loadEnvFile never overrides). - loadEnv(NAME, resolvePersonalConfigDir()) + loadEnv(NAME, resolveDshHome()) // An explicit `--resume` flag beats any ambient RESUME_SESSION_ID, so set it // after loadEnv and before boot reads it through the config's `!!js`. const { resumeSessionId, rest } = parseResumeArg(argv) diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json index c0f389409b..394a475c8f 100644 --- a/apps/cli/tsconfig.json +++ b/apps/cli/tsconfig.json @@ -10,6 +10,9 @@ "references": [ { "path": "../../packages/ui/app-boot" + }, + { + "path": "../../packages/util/paths" } ] } diff --git a/packages/ui/app-boot/tests/personal-config.spec.ts b/packages/ui/app-boot/tests/personal-config.spec.ts index de29c6d0bf..5d72238cfa 100644 --- a/packages/ui/app-boot/tests/personal-config.spec.ts +++ b/packages/ui/app-boot/tests/personal-config.spec.ts @@ -1,48 +1,27 @@ /** - * Personal-config behavior of `dsh-app-boot`: the `~/.config/dsh` directory - * resolution, the `config.yaml` overlay loader, and `boot()` applying the - * personal overlay over a real Loader tree. + * Personal-config behavior of `dsh-app-boot`: the Harness home (`~/.dsh`) + * `config.yaml` overlay loader and `boot()` applying the personal overlay over + * a real Loader tree. */ import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' -import { join, resolve, sep } from 'node:path' +import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import type { Context } from 'cordis' import { boot, - DSH_CONFIG_HOME_ENV, loadPersonalPatches, PERSONAL_CONFIG_FILENAME, - resolvePersonalConfigDir, } from '../src/index.ts' const NAME = 'dsh-test-bin' const tmp = (): string => mkdtempSync(join(tmpdir(), 'dsh-personal-config-')) -describe('resolvePersonalConfigDir', () => { - it('prefers $DSH_CONFIG_HOME, then $XDG_CONFIG_HOME/dsh, then ~/.config/dsh', () => { - const home = `${sep}home${sep}user` - expect(resolvePersonalConfigDir({ DSH_CONFIG_HOME: `${sep}explicit`, XDG_CONFIG_HOME: `${sep}xdg` }, home)) - .toBe(resolve(`${sep}explicit`)) - expect(resolvePersonalConfigDir({ XDG_CONFIG_HOME: `${sep}xdg` }, home)) - .toBe(resolve(`${sep}xdg`, 'dsh')) - expect(resolvePersonalConfigDir({}, home)).toBe(resolve(home, '.config', 'dsh')) - }) - - it('treats empty variables as unset and defaults to the real env and home', () => { - const home = `${sep}home${sep}user` - expect(resolvePersonalConfigDir({ DSH_CONFIG_HOME: '', XDG_CONFIG_HOME: '' }, home)) - .toBe(resolve(home, '.config', 'dsh')) - // Default-arg arm: resolves against the ambient environment without throwing. - expect(resolvePersonalConfigDir().length).toBeGreaterThan(0) - }) -}) - describe('loadPersonalPatches', () => { afterEach(() => { - delete process.env.DSH_CONFIG_HOME + delete process.env.DSH_HOME }) it('returns undefined when no personal patches file exists', () => { @@ -70,10 +49,10 @@ describe('loadPersonalPatches', () => { expect(patches?.[1]?.insert).toHaveLength(1) }) - it('defaults its directory to the resolved personal config dir', () => { + it('defaults its directory to the Harness home ($DSH_HOME)', () => { const dir = tmp() writeFileSync(join(dir, PERSONAL_CONFIG_FILENAME), '- id: x\n config:\n a: 1\n') - process.env[DSH_CONFIG_HOME_ENV] = dir + process.env.DSH_HOME = dir expect(loadPersonalPatches(NAME)).toHaveLength(1) }) diff --git a/scripts/install.sh b/scripts/install.sh index d54c52bad3..cb3b04ead4 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -6,8 +6,8 @@ # It clones the harness to ~/.dsh/source, checks host dependencies (git, Node, # pnpm) and offers to install a missing pnpm, runs `pnpm install` (no build — # the `bin/dsh` launcher runs the TypeScript source through the repo's own tsx), -# symlinks `dsh` onto PATH, records your API credentials in the personal config -# dir dsh reads at boot, and drops you into `dsh`. +# symlinks `dsh` onto PATH, records your API credentials in the Harness home +# (`~/.dsh`) dsh reads at boot, and drops you into `dsh`. # # When run from inside an existing checkout (e.g. `sh scripts/install.sh` rather # than `curl ... | sh`) it reuses that checkout and skips the clone/update, leaving @@ -23,7 +23,7 @@ # DSH_REPO clone URL (default: the GitHub repo) # DSH_SOURCE checkout location (default: ~/.dsh/source) # DSH_BIN_DIR directory the `dsh` symlink lands in (default: ~/.local/bin) -# DSH_CONFIG_HOME personal config dir (also XDG_CONFIG_HOME/dsh, ~/.config/dsh) +# DSH_HOME Harness home holding the personal config (default: ~/.dsh) set -eu DSH_REF=${DSH_REF:-master} @@ -224,13 +224,11 @@ if [ "$ON_PATH" = 0 ]; then fi # --- 5. credentials ------------------------------------------------------------ -# Mirror app-boot's resolvePersonalConfigDir precedence so creds land where dsh reads them. -if [ -n "${DSH_CONFIG_HOME:-}" ]; then - CONF="$DSH_CONFIG_HOME" -elif [ -n "${XDG_CONFIG_HOME:-}" ]; then - CONF="$XDG_CONFIG_HOME/dsh" +# Mirror app-boot's resolveDshHome precedence ($DSH_HOME, else ~/.dsh) so creds land where dsh reads them. +if [ -n "${DSH_HOME:-}" ]; then + CONF="$DSH_HOME" else - CONF="$HOME/.config/dsh" + CONF="$HOME/.dsh" fi ENV_FILE="$CONF/.env"