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.
- package/README.md +85 -24
- package/install.mjs +38 -120
- 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`) |
|
|
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
|
-
- **
|
|
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
|
-
- **
|
|
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.
|
|
42
|
-
- herdr ≥ 0.
|
|
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
|
|
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 #
|
|
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)
|
|
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
|
|
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
|
|
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):
|
|
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,
|
|
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
|
|
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
|
|
3
|
+
* pier one-shot installer / uninstaller / updater / version inspector (cross-platform: macOS / Linux / Windows).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* node install.mjs install [--dev]
|
|
7
|
-
* node install.mjs update [--dev]
|
|
8
|
-
* node install.mjs version [--json]
|
|
9
|
-
* node install.mjs uninstall [--dev]
|
|
10
|
-
* node install.mjs --prepare # npm prepare
|
|
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
|
-
*
|
|
13
|
+
* Modes:
|
|
14
|
+
* User mode (default): pi install npm:pi-pier
|
|
15
15
|
* + herdr plugin install July24/pier/packages/pier-workbench --yes
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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,
|
|
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'; //
|
|
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
|
|
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
|
|
91
|
-
//
|
|
92
|
-
// POSIX
|
|
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
|
-
/* ──
|
|
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,
|
|
156
|
-
die(`pi ≥ 0.
|
|
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,
|
|
161
|
-
die(`herdr ≥ 0.
|
|
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
|
|
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
|
-
/* ──
|
|
196
|
-
//
|
|
197
|
-
//
|
|
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
|
|
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]
|
|
348
|
-
pier-setup update [--dev]
|
|
279
|
+
pier-setup [install] [--dev]
|
|
280
|
+
pier-setup update [--dev]
|
|
349
281
|
pier-setup version [--json]
|
|
350
|
-
pier-setup uninstall [--dev]
|
|
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);
|
|
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
|
|
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
|
|
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(
|
|
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.
|
|
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
|
-
"
|
|
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": [
|