pier-setup 0.2.7 → 0.3.1

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.
Files changed (3) hide show
  1. package/README.md +85 -24
  2. package/install.mjs +38 -120
  3. package/package.json +5 -3
package/README.md CHANGED
@@ -19,18 +19,25 @@ pier ships as **two halves**, installed separately:
19
19
  | Half | Package | Role |
20
20
  |---|---|---|
21
21
  | **pi extension** | `packages/pier-ext` (npm: [`pi-pier`](https://www.npmjs.com/package/pi-pier), [pi.dev gallery](https://pi.dev/packages/pi-pier)) | Injects `todo_write` / `subagent` / `terminal` / `ask_user_question` tools into a pi session, and reports pane state over the herdr socket API |
22
- | **herdr plugin** | `packages/pier-workbench` (`pier.workbench`) | Workspace bootstrap, blocked human-gate notifications, focus heat layout (focused pane grows in place) |
22
+ | **herdr plugin** | `packages/pier-workbench` (`pier.workbench`) | Blocked human-gate notifications, focus heat layout (focused pane grows in place), ops dashboard + sidebar agent view |
23
23
 
24
24
  ### Highlights
25
25
 
26
26
  - **Todo loop**: `todo_write` with full-replace semantics, session JSONL as source of truth, correct branch rollback; live pane-title projection `▶i ○p ■b ✓c (N/M) · current task`; TUI widget is an activity-anchored window (the in_progress entry and its surrounding context stay visible as work moves down the list); `/todos` command (including unblock)
27
27
  - **Todo anti-freeze**: an all-completed list unchanged for ≥6 turns flips the read hook from recitation to a rewrite warning; after ≥1h it archives — first offering one rewrite window with old entries as reference, then (if ignored) a final notice that clears the list (rm persisted to JSONL; the empty guard takes over — multi-step work must re-track); `/todos` still shows history, session JSONL untouched
28
28
  - **Interactive subagents**: every subagent = an isolated pi session in its own pane (separate context window); **a human can enter that pane and talk to it directly** (fix bugs, take over, answer ask_user_question)
29
- - **Soft write-locks**: per-pane lock tokens on write paths; cross-pane conflicts warn or block
29
+ - **Subagent output preview**: `subagent(action: "output", agentId)` returns what a running background subagent has printed since the previous call — an in-memory per-pane cursor computes the delta (`agent.read`, falling back to `pane.read` on older herdr), and the result carries status, revision, truncation and a `restart` flag for scrolled/cleared buffers, so the master can notice a stuck worker instead of waiting for settlement
30
+ - **Soft write-locks**: per-pane lock beacons on `write`/`edit` paths; cross-pane conflicts warn (default) or block (`PI_HERDR_WRITE_LOCK=1`). The warning lists every holder; `/locks` is the human view of the same table (`herdr agent list` exposes the raw tokens to agents)
30
31
  - **Role profiles**: built-in `master` / `worker-default`; custom roles as `.pi-herdr/roles/<name>.json`; toolset converges per role (deny rules are not bypassable)
31
32
  - **Human gate**: subagent `ask_user_question` → sidebar blocked marker + notification; manual takeover (ESC-interrupt then typing) is heuristically detected so the master pauses/returns management automatically
33
+ - **Ask picker**: every choice question renders through one pier-owned picker in the TUI — single-select (arrows + enter/space pick the highlighted row) and `multi: true` toggle list (checkboxes: space toggles, `a` toggles all, live selection count) share the same framed chrome, `recommended` seeds the cursor (and pre-checks in multi), and the free-text row stays last; `allowOther: false` gives pure choice. The host select dialog and typed-index prompts remain the fallbacks for RPC mode and older pi
34
+ - **Blocking dialogs mark the pane**: pi's `ui_prompt_start` / `ui_prompt_end` events cover the inherently blocking `ctx.ui` dialogs (`select` / `confirm` / `input` / `editor`), so a dialog from *any* extension marks the pane blocked and emits the `herdr:blocked` edge; nested gates coalesce, and the ask tool no longer needs its 5s refresh heartbeat. `custom` is deliberately excluded — pi routes resident overlays through it too (pier's own slim-frame overlay never calls `done()`), which used to leave a working pane marked blocked for the whole session
35
+ - **Role gate stops early**: a denied worker tool call returns `terminate`, so a batch whose results are all terminating ends without another model round trip
36
+ - **Transcript cards**: pier's own session entries (todo edits, subagent/terminal registries, role manifests, soft approvals) and its reminder messages render as compact cards instead of raw JSON — entry/message renderers, no pi-tui dependency
32
37
  - **Isolated worktree subagents (`isolate`)**: heavy parallel writers get a fresh git worktree (branch `pier/<slug>` from your HEAD under `~/.herdr/worktrees/<repo>/`) with commit discipline in the prompt; settlement carries a diff summary (commits since base, files changed, uncommitted count); merge with `git merge --no-ff` and the worktree auto-removes once merged and clean — the branch stays for audit
33
- - **Long-task lifecycle (01a03c0d review)**: observation timeout is now an inactivity budget (working slices renew it — healthy >10min tasks are no longer killed); follow_up messages deliver via steer at tool-call gaps (supplementary contracts arrive in seconds, not after the whole run); GC waits for the settlement notice to be delivered before closing panes; takeover detection attributes recent machine injections first; ledger rows carry a `via` tag, closed rows inherit their outcome, zombie running rows are swept on startup; SUBS snapshots are hash-gated. Tunables: `PI_HERDR_SUBAGENT_TIMEOUT_MS` (inactivity seconds), `PI_HERDR_INJECT_GRACE_MS`, `PI_HERDR_OBSERVE_MS`
38
+ - **Worktree collection is ownership-scoped**: only worktrees this session registered are collected, and the running process's own directory is never a candidate — the earlier prefix match on `refs/heads/pier/` could delete a parallel session's worktree out from under it. Sweeping branches the session never registered is opt-in (`PIER_ISOLATE_SWEEP_ORPHANS=1`)
39
+ - **Ops dashboard + sidebar view (herdr 0.9)**: `herdr plugin pane open --plugin pier.workbench --entrypoint dashboard` opens a live pane/tab/agent board (roles, states, todo progress); the plugin also registers a `Pier` sidebar agent view that lists every detected agent (no harness filter) with attention-first ordering
40
+ - **Long-task lifecycle (01a03c0d review)**: observation timeout is now an inactivity budget (working slices renew it — healthy >10min tasks are no longer killed); follow_up messages deliver via steer at tool-call gaps (supplementary contracts arrive in seconds, not after the whole run); GC waits for the settlement notice to be delivered before closing panes; takeover detection attributes recent machine injections first; ledger rows carry a `via` tag, closed rows inherit their outcome, zombie running rows are swept on startup; SUBS snapshots are hash-gated. Tunables: `PIER_SUBAGENT_TIMEOUT_MS` (inactivity ms), `PIER_SETTLEMENT_WINDOW_MS` (ms), `PIER_OBSERVATION_WINDOW_MS` (ms)
34
41
  - **Settlement notice folding**: subagent settlements no longer flood back when a long main run ends — they inject at turn gaps, up to 3 shown, the rest folded with pointers
35
42
 
36
43
  ## Installation
@@ -38,8 +45,8 @@ pier ships as **two halves**, installed separately:
38
45
  ### Requirements
39
46
 
40
47
  - Node ≥ 22
41
- - pi ≥ 0.84 (`@earendil-works/pi-coding-agent`)
42
- - herdr ≥ 0.8.0 (macOS / Linux / Windows; Windows is preview beta)
48
+ - pi ≥ 0.86.0 (`@earendil-works/pi-coding-agent`) — dynamic toolset (transcript-backed `setActiveTools` deltas) and role switching require it
49
+ - herdr ≥ 0.9.0 (macOS / Linux / Windows; Windows is preview beta)
43
50
 
44
51
  ### One-shot install (recommended)
45
52
 
@@ -49,7 +56,7 @@ No clone needed — run the npm package directly:
49
56
  npx pier-setup@latest # user mode: pi install npm:pi-pier + herdr plugin install
50
57
  npx pier-setup@latest version # local vs npm latest (installer / pi-pier / herdr plugin)
51
58
  npx pier-setup@latest update # refresh both halves in place (does not uninstall first)
52
- npx pier-setup@latest uninstall # --purge also drops boot-config.json
59
+ npx pier-setup@latest uninstall
53
60
 
54
61
  npm i -g pier-setup # or install globally, then: pier-setup / version / update
55
62
  ```
@@ -63,15 +70,13 @@ Dev mode still requires cloning the repo:
63
70
  git clone https://github.com/July24/pier && cd pier
64
71
  node install.mjs install --dev # local-path pi install + herdr plugin link; code changes are live
65
72
  node install.mjs version --dev
66
- node install.mjs update --dev # rewrite boot-config only; pull the repo yourself
73
+ node install.mjs update --dev # re-check env + versions; pull the repo yourself
67
74
  ```
68
75
 
69
- The script verifies the environment (node / pi / herdr versions), probes pi's node
70
- and cli.js absolute paths, generates boot-config.json (user mode stores it in the
71
- herdr plugin config dir, so reinstalls don't lose it), and registers both halves.
76
+ The script verifies the environment (node / pi / herdr versions) and registers both halves.
72
77
 
73
78
  `update` re-runs `pi update npm:pi-pier` (falls back to `pi install`) and
74
- `herdr plugin install … --yes`, then rewrites boot-config. Override sources with
79
+ `herdr plugin install … --yes`. Override sources with
75
80
  `--pi-spec=` / `--herdr-spec=` (npm publishing or forks). `pier-setup --help`
76
81
  lists every command.
77
82
 
@@ -124,15 +129,7 @@ pi overwrites tools/commands by name; event listeners stack. Two todo or subagen
124
129
  **Designed coexistence:** herdr’s official `herdr:pi` reporter. Keep it. pier emits `herdr:blocked` so that plugin remains lifecycle authority.
125
130
 
126
131
  - **other agents inside herdr (claude code / codex etc.)**: tabs without a pi pane
127
- are excluded from heat reflow; blocked notifications are pi-only; master-tab
128
- auto-bootstrap can be disabled with `autoBootstrap: false` in boot-config.
129
-
130
- ### Bootstrap config (workbench half)
131
-
132
- Master-tab bootstrap needs local node / pi paths: `pier-setup` generates
133
- them automatically; manually, copy `packages/pier-workbench/scripts/boot-config.example.json`
134
- (placeholders for both macOS and Windows). User mode reads it from
135
- `herdr plugin config-dir pier.workbench`; dev mode from `packages/pier-workbench/scripts/boot-config.json`.
132
+ are excluded from heat reflow; blocked notifications are pi-only.
136
133
 
137
134
  ### Usage
138
135
 
@@ -146,17 +143,81 @@ them automatically; manually, copy `packages/pier-workbench/scripts/boot-config.
146
143
  ```
147
144
  packages/
148
145
  pier-ext/ # pi extension (npm: pi-pier): todo/subagent tools, herdr client, vocab authority, skill
149
- pier-workbench/ # herdr plugin (pier.workbench): workspace bootstrap + blocked notify + heat layout
150
- docs/ # install guide, role profile docs, sidebar role config
146
+ pier-workbench/ # herdr plugin (pier.workbench): blocked notify + heat layout + ops dashboard
147
+ docs/ # install guide, configuration overview, role profile docs, sidebar role config
151
148
  ```
152
149
 
153
150
  ## Testing
154
151
 
155
152
  ```sh
156
153
  npm install --ignore-scripts
157
- npm test # node --test, 278 unit tests (planner / todo replay / anti-freeze staleness / session tail / GC / lifecycle)
154
+ npm test # node --test, 733 unit tests (planner / todo replay / anti-freeze staleness / session tail / GC / lifecycle / renderers / ask picker / subagent output / jev decision layer: diagnostic gate, notice ranking, excerpt windows)
158
155
  ```
159
156
 
157
+ ## Configuration
158
+
159
+ Runtime policies and operational limits are centralized in `runtime-policy.ts` (with terminal read limits in `terminal.ts`). All timeouts use millisecond units unless specified otherwise:
160
+
161
+ | Variable | Default | Unit | Purpose |
162
+ |---|---|---|---|
163
+ | `PIER_SUBAGENT_TIMEOUT_MS` | `600000` | ms | Subagent inactivity budget / overall timeout before forced termination |
164
+ | `PIER_SETTLEMENT_WINDOW_MS` | `60000` | ms | Settlement notice window, machine injection grace period, and takeover idle threshold |
165
+ | `PIER_OBSERVATION_WINDOW_MS` | `30000` | ms | Post-settle observation window before auto-consuming finished subagents |
166
+ | `PIER_FOREGROUND_PATIENCE_MS` | `300000` | ms | Foreground execution patience before auto-demoting a subagent to background |
167
+ | `PIER_GC_TICK_MS` | `30000` | ms | Subagent garbage collection ticker interval |
168
+ | `PIER_POLL_INTERVAL_MS` | `30000` | ms | State observation polling interval for subagent state transitions |
169
+ | `PIER_READY_TIMEOUT_MS` | `90000` | ms | Subagent pane pipe readiness wait (exponential backoff; a pane that exited fails immediately with its last output attached) |
170
+ | `PIER_SESSION_TTL_SECONDS` | `600` | s | Session retention TTL after subagent exit before GC cleanup |
171
+ | `PIER_GIT_TIMEOUT_MS` | `10000` | ms | Execution timeout for git operations (worktree creation, diff summary, cleanup) |
172
+ | `PIER_FOCUS_POLL_MS` | `1500` / `0` | ms | Focus sampling for heat layout (`0` disables). Default 1500ms on Herdr <0.9.1; 0 (event-first) on 0.9.1+ |
173
+ | `PIER_TERM_READ_MAX` | `8000` | chars | Maximum terminal buffer characters read per operation |
174
+ | `PIER_TERM_IDLE_MS` | `1800000` | ms | Idle time before pier nudges about an open terminal |
175
+ | `PIER_TODO_GRACE_MS` | `30000` | ms | Settle grace before the unfinished-todo reminder |
176
+ | `PIER_TRACE` | – | flag | Write diagnostics (tool renderers, swallowed errors) to stderr |
177
+ | `PIER_JEV_ENABLE` | `0` | flag | Jev decision layer (TypeSafe System One classification) master switch; every call site fails open — see `docs/rfc-jev-integration.md` |
178
+ | `PIER_JEV_LOG` | `0` | flag | Write `efficiency-logs/jev.jsonl` (metadata + hashes only, never bodies) |
179
+ | `PIER_JEV_API_KEY` | – | – | API key; precedence env > config `jev.apiKey` > `TYPESAFE_API_KEY` |
180
+ | `PIER_JEV_MODEL` | `jev-1.13.0` | – | Pinned versioned model id (aliases drift silently and skew tuned thresholds) |
181
+ | `PIER_JEV_TIMEOUT_MS` | `2000` | ms | Total per-call budget (AbortController hard kill) |
182
+ | `PIER_JEV_MIN_CONFIDENCE` | `0.6` | – | Minimum Choice/Score confidence to adopt an answer |
183
+ | `PIER_JEV_BASE_URL` | – | – | API root override (relay/gateway) |
184
+
185
+ **Naming:** `PIER_*` is the canonical namespace for pier options; the historical `PI_HERDR_*` spelling
186
+ of the same knob is still read as an alias (an empty value counts as unset). Names handed to child
187
+ processes stay as they are (`PI_HERDR_SUBAGENT`, `PI_HERDR_ROLE_MANIFEST`, `PI_HERDR_TUI`,
188
+ `PI_HERDR_META_KEY`), because renaming them would split a running worker from its parent.
189
+ `/pier-config doctor` lists every option with its effective value and where it came from, plus the
190
+ errors pier deliberately swallowed this session.
191
+
192
+ **Focus heat:** Herdr 0.9.0 resolved mouse focus in the client and did not deliver `pane.focused`
193
+ to plugins, so each pane sampled `layout.export → focused_pane_id` and replayed the workbench
194
+ event. Herdr 0.9.1+ delivers `pane.focused` natively, so the poller defaults off. Override with
195
+ `PIER_FOCUS_POLL_MS`; `PIER_WORKBENCH_ROOT` points at a relocated plugin checkout.
196
+
197
+ ### Efficiency mechanisms (D100–D103, opt-in)
198
+
199
+ Three token-saving mechanisms ship **disabled by default** and are safe to try one at a time:
200
+
201
+ - **ObservationPack** — large tool outputs are projected as placeholders after the first `fullSends` requests, with paged retrieval via `obs_recall` (session JSONL stays untouched).
202
+ - **EPR** — long diagnostic (`bash` test/build) logs are reduced in-process to a byte-verified evidence receipt; the raw log is archived first and stays readable.
203
+ - **OCC** — `todo_write` boundaries drive pi's native compaction under a KV-cache cost model, carrying unfinished tasks across the compaction.
204
+
205
+ ```bash
206
+ PI_HERDR_OBS_PACK_ENABLE=1 PI_HERDR_OBS_PACK_LOG=1 pi # start here
207
+ ```
208
+
209
+ Full quick start, what to watch in `efficiency-logs/*.jsonl`, rollback steps and a feedback template: **[docs/efficiency-trial.md](docs/efficiency-trial.md)**.
210
+
211
+ Everything above (plus roles, pi settings and the `PIER_*`/`PI_HERDR_*` env knobs) is also inspectable in-session:
212
+
213
+ ```text
214
+ /pier-config # 4-plane index; hands a guided change to the agent
215
+ /pier-config show all # effective value + source (env > workspace > user > default) per key
216
+ /pier-config check # validation across planes · /pier-config doc → written report
217
+ ```
218
+
219
+ Read-only by design — see **[docs/configuration.md](docs/configuration.md)** for the plane map and the "which plane do I edit?" table.
220
+
160
221
  ## Design principles
161
222
 
162
223
  - **Session JSONL is the single source of truth**: todos, delegations, and registries replay from session branches — restart and branch switches stay correct
@@ -170,6 +231,6 @@ MIT
170
231
 
171
232
  ---
172
233
 
173
- > 💡 **For contributors**: `.gitignore` is deliberately **not committed** to this repository (ignore rules are per-environment). Maintain your own rules based on `README.md` / `README_ZH.md`; `packages/pier-workbench/scripts/boot-config.json` is also machine-local (see the `.example.json` template).
234
+ > 💡 **For contributors**: `.gitignore` is tracked in the repository. `docs/research/` contains local research notes ignored by `.gitignore`.
174
235
  >
175
236
  > **Naming convention**: the brand is **pier** (repo/packages/plugin); runtime protocol identifiers keep the **`pi-herdr`** prefix (`.pi-herdr/roles/` dir, `pi-herdr.subs` session custom entries, `~/.pi/agent/herdr-pi/roles/` user dir) — they persist with user sessions/config files, renaming would break existing data, so they are the compatibility layer.
package/install.mjs CHANGED
@@ -1,27 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * pier 一键安装/卸载/更新/查版本(跨平台:macOS / Linux / Windows)。
3
+ * pier one-shot installer / uninstaller / updater / version inspector (cross-platform: macOS / Linux / Windows).
4
4
  *
5
- * 用法:
6
- * node install.mjs install [--dev] [--hmr-dev] [--force] # 安装(默认命令,可省略 install)
7
- * node install.mjs update [--dev] [--hmr-dev] # 原地刷新两半区到最新发行(不先卸载)
8
- * node install.mjs version [--json] # 本地 vs npm latest
9
- * node install.mjs uninstall [--dev] [--purge] # 卸载;--purge 连 boot-config.json 一起删
10
- * node install.mjs --prepare # npm prepare:hooksPath + 仓库根 bin 链接
5
+ * Usage:
6
+ * node install.mjs install [--dev] # Install (default command, 'install' can be omitted)
7
+ * node install.mjs update [--dev] # Refresh both halves in place to latest release (no prior uninstall)
8
+ * node install.mjs version [--json] # Local vs npm latest
9
+ * node install.mjs uninstall [--dev] # Uninstall both halves
10
+ * node install.mjs --prepare # npm prepare: hooksPath + repo root bin link
11
11
  * node install.mjs --help
12
12
  *
13
- * 模式:
14
- * 用户模式(默认):pi install npm:pi-pier
13
+ * Modes:
14
+ * User mode (default): pi install npm:pi-pier
15
15
  * + herdr plugin install July24/pier/packages/pier-workbench --yes
16
- * boot-config.json 写 herdr plugin config-dir;extPath 指向 pi 已安装的
17
- * pi-pier(~/.pi/agent/npm/...),不是 pier-setup 包内的仓库路径。
18
- * --dev 开发模式:pi install <repo>/packages/pier-ext + herdr plugin link <repo>/packages/pier-workbench
19
- * (link 目录是活的,改码即生效;update 只重写 boot-config)。
20
- * 发行规格可用 --pi-spec= / --herdr-spec= 覆盖(npm 发布或 fork 场景)。
21
- * 失败语义:每步给出手动等价命令;步骤失败不阻断报告(exitCode=1)。
16
+ * --dev Development mode: pi install <repo>/packages/pier-ext + herdr plugin link <repo>/packages/pier-workbench
17
+ * (linked directory is live; code edits take effect immediately).
18
+ * Distribution specs can be overridden via --pi-spec= / --herdr-spec= (for npm publish or fork scenarios).
19
+ * Failure semantics: Each step provides manual equivalent command; step failure does not abort reporting (exitCode=1).
22
20
  */
23
21
  import { execFileSync } from 'node:child_process';
24
- import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
22
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
25
23
  import { homedir } from 'node:os';
26
24
  import { dirname, join, resolve } from 'node:path';
27
25
  import { fileURLToPath } from 'node:url';
@@ -29,7 +27,6 @@ import { fileURLToPath } from 'node:url';
29
27
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)));
30
28
  const EXT_DIR = join(ROOT, 'packages', 'pier-ext');
31
29
  const WB_DIR = join(ROOT, 'packages', 'pier-workbench');
32
- const DEV_BOOT_CONFIG = join(WB_DIR, 'scripts', 'boot-config.json');
33
30
  const EXT_PATH = join(EXT_DIR, 'src', 'index.ts');
34
31
  const IS_WIN = process.platform === 'win32';
35
32
  const COMMANDS = ['install', 'uninstall', 'update', 'version', 'help'];
@@ -66,17 +63,17 @@ const command = COMMANDS.includes(argv[0]) ? argv.shift() : null;
66
63
  const flags = new Set(argv.filter((a) => (a.startsWith('--') && !a.includes('=')) || a === '-h' || a === '-v'));
67
64
  const optValue = (name) => argv.find((a) => a.startsWith(`--${name}=`))?.split('=').slice(1).join('=') ?? null;
68
65
  const dev = flags.has('--dev');
69
- const PI_SPEC = optValue('pi-spec') ?? 'npm:pi-pier'; // 用户模式默认 npm 发行版;--pi-spec=git:github.com/July24/pier 可跟随仓库 main
66
+ const PI_SPEC = optValue('pi-spec') ?? 'npm:pi-pier'; // User mode defaults to npm release; --pi-spec=git:github.com/July24/pier tracks repo main
70
67
  const HERDR_SPEC = optValue('herdr-spec') ?? 'July24/pier/packages/pier-workbench';
71
68
 
72
69
  const log = (m) => console.log(m);
73
70
  const die = (m) => { console.error(`✗ ${m}`); process.exit(1); };
74
71
 
75
- /* ── 工具函数 ────────────────────────────────────────────────────── */
72
+ /* ── Utility functions ────────────────────────────────────────────── */
76
73
  function which(bin) {
77
74
  try {
78
75
  const out = execFileSync(IS_WIN ? 'where' : 'which', [bin], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
79
- // where 可能多行(PATH 逐条命中),取第一条存在的
76
+ // 'where' may return multiple lines (one per PATH hit); pick the first existing line
80
77
  for (const line of out.split('\n').map((l) => l.trim()).filter(Boolean)) {
81
78
  if (existsSync(line)) return line;
82
79
  }
@@ -87,9 +84,9 @@ function which(bin) {
87
84
  }
88
85
 
89
86
  function run(cmd, args, opts = {}) {
90
- // win32: npm 全局 bin 是 .cmd shim,无 shell 直接 spawn 会 ENOENT/EINVAL,须走 cmd.exe;
91
- // 且 shell:true 时 Node 把 args 按空格裸拼接(不加引号),含空格的路径参数会被拆碎,故预拼接并加引号。
92
- // POSIX 走原生分支,保持 execFileSync(cmd, args) 原语义。
87
+ // win32: npm global bin is a .cmd shim; spawning directly without shell triggers ENOENT/EINVAL, must run via cmd.exe;
88
+ // also when shell:true, Node concatenates args with bare spaces (unquoted), breaking path args with spaces, so pre-join and quote.
89
+ // POSIX uses native branch, preserving execFileSync(cmd, args) semantics.
93
90
  if (IS_WIN) {
94
91
  const cmdline = [cmd, ...args]
95
92
  .map((a) => (/[\s"]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a))
@@ -141,60 +138,36 @@ function npmLatest(name) {
141
138
  }
142
139
  }
143
140
 
144
- /** herdr 插件配置目录(用户模式 boot-config 落点;插件已注册才存在路径语义)。 */
145
- function herdrConfigDir() {
146
- try { return run('herdr', ['plugin', 'config-dir', 'pier.workbench']); } catch { return null; }
147
- }
148
141
 
149
- /* ── 环境校验(install 前置) ────────────────────────────────────── */
142
+ /* ── Environment check (pre-install) ────────────────────────────────── */
150
143
  function checkEnv() {
151
144
  const [major] = process.versions.node.split('.').map(Number);
152
145
  if (major < 22) die(`node ≥ 22 required, found ${process.versions.node}`);
153
146
  log(`✓ node ${process.versions.node}`);
154
147
  const piVersion = (() => { try { return parseVersion(run('pi', ['--version'])); } catch { return null; } })();
155
- if (!piVersion || !geVersion(piVersion, [0, 84, 0])) {
156
- die(`pi ≥ 0.84 required${piVersion ? `, found ${piVersion.join('.')}` : ' (pi not found or not runnable)'}. Install: npm i -g @earendil-works/pi-coding-agent`);
148
+ if (!piVersion || !geVersion(piVersion, [0, 86, 0])) {
149
+ die(`pi ≥ 0.86.0 required${piVersion ? `, found ${piVersion.join('.')}` : ' (pi not found or not runnable)'}. Install: npm i -g @earendil-works/pi-coding-agent`);
157
150
  }
158
151
  log(`✓ pi ${piVersion.join('.')}`);
159
152
  const herdrVersion = (() => { try { return parseVersion(run('herdr', ['--version'])); } catch { return null; } })();
160
- if (!herdrVersion || !geVersion(herdrVersion, [0, 8, 0])) {
161
- die(`herdr ≥ 0.8.0 required${herdrVersion ? `, found ${herdrVersion.join('.')}` : ' (herdr not found)'}. Install: https://herdr.dev/`);
153
+ if (!herdrVersion || !geVersion(herdrVersion, [0, 9, 0])) {
154
+ die(`herdr ≥ 0.9.0 required${herdrVersion ? `, found ${herdrVersion.join('.')}` : ' (herdr not found)'}. Install: https://herdr.dev/`);
162
155
  }
163
156
  log(`✓ herdr ${herdrVersion.join('.')}`);
164
157
  return { piVersion, herdrVersion };
165
158
  }
166
159
 
167
- /** pi 半区源:用户模式 git 规格,dev 模式本地目录。 */
160
+ /** pi extension source: user mode uses git/npm spec, dev mode uses local directory. */
168
161
  const piSource = () => (dev ? EXT_DIR : PI_SPEC);
169
162
 
170
- /* ── boot-config 探测与写入 ──────────────────────────────────────── */
171
- function probePiRuntime() {
172
- const piBin = which('pi');
173
- // bin 是 node shim:realpath 落到 .../pi-coding-agent/dist/cli.js(npm 布局,win/posix 同构)
174
- if (piBin) {
175
- try {
176
- const real = realpathSync(piBin);
177
- if (real.endsWith('cli.js') && existsSync(real)) {
178
- return { piCli: real, piNode: process.execPath };
179
- }
180
- } catch { /* 落到兜底 */ }
181
- }
182
- // npm root -g 兜底:pi 不在 PATH(nvm shim 等)时按全局布局拼
183
- try {
184
- const gRoot = run('npm', ['root', '-g']);
185
- const cand = join(gRoot, '@earendil-works', 'pi-coding-agent', 'dist', 'cli.js');
186
- if (existsSync(cand)) return { piCli: cand, piNode: process.execPath };
187
- } catch { /* npm 不可用 */ }
188
- return null;
189
- }
190
163
 
191
164
  function piAgentDir() {
192
165
  return process.env.PI_CODING_AGENT_DIR || join(homedir(), '.pi', 'agent');
193
166
  }
194
167
 
195
- /* ── 旧版 git 模式注册迁移 ──────────────────────────────────────── */
196
- // 早期 `pi install git:github.com/July24/pier` 的遗留注册与 npm:pi-pier 并存时,
197
- // 两份扩展会注册同名工具(todo_write / ask_user_question…),pi 启动即报 conflict。
168
+ /* ── Legacy git-mode registration migration ────────────────────────── */
169
+ // When legacy `pi install git:github.com/July24/pier` coexists with npm:pi-pier,
170
+ // both extensions register identical tools (todo_write / ask_user_question...), causing conflict at pi launch.
198
171
  const LEGACY_GIT_SPEC = 'git:github.com/July24/pier';
199
172
 
200
173
  function registeredPiPackages() {
@@ -203,7 +176,7 @@ function registeredPiPackages() {
203
176
  } catch { return []; }
204
177
  }
205
178
 
206
- /** install/update 前清理旧 git 模式注册;--pi-spec 显式指定该 git 源时视为目标源,不动。 */
179
+ /** Clean up legacy git-mode registration before install/update; if --pi-spec explicitly specifies that git source, treat it as target and keep it. */
207
180
  function removeLegacyGitRegistration() {
208
181
  if (piSource() === LEGACY_GIT_SPEC) return;
209
182
  if (!registeredPiPackages().includes(LEGACY_GIT_SPEC)) return;
@@ -260,47 +233,6 @@ function resolveUserExtPath() {
260
233
  return extPathFromPiList();
261
234
  }
262
235
 
263
- function writeBootConfig(hmrDev, force) {
264
- const probed = probePiRuntime();
265
- if (!probed) die('cannot locate pi cli.js (tried: pi shim realpath, npm root -g). Fill boot-config.json manually.');
266
- let extPath;
267
- if (dev) {
268
- if (!existsSync(EXT_PATH)) die(`pier-ext entry missing: ${EXT_PATH} (repo layout broken?)`);
269
- extPath = EXT_PATH;
270
- } else {
271
- extPath = resolveUserExtPath();
272
- if (!extPath) die(`cannot locate installed pier-ext (npm:pi-pier / git clone). Did \`pi install ${piSource()}\` succeed?`);
273
- }
274
-
275
- const config = {
276
- mainTabLabel: 'main',
277
- piNode: probed.piNode,
278
- piCli: probed.piCli,
279
- extPath,
280
- workbenchPluginId: 'pier.workbench',
281
- hmrDev,
282
- };
283
-
284
- const target = dev ? DEV_BOOT_CONFIG : join(herdrConfigDir() ?? die('cannot resolve herdr plugin config-dir (is pier.workbench installed?)'), 'boot-config.json');
285
- if (existsSync(target) && !force) {
286
- let existing = null;
287
- try { existing = JSON.parse(readFileSync(target, 'utf8')); } catch { /* 视为损坏 */ }
288
- if (existing) {
289
- log(`• ${target} exists, diff (existing → new):`);
290
- for (const k of Object.keys(config)) {
291
- if (existing[k] !== config[k]) log(` ${k}: ${JSON.stringify(existing[k])} → ${JSON.stringify(config[k])}`);
292
- }
293
- log(' kept existing (use --force to overwrite)');
294
- return;
295
- }
296
- }
297
- mkdirSync(dirname(target), { recursive: true });
298
- writeFileSync(target, JSON.stringify(config, null, 2) + '\n');
299
- log(`✓ boot-config.json ${force ? 'overwritten (--force)' : 'written'} → ${target}`);
300
- log(` piNode = ${config.piNode}`);
301
- log(` piCli = ${config.piCli}`);
302
- log(` extPath = ${config.extPath}`);
303
- }
304
236
 
305
237
  function pkgDirFromExtPath(extPath) {
306
238
  let d = dirname(extPath);
@@ -339,22 +271,22 @@ function collectStatus() {
339
271
  };
340
272
  }
341
273
 
342
- /* ── 命令 ────────────────────────────────────────────────────────── */
274
+ /* ── Commands ─────────────────────────────────────────────────────── */
343
275
  function usage() {
344
276
  log(`pier-setup — install / update / inspect pier (pi-pier + pier.workbench)
345
277
 
346
278
  Usage:
347
- pier-setup [install] [--dev] [--hmr-dev] [--force]
348
- pier-setup update [--dev] [--hmr-dev]
279
+ pier-setup [install] [--dev]
280
+ pier-setup update [--dev]
349
281
  pier-setup version [--json]
350
- pier-setup uninstall [--dev] [--purge]
282
+ pier-setup uninstall [--dev]
351
283
 
352
284
  npx pier-setup@latest # recommended user install
353
285
  npx pier-setup@latest update # refresh both halves to newest release
354
286
  npx pier-setup@latest version # local vs npm latest
355
287
 
356
288
  --pi-spec= / --herdr-spec= override distribution sources
357
- --dev local link (clone); update only rewrites boot-config`);
289
+ --dev local link (clone); the linked directories are live`);
358
290
  }
359
291
 
360
292
  function version() {
@@ -390,25 +322,20 @@ function update() {
390
322
  if (latestSetup && me && cmpSemver(me, latestSetup) < 0) {
391
323
  log(`⚠ installer ${me} < latest ${latestSetup} — re-run: npx pier-setup@latest update`);
392
324
  }
393
- const hmrDev = flags.has('--hmr-dev');
394
325
  if (dev) {
395
- log('dev mode: local link is live; pull the repo yourself. Refreshing boot-config only.');
396
- writeBootConfig(hmrDev, true);
326
+ log('dev mode: local link is live; pull the repo yourself (no installer-managed files to refresh).');
397
327
  } else {
398
328
  if (tryRun('pi', ['update', piSource()])) log(`✓ pi extension updated (${piSource()})`);
399
329
  else if (tryRun('pi', ['install', piSource()])) log(`✓ pi extension installed (${piSource()})`);
400
330
  else { console.error('✗ pi update/install failed'); log(` manual: pi update ${piSource()}`); process.exitCode = 1; }
401
331
  if (tryRun('herdr', ['plugin', 'install', HERDR_SPEC, '--yes'])) log(`✓ herdr plugin updated (${HERDR_SPEC})`);
402
332
  else { console.error('✗ herdr plugin update failed'); log(` manual: herdr plugin install ${HERDR_SPEC} --yes`); process.exitCode = 1; }
403
- writeBootConfig(hmrDev, true);
404
333
  }
405
334
  log('');
406
335
  version();
407
336
  }
408
337
 
409
338
  function install() {
410
- const hmrDev = flags.has('--hmr-dev');
411
- const force = flags.has('--force');
412
339
  checkEnv();
413
340
  removeLegacyGitRegistration();
414
341
 
@@ -417,40 +344,31 @@ function install() {
417
344
  else { console.error('✗ pi install failed'); log(` manual: pi install ${EXT_DIR}`); process.exitCode = 1; }
418
345
  if (tryRun('herdr', ['plugin', 'link', WB_DIR])) log('✓ herdr plugin linked (packages/pier-workbench, local)');
419
346
  else { console.error('✗ herdr plugin link failed'); log(` manual: herdr plugin link ${WB_DIR}`); process.exitCode = 1; }
420
- writeBootConfig(hmrDev, force);
421
347
  } else {
422
348
  if (tryRun('pi', ['install', piSource()])) log(`✓ pi extension installed (${PI_SPEC})`);
423
349
  else { console.error('✗ pi install failed'); log(` manual: pi install ${PI_SPEC}`); process.exitCode = 1; }
424
350
  if (tryRun('herdr', ['plugin', 'install', HERDR_SPEC, '--yes'])) log(`✓ herdr plugin installed (${HERDR_SPEC})`);
425
351
  else { console.error('✗ herdr plugin install failed'); log(` manual: herdr plugin install ${HERDR_SPEC}`); process.exitCode = 1; }
426
- writeBootConfig(hmrDev, force);
427
352
  }
428
353
 
429
354
  log(`
430
355
  pier installed (${dev ? 'dev mode: local paths, code changes are live' : 'user mode: managed checkouts'}). Next:
431
- 1. start herdr, open/create a workspace — the main tab auto-bootstraps a pi session
356
+ 1. start herdr, open/create a workspace and run \`pi\` inside a pane
432
357
  2. inside pi, ask the model to use todo_write / subagent tools`);
433
358
  }
434
359
 
435
360
  function uninstall() {
436
- const purge = flags.has('--purge');
437
361
  if (tryRun('pi', ['remove', piSource()])) log('✓ pi extension removed');
438
362
  else { console.error('✗ pi remove failed'); log(` manual: pi remove ${piSource()}`); process.exitCode = 1; }
439
363
 
440
364
  if (dev) {
441
365
  if (tryRun('herdr', ['plugin', 'unlink', 'pier.workbench'])) log('✓ herdr plugin unlinked (files kept)');
442
366
  else { console.error('✗ herdr plugin unlink failed'); log(' manual: herdr plugin unlink pier.workbench'); process.exitCode = 1; }
443
- if (purge && existsSync(DEV_BOOT_CONFIG)) { rmSync(DEV_BOOT_CONFIG); log('✓ dev boot-config.json purged'); }
444
367
  } else {
445
368
  if (tryRun('herdr', ['plugin', 'uninstall', 'pier.workbench'])) log('✓ herdr plugin uninstalled (managed checkout removed)');
446
369
  else { console.error('✗ herdr plugin uninstall failed'); log(' manual: herdr plugin uninstall pier.workbench'); process.exitCode = 1; }
447
- if (purge) {
448
- const dir = herdrConfigDir();
449
- const f = dir ? join(dir, 'boot-config.json') : null;
450
- if (f && existsSync(f)) { rmSync(f); log('✓ boot-config.json purged'); }
451
- }
452
370
  }
453
- log(`\npier uninstalled.${purge ? '' : ' (boot-config.json kept; use --purge to remove)'}`);
371
+ log('\npier uninstalled.');
454
372
  }
455
373
 
456
374
  if (command === 'help' || flags.has('--help') || flags.has('-h')) usage();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pier-setup",
3
- "version": "0.2.7",
3
+ "version": "0.3.1",
4
4
  "description": "One-shot installer for pier: install / update / version / uninstall for pi-pier + pier.workbench",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,7 +31,8 @@
31
31
  "packages/*"
32
32
  ],
33
33
  "scripts": {
34
- "test": "node --test \"packages/*/test/*.test.ts\"",
34
+ "typecheck": "tsc --noEmit -p packages/pier-ext/tsconfig.json",
35
+ "test": "npm run typecheck && node --test \"packages/*/test/*.test.ts\"",
35
36
  "test:mutation": "stryker run",
36
37
  "prepare": "node ./install.mjs --prepare"
37
38
  },
@@ -41,7 +42,8 @@
41
42
  "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
42
43
  "@deepseek-ai/cordis-plugin-timer": "^1.1.3",
43
44
  "@stryker-mutator/core": "^10.0.0",
44
- "@stryker-mutator/typescript-checker": "^10.0.0"
45
+ "@stryker-mutator/typescript-checker": "^10.0.0",
46
+ "typescript": "~5.9.3"
45
47
  },
46
48
  "pi": {
47
49
  "extensions": [