clearai-dsh 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,74 @@
2
2
 
3
3
  All notable changes to this project are recorded here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
4
 
5
+ ## [0.1.4] — 2026-09-16
6
+
7
+ **本版重点:子任务的交付链修好了。** 侦察与世界线执行者的结论此前只进账本、模型读不到
8
+ (账本里也有过「派出去就再也没人收」的挂空)。现在四类子任务(侦察 / 世界线执行者 /
9
+ 评估者 / 横评仲裁)统一走原生 `subagents.start()` 的一次性句柄:账本只认本进程攥着的
10
+ `run.result`,结论正文由**收集那一刻的工具返回**交给模型,全文另落
11
+ `clear/knowledge/materials/<id>.md` 供模型、独立评估者与人共读。
12
+ 试过的另一条路(拿运行时的结算通知当账本信号)已撤回——它是 best-effort,当不了承重结构。
13
+
14
+ ### Added
15
+
16
+ - **ClearAI's own `/` command menu.** `/goal` `/plan` `/evidence` `/worldline` are read-only state windows computed from the ledger on the spot; `/plan-review` re-presents the active plan through the native review card instead of stamping anything itself (commands carry no mutation channel — the authority boundary test pins this).
17
+ - **Native working tools return.** todo, subagent (with model selection), workflow and ralph mount from the standard preset's own rows; the composition suite pins both directions — present: these four; absent: `tool-goal`, `command-goal`, `plan-mode` (the second ledger stays off).
18
+ - **`test/prompt-sections.test.mjs`.** All 23 prompt sections carry a `hard` / `native` / `advisory` class tag, and the suite pins that the classification matches the content (hard sections name a mechanism anchor; native sections name no kernel tool; advisory sections make no mechanism promises).
19
+
20
+ ### Changed
21
+
22
+ - **Hypothesis floor is now a hard boundary.** `SetGoal` rejects zero or one hypotheses when `minHypotheses > 0` (kernel default 0 stays neutral; the preset sets 2). Revisions of an existing goal are exempt.
23
+ - **Authorization wording unified to one sentence everywhere.** Kernel messages, the runtime card and the prompts all say: an unapproved plan does not auto-continue; when you advance it explicitly, the first delivery records attribution as it happened (behaviour is authorization). The card says it in human words — ledger field names no longer appear.
24
+ - **Stale native-tool contracts rewritten.** `edit` is literal replacement, not unified diff; `web_search`/`web_fetch` parameter references that no longer exist were removed.
25
+ - **Comment debt cleared to zero.** ~310 comments rewritten to the style rule (why / what breaks / boundary — no dates, no internal section numbers, no incident narratives); the ratchet quotas are now {0, 0, 0}.
26
+
27
+ ### Changed
28
+
29
+ - **Async sub-runs now deliver their conclusions through the runtime's own settlement notice.** `SpawnScout`, `MapScouts` and the worldline executors are started with `subagents.startContinuable()`, whose Activation delivers the child's closing message to the parent as a durable user message; the kernel keeps owning only what it must (the `scout/dispatched` / `scout/settled` ledger, the full-text material file under `clear/knowledge/materials/`, and the pointer on the runtime card). Evaluators and the arbitration reviewer stay on the one-shot path: the native durable-child descriptor deliberately omits `outputSchema`, which belongs to a one-shot activation's result contract.
30
+ - **Scout conclusions are persisted in full** to `clear/knowledge/materials/<id>.md` so the model, the independent evaluator and the human read the same copy; the ledger and the runtime card carry a pointer plus a bounded excerpt, and over-long text is marked `…truncated (N chars total, see <path>)` instead of being silently cut. The scout persona now caps its answer at 3000 characters.
31
+ - **A goal's closure records the hypotheses nobody touched.** `goal/closed` carries `unjudged`, the card writes `(untouched)` for a hypothesis with no evidence at all (distinct from "judged inconclusive"), and the loop contract asks for either one touch of evidence or an explicit note about why there was none — no verdict is ever forced.
32
+
33
+ ### Fixed
34
+
35
+ - **`install.sh` aborted on macOS (bash 3.2).** It expanded an empty array as `"${OLD_PANEL_PKGS[@]}"` under `set -u`; bash only tolerates that from 4.4 on, while macOS ships 3.2 — so the documented developer install died at step ② for every macOS contributor. CI runs on Linux (bash 5), which is why it never caught it. Both expansions now use the portable `${arr[@]+"${arr[@]}"}` form.
36
+ - **Long-run evidence was overwritten or lost.** Every run now gets its own timestamped archive: the light half (stdout, structured result, provenance, decoded one-line-per-event trajectory, append-only index) is committed, while the heavy half (workspace, raw session log) stays on disk under `~/.dsh/e2e-archive/` so it can be re-judged offline with `tools/e2e-replay.mjs`. `--workspace` pointing inside any git repository is now refused outright: ClearAI commits each delivery into the workspace's own repository, so an in-repo workspace had the kernel commit its delivery snapshots — and the author's uncommitted work — into the host project.
37
+ - **Session-directory name derivation dropped dots.** DSH keeps `.` (and `_`) when it slugs a workspace path; the old rule folded both away, so a workspace under `~/.dsh/…` was reported as "no session log" (38 assertions red in one run). The rule is now taken from a real directory comparison.
38
+ - **Scout conclusions never came back in a scouts-only run.** `AwaitWorldlines` decided whether to keep waiting from the wait-lines produced by `sweepWorldlineExecutors()`, and `sweepScouts()` never produced one — so with only scouts in flight the loop exited on its first tick, even though `SpawnScout`'s own reply tells the model to "wait for it this turn with `AwaitWorldlines`". The one-shot form added a second layer: with no next turn, a conclusion that settled after the last tool call never met another collection point. `sweepScouts()` now reports how many scouts are still unsettled, `collectExecutors()` passes it through, and `AwaitWorldlines` counts it. Verified end to end: the same scenario that stalled twice (goal left open, evaluator refusing `inconclusive`) now finishes 36/36 with the conclusion in the material surface and the goal `achieved`.
39
+ - **Scout conclusions were invisible to the model even after they settled.** They landed only in the mutation record while the section the model reads every step is the runtime card — which had no material surface. Now delivery rides the native settlement notice, the card lists foreign observations (pointer + excerpt) plus the scouts still in flight, and a long-run invariant asserts that an async sub-run's conclusion appears in model-visible text rather than only in the ledger.
40
+ - **Scouts had no "lost" ending.** Worldline executors and evaluators already wrote one; a scout whose child was gone stayed "not yet reported" forever. The judgement now mirrors the evaluator's: in-process dispatches are alive, the native child catalog decides what is still running, and an unavailable read surface means *no judgement* rather than a fabricated one.
41
+ - **The prompt described `SpawnScout` as synchronous.** It said the conclusion comes straight back as the return value and that the tool waits; the kernel is deliberately asynchronous (the blocking wait used to lose the "dispatched" fact when a run was interrupted). The delegation table now teaches the real contract (fire-and-forget, conclusion replays into the material surface, wait with `AwaitWorldlines`), and a drift check pins it.
42
+ - **`AwaitWorldlines` counted mutations, not conclusions.** One scout writes two mutations (`scout/settled` plus the observation), so a single scout was reported as "回灌 2 条". The count and wording now speak of conclusions.
43
+ - **`clearai-commands` cross-plane import.** It imported `ui/lib/fold.js` from the preset plane; in the installed package the relative layout differs, so switching to the preset in a browser failed at import. The command renderers now use the host-provided `clearai` facade for `derive` as well, and the boundary suite pins that no preset plugin imports across planes.
44
+ - **macOS temp-path realpath mismatches.** Session-log lookup and clean-install workspace registration now resolve realpaths (`/var` is a symlink to `/private/var`), which had made e2e logs unfindable and browser session attach fail.
45
+
46
+ ### Changed (sub-run lifecycle, unified)
47
+
48
+ - **All four sub-run kinds now share one native lifecycle.** Scout, worldline executor, evaluator and the arbitration reviewer all go through one-shot `subagents.start()` handles; the ledger settles from the `run.result` this process holds, and the conclusion text reaches the model in the tool return of the collecting call. The attempt to use the runtime's settlement notice as a ledger signal is withdrawn: a notice is best-effort, and a live kernel could not reliably see it through either the projection or its own session log. Role differences are now only persona, tool face, and how the result is interpreted — permission and authority boundaries are unchanged.
49
+ - **A scout's conclusion is recorded even when its in-memory entry exists.** The old collection guards skipped exactly the sub-runs the table was holding, so a scout could sit "dispatched, never collected" forever. The sweep now walks every unsettled sub-run in the projection, and de-duplication moved to a session+epoch map that also works after a restart.
50
+
51
+ ### Tooling
52
+
53
+ - **Long-run end-to-end scenarios with offline re-judging.** Five scenarios (`worldline-arbitration`, `falsification`, `long-plan`, `scout-first`, `goal-chain`) plus a set of cross-mechanism invariants (no advance without admission, no dangling evaluator, no orphaned fork, promotion level consistency, no dangling scout, evidence bound to real steps, declared artifacts on disk). `tools/e2e-parallel.mjs` runs them concurrently (cap 3, because each run spawns its own worldline executors and evaluators), `tools/e2e-replay.mjs` re-judges a saved session log without spending tokens, and `test/e2e-scenarios.test.mjs` pins every invariant with a negative case so a mis-written judge cannot report a false green.
54
+
55
+ ### Validated
56
+
57
+ - **deepseek-flash end-to-end, two headless scenarios** (24/24 plan-and-stop; 25/25 full completion including independent-evaluator settlement) and **one real-browser session** (clean install + Chrome): preset switching, native review-card approval landing `by='user'`, the full thirteen-beat chain, `/goal` rendering, and all four panels drawing — screenshots in `docs/shots/browser-e2e-*.png`.
58
+
59
+ ## [0.1.3] — 2026-09-15
60
+
61
+ ### Added
62
+
63
+ - **`npx clearai-dsh install` — one command, and the only prerequisite left is DSH's own.** The published package has always carried an install-side tool, but it only *diagnosed*: `doctor`, `root-yaml`, `seed`, `unseed`. The installer that could actually place the package lived in `tools/install-native.mjs`, which is not in the published files — so a stranger had nothing to run but `dsh plugin … add`, a command whose first word assumes a `dsh` that an `npx`-launched harness never puts on `PATH`. The new `install` verb resolves the CLI (a `dsh` on `PATH`, else `npx --yes @deepseek-ai/dsh`), installs into the profile, and then reads the composed config back to show that the `clearai-host` row really landed. `--dist` / `--tarball` / `--spec` point it at a local build instead of the registry, which is what the lifecycle check now exercises.
64
+
65
+ ### Changed
66
+
67
+ - **The install instructions no longer teach a mechanism we do not own.** They handed the reader a `corepack enable` line as the way to get pnpm. Corepack is a version *router*, not an install: its 186-byte shim fetches a pnpm on first use, and corepack 0.34 — the one Node 24 ships — launches pnpm by looking for `bin/pnpm.cjs`, which pnpm 11 and later no longer provide. It can therefore fetch a version it is unable to run, and its shims can shadow a pnpm that already worked. The docs now name the requirement (a `pnpm` on `PATH`, which is DSH's rather than ours) and leave the choice of how to satisfy it to the reader.
68
+ - `install` does not bootstrap a profile or hand-reconcile one. The CLI initializes a profile the first time it is used for one (`initialized profile web at …`), and a second implementation of the host's reconcile step is exactly the duplication this project rejects. Passing a shipped profile name to `--from-default-profile` is an error in the CLI (`profile "web" is shipped and cannot be a custom profile target`), so the verb does not offer that flag at all.
69
+ - `install` stops when `pnpm` is missing instead of degrading: pnpm is DSH's prerequisite, not this plugin's. The degraded, pnpm-less path stays in `tools/install-native.mjs`, where it exists for one-shot E2E homes and labels itself as degraded.
70
+ - **`doctor` asks the composed config through a `dsh` on `PATH` first**, falling back to `npx --no-install`. It previously always went through npx, so a machine that had the CLI on `PATH` could still be told the composition could not be determined.
71
+ - **The lifecycle check had a gate that could never open.** Its byte-for-byte comparison included `INVENTORY.txt` — the build's own file manifest, which is not in `files` and is therefore never present in a pnpm-installed copy — so that assertion was red on every run, and because the lifecycle check is not part of CI, nobody saw it. It also drove every compose query through `npx --no-install`, which returns an empty string when npx cannot run: two positive assertions failed while the negative one ("the row is gone") passed on that empty output. Both are fixed — the CLI is resolved from `PATH` first, and the comparison ignores the build manifest — and the check now exercises the shipped `install` verb too (28 checks).
72
+
5
73
  ## [0.1.2] — 2026-09-12
6
74
 
7
75
  ### Fixed
package/README.md CHANGED
@@ -22,6 +22,32 @@ A language model can produce a plausible answer in seconds. ClearAI is about wha
22
22
 
23
23
  ---
24
24
 
25
+ ## Install
26
+
27
+ One command, and it needs nothing but Node:
28
+
29
+ ```bash
30
+ npx clearai-dsh install
31
+ ```
32
+
33
+ It resolves the DSH CLI (from your `PATH`, or through `npx`), installs the plugin into your `web` profile, and reads the composed config back so you are not taking "success" on faith. Underneath it is the host's own install, so this is the same command: `dsh plugin --profile web add clearai-dsh`.
34
+
35
+ **Restart `dsh web` after that** (`npx @deepseek-ai/dsh web`). Both halves of the plugin are cached inside the running process, so refreshing the browser is not enough. Then open a session and pick **ClearAI** in the preset picker.
36
+
37
+ If it stops because **pnpm is not on your `PATH`**: DSH manages a profile by driving pnpm, so it needs one. Install it with `npm install -g pnpm`, or your system package manager. Prefer that to `corepack enable`, which installs a version *router* rather than pnpm, and the corepack shipped with current Node can fetch a pnpm it is unable to launch.
38
+
39
+ From a checkout (development, not the install path):
40
+
41
+ ```bash
42
+ npm test # kernel / host / brain / client / ontology suites
43
+ node tools/build-package.mjs # assemble dist/ from source
44
+ node tools/verify-package.mjs # rebuild and compare byte-for-byte
45
+ node tools/verify-clean-install.mjs # install into an empty DSH_HOME through the real CLI
46
+ node docs/diagrams/build.mjs # regenerate the loop diagram (needs google-chrome)
47
+ ```
48
+
49
+ `dist/` is generated and never committed. See [DSH integration](docs/dsh-integration.md).
50
+
25
51
  ## Why this is not just another agent loop
26
52
 
27
53
  Most agent loops track one thing: whether the task is done. The Epistemic Loop also tracks **how a conclusion came to be trusted**:
@@ -73,32 +99,9 @@ The plugin contributes three surfaces on top of stock DSH: a **deliverables** vi
73
99
 
74
100
  ![External brain](docs/shots/en/skills.png)
75
101
 
76
- ## Install
77
-
78
- Requires **Node ≥ 22** and **`pnpm` on `PATH`** — `dsh plugin …` is a pnpm forwarder, so without pnpm the profile cannot be managed at all:
79
-
80
- ```bash
81
- corepack enable --install-directory ~/.local/bin # if you do not have pnpm yet
82
- dsh plugin --profile web add clearai-dsh
83
- ```
84
-
85
- **Restart `dsh web` afterwards.** Both halves of the plugin are cached inside the running process, so refreshing the browser is not enough. Then open a session and pick **ClearAI** in the preset picker.
86
-
87
- From a checkout:
88
-
89
- ```bash
90
- npm test # kernel / host / brain / client / ontology suites
91
- node tools/build-package.mjs # assemble dist/ from source
92
- node tools/verify-package.mjs # rebuild and compare byte-for-byte
93
- node tools/verify-clean-install.mjs # install into an empty DSH_HOME through the real CLI
94
- node docs/diagrams/build.mjs # regenerate the loop diagram (needs google-chrome)
95
- ```
96
-
97
- `dist/` is generated and never committed. See [DSH integration](docs/dsh-integration.md).
98
-
99
102
  ## Where it lands in DSH
100
103
 
101
- ClearAI adds an epistemic layer on the DSH **composition surface** — one host package, one agent preset, one client module. The DSH engine is not modified.
104
+ ClearAI adds an epistemic layer on the DSH **composition surface** — one host package, one agent preset, one client module. The DSH engine is not modified. `/goal` `/plan` `/evidence` `/worldline` `/plan-review` are the human's read-only state windows in the `/` menu (computed from the ledger on the spot); todo, subagents, workflows and model switching are DSH-native — working style is unbounded, but none of it can write the authoritative ledger (the authority boundary is pinned by tests).
102
105
 
103
106
  ![ClearAI in DSH](docs/diagrams/loop-to-dsh-planes.svg)
104
107
 
@@ -120,6 +123,7 @@ They are illustrations of the mechanism, not shipped run records.
120
123
  - [Glossary](docs/glossary.md)
121
124
  - [Loop philosophy](docs/loop-philosophy.md) · [Verification ontology](docs/verification-loop.md)
122
125
  - [Known gaps](docs/known-gaps.md) · [Release verification](docs/release-verification.md)
126
+ - [Convergence and slimming plan](docs/optimization/plan.md) · [Full-coverage design](docs/optimization/epistemic-coverage.md) · [Execution progress](docs/optimization/progress.zh-CN.md)
123
127
 
124
128
  ## Work attribution
125
129
 
package/README.zh-CN.md CHANGED
@@ -22,6 +22,32 @@ ClearAI 是一个**原生 DSH 插件**,把认识论循环带进 DeepSeek Harne
22
22
 
23
23
  ---
24
24
 
25
+ ## 安装
26
+
27
+ 一条命令,除了 Node 什么都不需要:
28
+
29
+ ```bash
30
+ npx clearai-dsh install
31
+ ```
32
+
33
+ 它会自己找到 DSH CLI(PATH 上有就用,没有就走 npx),把插件装进你的 `web` profile,再把组合读回来核一眼 —— 不用凭一句「成功」相信它。底下就是宿主自己的安装动作,所以两者等价:`dsh plugin --profile web add clearai-dsh`。
34
+
35
+ **装完要重启 `dsh web`**(`npx @deepseek-ai/dsh web`)。插件的两半都在运行中的进程里按模块 URL 缓存,只刷新浏览器不够。然后新建会话,在预设选择器里选 **ClearAI**。
36
+
37
+ 如果它因为 **PATH 上没有 pnpm** 而停下:DSH 管理 profile 就是靠 pnpm,所以需要一个。用 `npm install -g pnpm` 装,或用你的系统包管理器。**别用 `corepack enable` 抄近路**——它装的是一个版本**转发器**而不是 pnpm,而当前 Node 自带的那份 corepack 可能下载一个它自己启动不了的 pnpm。
38
+
39
+ 从仓库开发(这是开发路径,不是安装路径):
40
+
41
+ ```bash
42
+ npm test # 内核 / 宿主 / 外脑 / 客户端 / 本体 五份套件
43
+ node tools/build-package.mjs # 由源装配 dist/
44
+ node tools/verify-package.mjs # 现场重建并逐字节比对
45
+ node tools/verify-clean-install.mjs # 空 DSH_HOME + 真 CLI 装一遍(16 条断言)
46
+ node docs/diagrams/build.mjs # 重画循环主图(需 google-chrome)
47
+ ```
48
+
49
+ `dist/` 是生成物,不进版本库。见 [DSH 集成](docs/dsh-integration.zh-CN.md)。
50
+
25
51
  ## 为什么它不只是又一个 agent loop
26
52
 
27
53
  多数 agent loop 只跟踪一件事:任务做完没有。认识论循环还跟踪**一个结论凭什么被信任**:
@@ -73,32 +99,9 @@ ClearAI **不**声称实现递归自我改进。它提供的是自我改进系
73
99
 
74
100
  ![外脑](docs/shots/zh/skills.png)
75
101
 
76
- ## 安装
77
-
78
- 需要 **Node ≥ 22** 和 **`pnpm` 在 PATH 上** —— `dsh plugin …` 是 pnpm 的一层转发器,没有 pnpm 就管不了 profile:
79
-
80
- ```bash
81
- corepack enable --install-directory ~/.local/bin # 还没有 pnpm 就先装它
82
- dsh plugin --profile web add clearai-dsh
83
- ```
84
-
85
- **装完要重启 `dsh web`。** 插件的两半都在运行中的进程里按模块 URL 缓存,只刷新浏览器不够。然后新建会话,在预设选择器里选 **ClearAI**。
86
-
87
- 从仓库开发:
88
-
89
- ```bash
90
- npm test # 内核 / 宿主 / 外脑 / 客户端 / 本体 五份套件
91
- node tools/build-package.mjs # 由源装配 dist/
92
- node tools/verify-package.mjs # 现场重建并逐字节比对
93
- node tools/verify-clean-install.mjs # 空 DSH_HOME + 真 CLI 装一遍(16 条断言)
94
- node docs/diagrams/build.mjs # 重画循环主图(需 google-chrome)
95
- ```
96
-
97
- `dist/` 是生成物,不进版本库。见 [DSH 集成](docs/dsh-integration.zh-CN.md)。
98
-
99
102
  ## 它落在 DSH 的哪一层
100
103
 
101
- ClearAI 把认识论层加在 DSH 的**组合面**上——一个宿主包、一个 agent 预设、一个客户端模块,**DSH 引擎一行都没改**。
104
+ ClearAI 把认识论层加在 DSH 的**组合面**上——一个宿主包、一个 agent 预设、一个客户端模块,**DSH 引擎一行都没改**。`/goal` `/plan` `/evidence` `/worldline` `/plan-review` 是人在 `/` 菜单里的状态窗(只读,从账本现算);todo、子代理、workflow、模型切换用 DSH 原生的——工作方式不设限,但它们写不进权威账本(权威边界由测试钉死)。
102
105
 
103
106
  ![ClearAI 在 DSH 中](docs/diagrams/loop-to-dsh-planes.zh-CN.svg)
104
107
 
@@ -120,6 +123,7 @@ ClearAI 把认识论层加在 DSH 的**组合面**上——一个宿主包、一
120
123
  - [术语表](docs/glossary.zh-CN.md)
121
124
  - [循环哲学](docs/loop-philosophy.zh-CN.md) · [验证本体](docs/verification-loop.zh-CN.md)
122
125
  - [已知缺口](docs/known-gaps.zh-CN.md) · [发布验收](docs/release-verification.zh-CN.md)
126
+ - [收敛与瘦身计划](docs/optimization/plan.zh-CN.md) · [认识论循环全覆盖设计](docs/optimization/epistemic-coverage.zh-CN.md) · [执行进度](docs/optimization/progress.zh-CN.md)
123
127
 
124
128
  ## 工作署名
125
129
 
package/bin/clearai.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * clearai-dsh 的安装侧工具:**只做三件事,而且都不改别人的东西**。
3
+ * clearai-dsh 的安装侧工具。**只有一个动词会动 profile(`install`),而它动的方式是把活交给宿主**。
4
4
  *
5
5
  * 分包的三样东西落在三个平面:
6
6
  * · 宿主半(`lib/host.js`)—— 由包自带的 `cordis.patch.yml` 在 profile 层插一行,`dsh plugin add` 自动生效;
@@ -8,8 +8,11 @@
8
8
  * · agent 预设(`presets/clearai/`)—— **名册(roster)只从 root 目录扫**,包没法直接声明,
9
9
  * 所以要么把 root 指进包里,要么把它播种到用户根。这一件就是本工具存在的理由。
10
10
  *
11
- * 三种处理方式(按「原生程度」排序,默认只**打印**不改):
11
+ * 处理方式(前四个默认只**打印**不改;默认动词仍是 `doctor` —— 一个安装侧工具不该在
12
+ * 你没说要装的时候动你的部署):
12
13
  * doctor 看现状:包在哪、预设在哪、名册能不能看见它、探测到的 dsh / profile 是什么
14
+ * install 把包装进 profile —— `dsh plugin --profile <p> add <spec>` 的一层**前置解析**
15
+ * (读者不必知道 profile 叫什么、CLI 从哪来、包名怎么写),装完给读数与下一步
13
16
  * root-yaml 打印**可以直接粘进 profile 的 `cordis.patch.yml`** 的那一行(路径已算成绝对路径)
14
17
  * seed 把预设**播种**到用户根 `~/.dsh/.agent-presets/<id>`(带哈希记账:改过的不覆盖)
15
18
  * unseed 撤掉播种:只删「我们播的、且没被改过」的文件;用户 fork 的 id 一律不碰
@@ -19,10 +22,10 @@
19
22
  */
20
23
 
21
24
  import { createHash } from 'node:crypto'
22
- import { execFileSync } from 'node:child_process'
25
+ import { spawnSync } from 'node:child_process'
23
26
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
24
27
  import { homedir } from 'node:os'
25
- import { dirname, join, relative, resolve } from 'node:path'
28
+ import { delimiter, dirname, join, relative, resolve } from 'node:path'
26
29
  import { fileURLToPath } from 'node:url'
27
30
 
28
31
  const HERE = dirname(fileURLToPath(import.meta.url))
@@ -85,6 +88,44 @@ function rosterRoots() {
85
88
  return [join(DSH_HOME, '.agent-presets')]
86
89
  }
87
90
 
91
+ /**
92
+ * 在 PATH 上找一个可执行文件,找到就返回全路径。
93
+ *
94
+ * 为什么不用 `bash -lc 'command -v pnpm'`(tools/install-native.mjs 的写法):
95
+ * 这里要能在 Windows 上跑,而 `command -v` 是 shell 内建;另外**只查文件、不执行它** ——
96
+ * 执行一个 pnpm 可能是别的东西(比如 corepack 的转发器)在跑,那会引出网络与副作用。
97
+ */
98
+ function findOnPath(name) {
99
+ const exts = process.platform === 'win32' ? ['.cmd', '.exe', '.bat'] : ['']
100
+ for (const dir of (process.env.PATH ?? '').split(delimiter)) {
101
+ if (dir === '') continue
102
+ for (const ext of exts) {
103
+ const candidate = join(dir, `${name}${ext}`)
104
+ try {
105
+ if (statSync(candidate).isFile()) return candidate
106
+ } catch {
107
+ /* 这个目录里没有 */
108
+ }
109
+ }
110
+ }
111
+ return null
112
+ }
113
+
114
+ /**
115
+ * 问组合:宿主行到底有没有真的进组合。
116
+ *
117
+ * 优先用 PATH 上**真正的** `dsh`(以前一律走 `npx --no-install`,于是 PATH 上有 CLI 的机器
118
+ * 也会报「问不到」);没有 CLI 才退回 npx,且仍然 `--no-install` —— 诊断动作不该顺手下载东西。
119
+ */
120
+ function composeQuery(profileName, env) {
121
+ const onPath = findOnPath('dsh')
122
+ const args = ['--profile', profileName, '--dump-config']
123
+ const run = onPath === null
124
+ ? spawnSync('npx', ['--no-install', '@deepseek-ai/dsh', ...args], { encoding: 'utf8', env, timeout: 120000, stdio: ['ignore', 'pipe', 'ignore'] })
125
+ : spawnSync(onPath, args, { encoding: 'utf8', env, timeout: 120000, stdio: ['ignore', 'pipe', 'ignore'] })
126
+ return run.status === 0 ? String(run.stdout ?? '') : null
127
+ }
128
+
88
129
  function doctor() {
89
130
  const rows = []
90
131
  rows.push(`包目录 ${PKG_DIR}`)
@@ -103,13 +144,7 @@ function doctor() {
103
144
  * **包的补丁层**,不在 profile 的 cordis.patch.yml 里 —— 读文件的写法会误报「还没挂」)。
104
145
  * 事实在组合里;问不到就如实说问不到,不猜。
105
146
  */
106
- const composed = (() => {
107
- try {
108
- return execFileSync('npx', ['--no-install', '@deepseek-ai/dsh', '--profile', profile, '--dump-config'], { encoding: 'utf8', env: { ...process.env, DSH_HOME }, timeout: 120000, stdio: ['ignore', 'pipe', 'ignore'] })
109
- } catch {
110
- return null
111
- }
112
- })()
147
+ const composed = composeQuery(profile, { ...process.env, DSH_HOME })
113
148
  if (composed === null) {
114
149
  rows.push('组合 (问不到:dsh 不可用或超时 —— 下面两条无法判定)')
115
150
  } else {
@@ -125,6 +160,82 @@ function doctor() {
125
160
  console.log('\n提示:doctor 只读;它不会替你改 profile,也不会替你播种。')
126
161
  }
127
162
 
163
+ /**
164
+ * install —— 把包装进一个 profile。
165
+ *
166
+ * 真正干活的**永远是宿主的 CLI**(`dsh plugin --profile <p> add <spec>`):它自己会初始化
167
+ * profile、在 profile 目录里跑 pnpm、再对账 `dsh.profile.bundles`。这里只解析三件事 ——
168
+ * 装什么、装到哪、谁来跑 —— 于是读者不必先读过别的文档。
169
+ *
170
+ * 两条纪律:
171
+ * · **不偷偷降级**。没有 pnpm 就停下来把话说清楚:pnpm 是 DSH 的前置,不是本插件的。
172
+ * 在 profile 里手工摆文件等于把宿主的 reconcile 抄成第二份实现,而它一旦与宿主漂移,
173
+ * 坏的是用户的部署。(降级只保留在 `tools/install-native.mjs` —— 那是一次性 DSH_HOME 上的
174
+ * E2E 需要,而且它如实标注自己是降级。)
175
+ * · **装完给读数**。不靠一句「成功」交差:再问一次组合,看宿主行是不是真的进去了。
176
+ */
177
+ function install() {
178
+ const home = resolve(value('home', DSH_HOME))
179
+ const env = { ...process.env, DSH_HOME: home }
180
+ const profileDir = join(home, 'profiles', profile)
181
+ const manifest = JSON.parse(readFileSync(join(PKG_DIR, 'package.json'), 'utf8'))
182
+ const explicit = value('spec', null)
183
+ const dist = value('dist', null)
184
+ const tarball = value('tarball', null)
185
+ /**
186
+ * 缺省装 **registry 上的这一版**:profile 从此真正拥有它(能升级、能卸载),也不依赖
187
+ * npx 缓存还在。`--dist` / `--tarball` / `--spec` 是给开发与 E2E 用的另一条入口。
188
+ */
189
+ const spec = explicit ?? (tarball !== null ? resolve(tarball) : dist !== null ? `file:${resolve(dist)}` : `${manifest.name}@${manifest.version}`)
190
+ const source = explicit !== null ? '你给的 spec' : tarball !== null ? '本地 tarball' : dist !== null ? '本地目录' : 'registry'
191
+ /** PATH 上有 `dsh` 就用它;没有就用 npx 取官方 CLI(`--yes`:一键安装不该卡在一个确认提示上)。 */
192
+ const dshPath = findOnPath('dsh')
193
+ const route = dshPath === null ? 'npx --yes @deepseek-ai/dsh(PATH 上没有 dsh)' : dshPath
194
+ /**
195
+ * 子进程**继承 stdio**:安装进度、以及 CLI 那句 `initialized profile …` 都如实流到用户眼前;
196
+ * 失败时他看到的也是真实报错,而不是我截出来的尾巴。
197
+ */
198
+ const dsh = (args) => (dshPath === null ? spawnSync('npx', ['--yes', '@deepseek-ai/dsh', ...args], { env, stdio: 'inherit', timeout: 900000 }) : spawnSync(dshPath, args, { env, stdio: 'inherit', timeout: 900000 }))
199
+
200
+ console.log(`【安装】${manifest.name}@${manifest.version}`)
201
+ console.log(` 装什么 ${spec}(${source})`)
202
+ console.log(` 装到哪 ${profileDir}`)
203
+ console.log(` 谁来跑 ${route}`)
204
+
205
+ const pnpmPath = findOnPath('pnpm')
206
+ if (pnpmPath === null) {
207
+ console.error('\n✗ PATH 上没有 pnpm,而 DSH 管理一个 profile 就是靠它:`dsh plugin …` 把参数转发给 pnpm。')
208
+ console.error(' 装一个再来:npm install -g pnpm(或用系统包管理器,如 brew install pnpm)。')
209
+ console.error(' 别用 corepack enable 抄近路 —— 它装的是版本**转发器**而不是 pnpm,而当前 Node 自带的')
210
+ console.error(' 那份 corepack 可能下载一个它自己启动不了的 pnpm。')
211
+ console.error(' 这里刻意不手工改 profile:那等于把宿主的 reconcile 抄成第二份实现,与宿主漂移时坏的是你的部署。')
212
+ process.exit(1)
213
+ }
214
+
215
+ /**
216
+ * **刻意不做 profile bootstrap**:CLI 第一次用到某个 profile 时自己就会初始化它
217
+ * (实测输出 `dsh: initialized profile web at …`),那本来就是宿主的不变量,抄一遍只会
218
+ * 多一个会漂移的实现。顺带说:`--from-default-profile` 只接受**自定义目标**,把 shipped
219
+ * 名字传给它会被 CLI 直接拒 —— `profile "web" is shipped and cannot be a custom profile
220
+ * target`。所以这里连那个开关都不提供。
221
+ */
222
+ const added = dsh(['plugin', '--profile', profile, 'add', spec])
223
+ if (added.status !== 0) {
224
+ console.error('\n✗ 安装失败:见上面的输出。')
225
+ process.exit(1)
226
+ }
227
+
228
+ const composed = composeQuery(profile, env)
229
+ if (composed === null) {
230
+ console.log(' 宿主行 (问不到组合:CLI 不可用或超时 —— 装没装进去,从这里确认不了;用 doctor 再看)')
231
+ } else {
232
+ const inCompose = /^- id: clearai-host$/m.test(composed)
233
+ console.log(` 宿主行 ${inCompose ? '在组合里 ✓' : '**不在组合里** —— 装是装上了,但组合里没看到它(用 doctor 查)'}`)
234
+ }
235
+ console.log('\n 下一步 重启 dsh web(两半都在进程里按模块 URL 缓存,只刷新浏览器不够),然后在预设选择器里选 ClearAI。')
236
+ console.log(` 卸载 dsh plugin --profile ${profile} remove ${manifest.name}(同样可以冠 npx)`)
237
+ }
238
+
128
239
  function rootYaml() {
129
240
  const line = [
130
241
  '# ClearAI 预设的 root(由 clearai-dsh 的 bin 打印,路径已算成绝对路径)',
@@ -214,11 +325,16 @@ function unseed() {
214
325
  }
215
326
 
216
327
  if (command === 'doctor') doctor()
328
+ else if (command === 'install') install()
217
329
  else if (command === 'root-yaml') rootYaml()
218
330
  else if (command === 'seed') seed()
219
331
  else if (command === 'unseed') unseed()
220
332
  else if (command === 'version' || flag('version')) console.log(JSON.parse(readFileSync(join(PKG_DIR, 'package.json'), 'utf8')).version)
221
333
  else {
222
- console.error(`unknown command: ${command}\n用法:clearai-dsh [doctor|root-yaml|seed|unseed] [--profile web] [--root <dir>]`)
334
+ console.error(
335
+ `unknown command: ${command}\n` +
336
+ '用法:clearai-dsh [doctor|install|root-yaml|seed|unseed] [--profile web] [--home <dir>]\n' +
337
+ ' install 还可以:--dist <dir> | --tarball <tgz> | --spec <spec>',
338
+ )
223
339
  process.exit(2)
224
340
  }