@msdavid/pi-distro 0.2.0 → 0.4.0
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 +61 -1
- package/README.md +85 -14
- package/docs/authoring.md +89 -15
- package/extensions/catalogue.ts +97 -52
- package/extensions/deploy.ts +37 -9
- package/extensions/github.ts +163 -2
- package/extensions/index.ts +4 -1
- package/extensions/info.ts +53 -26
- package/extensions/pick.ts +29 -13
- package/extensions/resolve.ts +49 -8
- package/extensions/save.ts +82 -11
- package/extensions/show.ts +30 -7
- package/extensions/undeploy.ts +78 -41
- package/extensions/update.ts +1 -1
- package/extensions/util.ts +81 -11
- package/package.json +1 -2
- package/skills/pi-distro/SKILL.md +143 -51
- package/harnesses/minimal/README.md +0 -21
- package/harnesses/minimal/files/AGENTS.md +0 -20
- package/harnesses/minimal/files/settings.json +0 -4
- package/harnesses/minimal/harness.md +0 -24
- package/harnesses/pi-distro-one/README.md +0 -50
- package/harnesses/pi-distro-one/files/.pi/extensions/claude-statusline.ts +0 -220
- package/harnesses/pi-distro-one/files/AGENTS.md +0 -166
- package/harnesses/pi-distro-one/files/settings.json +0 -9
- package/harnesses/pi-distro-one/harness.md +0 -79
- package/harnesses/web-fullstack/README.md +0 -25
- package/harnesses/web-fullstack/files/.pi/prompts/review.md +0 -12
- package/harnesses/web-fullstack/files/AGENTS.md +0 -37
- package/harnesses/web-fullstack/files/settings.json +0 -11
- package/harnesses/web-fullstack/harness.md +0 -40
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 - 2026-07-13
|
|
4
|
+
|
|
5
|
+
- **Install scope: local vs global.** Every component of a distro can now be installed
|
|
6
|
+
**project-locally** (default, `./.pi/`) or **globally** (`~/.pi/agent/`, opt-in). At
|
|
7
|
+
deploy/pick time the agent builds a deployment plan and offers three presets
|
|
8
|
+
(accept-defaults / all-global-where-safe / customize), with safety guards for dangerous
|
|
9
|
+
types (settings.json, SYSTEM.md/APPEND_SYSTEM.md, AGENTS.md). Authors can suggest a
|
|
10
|
+
global default per package with a `(global)` marker. `status`/`undeploy` detect where
|
|
11
|
+
each component actually landed by checking both scopes; `save` captures global config
|
|
12
|
+
too (marked `(global)`).
|
|
13
|
+
- **Offline/rate-limit awareness**: when the official catalogue can't be fetched from
|
|
14
|
+
GitHub, `list`, `status`, and the selectors now say so explicitly instead of implying
|
|
15
|
+
distros don't exist or were removed.
|
|
16
|
+
- **Official distro names in tab-completion**: the last successful catalogue listing is
|
|
17
|
+
cached to `~/.pi/harnesses/.official-cache.json`, so `deploy`/`show`/`pick` completion
|
|
18
|
+
offers official names, not just local ones.
|
|
19
|
+
- **Robustness fixes**: provenance header parsing is now field-order-independent;
|
|
20
|
+
`compareVersions` handles `v` prefixes and prerelease suffixes (`1.0.0-beta < 1.0.0`);
|
|
21
|
+
the `(global)` scope marker is only honored between the package ref and the description
|
|
22
|
+
dash (prose mentions no longer count); an h1 heading now also terminates the
|
|
23
|
+
`## pi packages` section; `status` renders object-form package entries correctly.
|
|
24
|
+
- **Save snapshot hygiene**: skips `.pi/loops/` runtime state, detects binary files
|
|
25
|
+
instead of inlining garbage, and caps the total inlined snapshot at 256 KB (per-file
|
|
26
|
+
cap unchanged; omitted files are listed by path).
|
|
27
|
+
- **GitHub temp clones cleaned up**: deploy/pick kickoffs now instruct the agent to
|
|
28
|
+
remove the temporary clone after copying bundled files.
|
|
29
|
+
- **CLI polish**: extra arguments are warned about instead of silently ignored; the
|
|
30
|
+
command description lists all subcommands.
|
|
31
|
+
- **Distro fixes**: `web-fullstack` 0.2.0 — removed the non-functional `tools` key from
|
|
32
|
+
bundled settings (pi restricts tools via the `--tools` launch flag, now documented in
|
|
33
|
+
the directives); `minimal` 0.1.1 — removed the empty `packages` array from bundled
|
|
34
|
+
settings (merge hazard); `trip-planner` 0.3.0 — statusline now provided by
|
|
35
|
+
`npm:pi-cc-status` (bundled extension removed), supi post-install aligned with
|
|
36
|
+
cc-knockoff's config-file flow, description shortened to the ≤300-char limit;
|
|
37
|
+
`cc-knockoff` 1.2.1 — README global-settings path corrected to
|
|
38
|
+
`~/.pi/agent/settings.json`.
|
|
39
|
+
- **New test suite for shipped distros** (`tests/harnesses.test.ts`): validates
|
|
40
|
+
frontmatter rules (name/dir match, plain semver, description length), README presence,
|
|
41
|
+
and that the bundled-files manifest matches `files/` on disk in both directions.
|
|
42
|
+
|
|
43
|
+
## 0.3.0 - 2026-07-10
|
|
44
|
+
|
|
45
|
+
- **Dynamic official distros from GitHub**: official distros (`minimal`, `web-fullstack`,
|
|
46
|
+
`cc-knockoff`) are no longer bundled inside the npm package. The catalogue fetches them
|
|
47
|
+
dynamically from the [`msdavid/pi-distro`](https://github.com/msdavid/pi-distro) repo's
|
|
48
|
+
`harnesses/` directory via the GitHub Contents API (listed cheaply, cloned on demand
|
|
49
|
+
when selected). Publishing a new official distro now only requires pushing to the repo —
|
|
50
|
+
no npm release needed. Selectors and `/pi-distro list` label each distro's source
|
|
51
|
+
clearly: **Official**, **Local**, or **GitHub (<owner>/<repo>)**. Official distros come
|
|
52
|
+
from the trusted package repo and skip the GitHub security confirmation that other-repo
|
|
53
|
+
distros require. Network failures degrade gracefully (local-only catalogue).
|
|
54
|
+
- **`harnesses/` removed from the npm tarball**: `package.json` `files` no longer includes
|
|
55
|
+
`harnesses/`. The directory remains in the repo as the GitHub source of truth.
|
|
56
|
+
- **`/pi-distro status` update check** now also checks official (GitHub) sources for
|
|
57
|
+
updates via the cached listing, instead of only local sources.
|
|
58
|
+
- **Renamed `pi-distro-one` → `cc-knockoff`**: the distro formerly known as `pi-distro-one`
|
|
59
|
+
is renamed `cc-knockoff` everywhere (directory, frontmatter, docs). Projects with
|
|
60
|
+
`appliedHarness: pi-distro-one` provenance will show "no longer in the catalogue" —
|
|
61
|
+
redeploy as `cc-knockoff` to migrate. No automatic migration is performed.
|
|
62
|
+
|
|
3
63
|
## 0.2.0 - 2026-07-08
|
|
4
64
|
|
|
5
65
|
- **`/pi-distro undeploy`**: the reverse of `deploy` — removes an applied distro from the
|
|
@@ -35,7 +95,7 @@
|
|
|
35
95
|
- **Semantic redundancy check**: the package-conflict check now evaluates semantic overlap
|
|
36
96
|
(different tool names, similar function), not just exact name collisions. Offers
|
|
37
97
|
skip / replace / keep both / cancel.
|
|
38
|
-
- **Auto-expand tool outputs**:
|
|
98
|
+
- **Auto-expand tool outputs**: cc-knockoff's status-line extension now auto-expands
|
|
39
99
|
tool output on session start (the Ctrl+O effect), while keeping thinking blocks hidden.
|
|
40
100
|
- **Per-distro README.md**: saved distros now include a `README.md` with an extended
|
|
41
101
|
human-readable description. All seed distros ship one too.
|
package/README.md
CHANGED
|
@@ -23,11 +23,13 @@ existing setup — nothing is silently overwritten.
|
|
|
23
23
|
## Get a fully-configured coder in 60 seconds
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
# 1. Install the package
|
|
26
|
+
# 1. Install the package (in your shell)
|
|
27
27
|
pi install npm:@msdavid/pi-distro
|
|
28
28
|
|
|
29
|
-
# 2.
|
|
30
|
-
|
|
29
|
+
# 2. Start pi, then deploy a distro from inside the session
|
|
30
|
+
# (cc-knockoff = a Claude Code-style multi-agent coder)
|
|
31
|
+
pi
|
|
32
|
+
> /pi-distro deploy cc-knockoff
|
|
31
33
|
|
|
32
34
|
# 3. Restart pi — done.
|
|
33
35
|
```
|
|
@@ -46,7 +48,10 @@ explore-before-acting methodology — all configured and ready to go.
|
|
|
46
48
|
substitutes, or chooses. Every state-changing decision (file merge, package install, tool
|
|
47
49
|
conflict, upgrade) is surfaced with options, and the agent waits for your explicit choice
|
|
48
50
|
- 📦 **Project-local by default** — each project gets its own isolated configuration, so
|
|
49
|
-
different projects can use different harnesses (coding, research, automation, trading…)
|
|
51
|
+
different projects can use different harnesses (coding, research, automation, trading…).
|
|
52
|
+
Need a component everywhere? **Install globally** — at deploy time, choose
|
|
53
|
+
accept-defaults / all-global / customize per component (with safety guards for
|
|
54
|
+
machine-wide changes)
|
|
50
55
|
- 🔄 **Round-trip** — snapshot your live config back into a reusable distro with `/pi-distro save`
|
|
51
56
|
- 🐙 **GitHub distros** — deploy distros straight from any GitHub repo, so your personal
|
|
52
57
|
config follows you to any machine (`/pi-distro deploy owner/repo`)
|
|
@@ -110,6 +115,57 @@ Other advantages:
|
|
|
110
115
|
You can still use global packages and settings alongside project-level ones — pi merges
|
|
111
116
|
them. But the distro itself lives at the project level.
|
|
112
117
|
|
|
118
|
+
## Installing locally vs globally
|
|
119
|
+
|
|
120
|
+
Although pi-distro defaults to **project-local** installation (so each project gets its own
|
|
121
|
+
isolated harness), you can choose to install any component **globally** — shared across
|
|
122
|
+
every project and session on your machine. This is an opt-in choice made at deploy time; the
|
|
123
|
+
project-local default is never changed silently.
|
|
124
|
+
|
|
125
|
+
When you run `/pi-distro deploy` (or `/pi-distro pick`), the agent builds a **deployment
|
|
126
|
+
plan** listing every component with its default scope, then offers three presets:
|
|
127
|
+
|
|
128
|
+
- **Accept defaults** — keep each component at its default scope (recommended). Most things
|
|
129
|
+
go project-local; themes default to global (they're user-wide by nature).
|
|
130
|
+
- **All-global (where safe)** — install every safe component globally. Dangerous types
|
|
131
|
+
(settings, `SYSTEM.md`, `AGENTS.md`) stay project-local with a surfaced warning, because
|
|
132
|
+
their blast radius is machine-wide.
|
|
133
|
+
- **Customize** — walk components one at a time and pick `local` / `global` / `skip` for each.
|
|
134
|
+
|
|
135
|
+
### What can go global
|
|
136
|
+
|
|
137
|
+
| Component | Default | Global? |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| Packages | local | ✅ |
|
|
140
|
+
| Extensions | local | ✅ |
|
|
141
|
+
| Skills | local | ✅ |
|
|
142
|
+
| Prompts | local | ✅ |
|
|
143
|
+
| Themes | **global** | ✅ |
|
|
144
|
+
| `settings.json` merge | local | ⚠️ guarded (explicit confirm) |
|
|
145
|
+
| `SYSTEM.md` / `APPEND_SYSTEM.md` | local | ⚠️ double-confirm |
|
|
146
|
+
| `AGENTS.md` | local | ⚠️ guarded (explicit confirm) |
|
|
147
|
+
|
|
148
|
+
Global placement writes to `~/.pi/agent/` (packages via `pi install` →
|
|
149
|
+
`~/.pi/agent/settings.json`; extensions/themes/skills/prompts into `~/.pi/agent/<type>/`).
|
|
150
|
+
Project-local writes to `./.pi/` (packages via `pi install -l`). pi merges the two;
|
|
151
|
+
project-local shadows global on conflict.
|
|
152
|
+
|
|
153
|
+
### Tracking global installs
|
|
154
|
+
|
|
155
|
+
Provenance (`./.pi/harness.md`) records the distro's directives — it does **not** record
|
|
156
|
+
scope. So `/pi-distro status` and `/pi-distro undeploy` detect where each component
|
|
157
|
+
actually landed by checking **both** `./.pi/...` and `~/.pi/agent/...` (plus `pi list` for
|
|
158
|
+
packages). When you undeploy, the agent offers the right removal command per location
|
|
159
|
+
(`pi remove -l` for local, `pi remove` for global). `/pi-distro save` captures
|
|
160
|
+
globally-installed config too (marked `(global)` in the snapshot), so saved distros stay
|
|
161
|
+
reproducible.
|
|
162
|
+
|
|
163
|
+
### Authoring a distro with a global hint
|
|
164
|
+
|
|
165
|
+
Distro authors can suggest a global default for a package with the `(global)` marker — see
|
|
166
|
+
[docs/authoring.md](docs/authoring.md). The hint is a suggested default; the user's
|
|
167
|
+
preset still governs at deploy time.
|
|
168
|
+
|
|
113
169
|
## Workflows
|
|
114
170
|
|
|
115
171
|
### Global setup (install once, use everywhere)
|
|
@@ -145,7 +201,7 @@ to apply — just the packages you want, or a single extension, or a subset of t
|
|
|
145
201
|
This is how you combine pieces from different distros to build your own:
|
|
146
202
|
|
|
147
203
|
```bash
|
|
148
|
-
/pi-distro pick
|
|
204
|
+
/pi-distro pick cc-knockoff # pick, say, just pi-subagents + the statusline
|
|
149
205
|
/pi-distro pick web-fullstack # then add web research from another distro
|
|
150
206
|
/pi-distro save # snapshot the combination as your own distro
|
|
151
207
|
```
|
|
@@ -166,7 +222,7 @@ doesn't write provenance (it's a custom config, not an applied distro) — run
|
|
|
166
222
|
```bash
|
|
167
223
|
/pi-distro status # shows the applied distro + live config
|
|
168
224
|
/pi-distro list # lists all available distros
|
|
169
|
-
/pi-distro show
|
|
225
|
+
/pi-distro show cc-knockoff # dry-run preview of what a distro would do
|
|
170
226
|
```
|
|
171
227
|
|
|
172
228
|
### Bring your config to any machine
|
|
@@ -239,22 +295,35 @@ All interaction happens through a single slash command — there is no standalon
|
|
|
239
295
|
|
|
240
296
|
The effective catalogue is the union of:
|
|
241
297
|
|
|
242
|
-
- **
|
|
243
|
-
|
|
298
|
+
- **Official distros** — fetched dynamically from the [`msdavid/pi-distro`](https://github.com/msdavid/pi-distro)
|
|
299
|
+
repo's `harnesses/` directory on GitHub (no longer bundled inside the npm package, so
|
|
300
|
+
new official distros ship by pushing to the repo — no npm release needed). The catalogue
|
|
301
|
+
is listed via the GitHub Contents API and each distro is cloned on demand when you
|
|
302
|
+
select it. Currently:
|
|
244
303
|
- **minimal** — clean starting point: basic `AGENTS.md` + `.pi/settings.json`.
|
|
245
304
|
- **web-fullstack** — React/Node project with web research, review skills, restricted tools.
|
|
246
|
-
- **
|
|
305
|
+
- **cc-knockoff** — a Claude Code–style multi-agent coder (see below).
|
|
247
306
|
- **User distros** — saved by you to `~/.pi/harnesses/<name>/` via `/pi-distro save`.
|
|
248
|
-
- **GitHub distros** — fetched on-demand from any GitHub repo.
|
|
307
|
+
- **GitHub distros** — fetched on-demand from any GitHub repo (`/pi-distro deploy owner/repo`).
|
|
249
308
|
- **Partial/combined configs** — built from `/pi-distro pick` across multiple distros, then
|
|
250
309
|
saved as a user distro.
|
|
251
310
|
|
|
311
|
+
Selectors and `/pi-distro list` show each distro's source clearly: **Official** (the
|
|
312
|
+
`msdavid/pi-distro` repo), **Local** (your `~/.pi/harnesses/`), or **GitHub (<owner>/<repo>)**
|
|
313
|
+
for distros from other repos.
|
|
314
|
+
|
|
315
|
+
If GitHub is unreachable (offline, or the unauthenticated API rate limit is hit), the
|
|
316
|
+
catalogue degrades to local-only and `/pi-distro list`, `/pi-distro status`, and the
|
|
317
|
+
selectors say so explicitly — official distros are temporarily hidden, not gone.
|
|
318
|
+
|
|
252
319
|
On a name collision, the **user distro takes precedence** — save a distro with the same
|
|
253
|
-
name as
|
|
320
|
+
name as an official distro to override it. Official distros come from a trusted repo (the
|
|
321
|
+
package's own repo), so they skip the GitHub security confirmation that other-repo
|
|
322
|
+
distros require.
|
|
254
323
|
|
|
255
|
-
###
|
|
324
|
+
### cc-knockoff
|
|
256
325
|
|
|
257
|
-
`
|
|
326
|
+
`cc-knockoff` is the author's draft shot at using the most popular pi coding packages to
|
|
258
327
|
closely resemble the capabilities of Claude Code. It's opinionated — spawning and
|
|
259
328
|
coordinating autonomous sub-agents is the primary capability, with web research, browser
|
|
260
329
|
automation, live shell, model routing, and task management integrated in support. It
|
|
@@ -300,7 +369,9 @@ it's saved to `~/.pi/harnesses/<name>/` with a `README.md` describing the distro
|
|
|
300
369
|
|
|
301
370
|
### Author a distro by hand
|
|
302
371
|
|
|
303
|
-
Create a directory under `~/.pi/harnesses/<name>/` (or contribute
|
|
372
|
+
Create a directory under `~/.pi/harnesses/<name>/` (or contribute an official distro via a
|
|
373
|
+
PR to the [`msdavid/pi-distro`](https://github.com/msdavid/pi-distro) repo's `harnesses/`
|
|
374
|
+
dir)
|
|
304
375
|
with a `harness.md` and optional `files/`. See
|
|
305
376
|
[docs/authoring.md](docs/authoring.md) for the complete format reference — frontmatter
|
|
306
377
|
fields, bundled file conventions, directive sections, and merge-don't-clobber
|
package/docs/authoring.md
CHANGED
|
@@ -125,25 +125,45 @@ manifest the agent uses during `deploy`.
|
|
|
125
125
|
### `## pi packages to install`
|
|
126
126
|
|
|
127
127
|
List pi packages the distro depends on. The agent installs these with `pi install -l`
|
|
128
|
-
(**project-local** — writes to `./.pi/settings.json
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
128
|
+
(**project-local** — writes to `./.pi/settings.json`) **or** `pi install` (**global** — writes
|
|
129
|
+
to `~/.pi/agent/settings.json`, shared across every project) per the user's deploy-time
|
|
130
|
+
scope choice, **only after confirming with the user** AND after a
|
|
131
|
+
**redundancy/conflict evaluation**: the agent compares each package's stated purpose against
|
|
132
|
+
the project's already-active tools and installed packages (both local and global, via
|
|
133
|
+
`pi list`), checking for both exact tool-name collisions and semantic redundancy (different
|
|
134
|
+
names, similar function). If overlap is detected, it offers the user **skip / replace /
|
|
135
|
+
keep both / cancel** instead of installing blindly. (Exact name collisions are non-fatal in
|
|
136
|
+
pi — project-local tools shadow global ones — but redundancy leaves a confusing duplicate
|
|
137
|
+
tool set, so the agent surfaces it for the user to decide.)
|
|
138
|
+
|
|
139
|
+
**Do not list these packages in the bundled `settings.json` `packages` array.**
|
|
140
|
+
`pi install -l` / `pi install` are the single mechanisms that register a package: each
|
|
141
|
+
installs the package AND appends the source to the corresponding settings file on success,
|
|
142
|
+
and leaves settings untouched on failure. This way a failed install never leaves a dangling,
|
|
143
|
+
unresolvable entry in settings.
|
|
144
|
+
|
|
145
|
+
#### Scope hint: `(global)`
|
|
146
|
+
|
|
147
|
+
By default, a package is suggested for **project-local** install. To suggest **global** as
|
|
148
|
+
the author-intended default, suffix the entry with `(global)`:
|
|
141
149
|
|
|
142
150
|
```markdown
|
|
143
151
|
## pi packages to install
|
|
144
152
|
- `npm:pi-browse` — for web search and content extraction
|
|
153
|
+
- `npm:my-shared-tool` (global) — a tool the user wants in every project
|
|
145
154
|
```
|
|
146
155
|
|
|
156
|
+
The hint is a **suggested default** for the deployment plan — it does not force global
|
|
157
|
+
install. At deploy time the user picks a preset (accept-defaults / all-global-where-safe /
|
|
158
|
+
customize) which governs the final scope. Use `(global)` for packages that are genuinely
|
|
159
|
+
user-wide (a favorite status-line, a shared utility); leave it off for project-specific
|
|
160
|
+
dependencies.
|
|
161
|
+
|
|
162
|
+
> **Marker placement:** the `(global)` / `(local)` marker must appear **between the
|
|
163
|
+
> package reference and the description dash** (as in the example above). A `(global)`
|
|
164
|
+
> that appears inside the description prose after the `—` dash is intentionally ignored,
|
|
165
|
+
> so descriptions can mention the word without changing the scope.
|
|
166
|
+
|
|
147
167
|
### `## Hooks`
|
|
148
168
|
|
|
149
169
|
Describe extensions/hooks to create under `./.pi/extensions/`. These are bundled as
|
|
@@ -161,6 +181,56 @@ Reference bundled skills (under `files/.pi/skills/`) and prompts (under
|
|
|
161
181
|
`files/.pi/prompts/`) that should be deployed, or describe skills/prompts the agent
|
|
162
182
|
should create.
|
|
163
183
|
|
|
184
|
+
## Install scope: local vs global
|
|
185
|
+
|
|
186
|
+
Every component a distro installs can be placed either **project-local** (the default,
|
|
187
|
+
under `./.pi/` — scoped to this project) or **global** (under `~/.pi/agent/` — shared
|
|
188
|
+
across every project and session on the machine). pi merges the two; project-local
|
|
189
|
+
shadows global on conflict. pi-distro's default philosophy is **project-local**, because
|
|
190
|
+
different projects need different harnesses — global is an opt-in choice the user makes at
|
|
191
|
+
deploy time.
|
|
192
|
+
|
|
193
|
+
### Per-type default scopes
|
|
194
|
+
|
|
195
|
+
| Component type | Default scope | Global allowed? |
|
|
196
|
+
|---|---|---|
|
|
197
|
+
| Packages | local | yes |
|
|
198
|
+
| Extensions | local | yes |
|
|
199
|
+
| Skills | local | yes |
|
|
200
|
+
| Prompts | local | yes |
|
|
201
|
+
| Themes | **global** | yes |
|
|
202
|
+
| settings.json merge | local | guarded (explicit confirm) |
|
|
203
|
+
| SYSTEM.md / APPEND_SYSTEM.md | local | double-confirm |
|
|
204
|
+
| AGENTS.md | local | guarded (explicit confirm) |
|
|
205
|
+
|
|
206
|
+
At deploy (and `/pi-distro pick`), the agent builds a **deployment plan** from the
|
|
207
|
+
directives — grouping every component with its default scope — and offers the user three
|
|
208
|
+
presets:
|
|
209
|
+
|
|
210
|
+
- **(a) Accept defaults** — keep each component at its default scope (recommended).
|
|
211
|
+
- **(b) All-global (where safe)** — flip every global-allowed component to global;
|
|
212
|
+
dangerous types (settings, SYSTEM.md, AGENTS.md) stay local with a surfaced warning.
|
|
213
|
+
- **(c) Customize** — walk items one at a time, offering `local` / `global` / `skip`.
|
|
214
|
+
|
|
215
|
+
When a dangerous type's final scope is global, the agent surfaces the blast radius
|
|
216
|
+
("affects every project/session on this machine") and requires explicit confirmation
|
|
217
|
+
(double-confirm for SYSTEM.md/APPEND_SYSTEM.md).
|
|
218
|
+
|
|
219
|
+
### `## Global deployment notes`
|
|
220
|
+
|
|
221
|
+
If your distro is intended to place some bundled files **globally** (e.g. an extension that
|
|
222
|
+
should live at `~/.pi/agent/extensions/` rather than `./.pi/extensions/`), add a `## Global
|
|
223
|
+
deployment notes` section listing which file targets should go global:
|
|
224
|
+
|
|
225
|
+
```markdown
|
|
226
|
+
## Global deployment notes
|
|
227
|
+
- `.pi/extensions/my-shared-ext.ts` → deploy globally to `~/.pi/agent/extensions/my-shared-ext.ts`
|
|
228
|
+
- `.pi/themes/shared-theme.json` → deploy globally to `~/.pi/agent/themes/shared-theme.json`
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The agent reads this note and places those files at the global path per the user's scope
|
|
232
|
+
choice. For packages, prefer the `(global)` marker in `## pi packages to install` instead.
|
|
233
|
+
|
|
164
234
|
## Merge-don't-clobber expectations
|
|
165
235
|
|
|
166
236
|
When a distro is deployed via `/pi-distro deploy`, **existing files are never
|
|
@@ -187,14 +257,14 @@ frontmatter + directives) with a provenance header injected at the top of the bo
|
|
|
187
257
|
<!-- pi-distro provenance
|
|
188
258
|
appliedHarness: <name>
|
|
189
259
|
appliedVersion: <version>
|
|
190
|
-
sourceCatalogue: <user|
|
|
260
|
+
sourceCatalogue: <user|github:owner/repo[/subpath]>
|
|
191
261
|
lastUpdated: <ISO8601>
|
|
192
262
|
-->
|
|
193
263
|
```
|
|
194
264
|
|
|
195
265
|
- `appliedHarness`: the name of the distro that was deployed.
|
|
196
266
|
- `appliedVersion`: the version from the distro frontmatter.
|
|
197
|
-
- `sourceCatalogue`: `
|
|
267
|
+
- `sourceCatalogue`: `user` (from `~/.pi/harnesses/`) or `github:owner/repo[/subpath]` (official distros are `github:msdavid/pi-distro/harnesses/<name>`).
|
|
198
268
|
- `lastUpdated`: ISO 8601 timestamp of the last apply/save.
|
|
199
269
|
|
|
200
270
|
The provenance file is updated automatically by the `deploy`, `save`, and `undeploy`
|
|
@@ -234,5 +304,9 @@ Never keep the same version when updating a distro — the version should always
|
|
|
234
304
|
that something changed. When authoring a distro by hand, pick a starting version (e.g.
|
|
235
305
|
`0.1.0`) and bump it on each meaningful change.
|
|
236
306
|
|
|
307
|
+
Write versions as plain `MAJOR.MINOR.PATCH` — no `v` prefix. Prerelease suffixes
|
|
308
|
+
(`1.0.0-beta`) are tolerated and sort before their release, but plain releases are
|
|
309
|
+
recommended for distros.
|
|
310
|
+
|
|
237
311
|
The saved distro is immediately available in `/pi-distro list` and can be deployed
|
|
238
312
|
into other projects with `/pi-distro deploy`.
|
package/extensions/catalogue.ts
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Catalogue reading logic for pi-distro.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The effective catalogue is the union of:
|
|
5
|
+
* - **Official distros** — fetched dynamically from `msdavid/pi-distro`'s
|
|
6
|
+
* `harnesses/` directory on GitHub (no longer bundled in the npm package).
|
|
7
|
+
* - **User distros** — saved locally to `~/.pi/harnesses/`.
|
|
8
|
+
* On a name collision, the user distro takes precedence (so a user can override
|
|
9
|
+
* an official distro by saving one with the same name).
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import { readdirSync, readFileSync, existsSync } from "node:fs";
|
|
9
|
-
import { join,
|
|
13
|
+
import { join, relative } from "node:path";
|
|
10
14
|
import { homedir } from "node:os";
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
12
15
|
import { parseFrontmatter, extractBody } from "./frontmatter.ts";
|
|
16
|
+
import { listOfficialDistros, isOfficialSource } from "./github.ts";
|
|
13
17
|
|
|
14
18
|
export interface HarnessEntry {
|
|
15
19
|
name: string;
|
|
16
20
|
title: string;
|
|
17
21
|
description: string;
|
|
18
22
|
version: string;
|
|
19
|
-
source: string; // "
|
|
20
|
-
dir
|
|
21
|
-
harnessMdPath
|
|
23
|
+
source: string; // "user", "github:owner/repo[/subpath]", or official "github:msdavid/pi-distro/harnesses/<name>"
|
|
24
|
+
dir?: string; // on-disk dir (user distros, or a fetched GitHub clone). Absent for official *listing* entries.
|
|
25
|
+
harnessMdPath?: string; // path to harness.md. Absent for official *listing* entries (fetched on selection).
|
|
22
26
|
filesDir?: string;
|
|
27
|
+
needsFetch?: boolean; // true for official listing entries — must fetchGithubDistro before use
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
export interface BundledFile {
|
|
@@ -27,17 +32,35 @@ export interface BundledFile {
|
|
|
27
32
|
target: string;
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
export interface PackageEntry {
|
|
36
|
+
source: string;
|
|
37
|
+
scope: "local" | "global"; // author hint; the user's deploy-time preset still governs
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
/** Parse pi package references (npm:...) from a directives body. */
|
|
31
41
|
export function parsePackageList(body: string): string[] {
|
|
32
|
-
|
|
42
|
+
return parsePackageListWithScope(body).map((p) => p.source);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Parse pi package references with their author-specified scope hint.
|
|
46
|
+
* A package may be suffixed with `(global)` to suggest global install; otherwise
|
|
47
|
+
* the default scope is `local`. The hint is a *default* — the user's deploy-time
|
|
48
|
+
* preset (accept-defaults / all-global / customize) still governs the final scope. */
|
|
49
|
+
export function parsePackageListWithScope(body: string): PackageEntry[] {
|
|
50
|
+
const packages: PackageEntry[] = [];
|
|
33
51
|
let inSection = false;
|
|
34
52
|
for (const line of body.split("\n")) {
|
|
35
53
|
const t = line.trim();
|
|
36
54
|
if (/^##\s+pi\s*packages/i.test(t)) { inSection = true; continue; }
|
|
37
55
|
if (inSection) {
|
|
38
|
-
if (
|
|
39
|
-
const m = t.match(/^-\s+`(npm:[^`]+)
|
|
40
|
-
if (m)
|
|
56
|
+
if (/^#{1,2}\s/.test(t)) { inSection = false; continue; } // any h1/h2 ends the section
|
|
57
|
+
const m = t.match(/^-\s+`(npm:[^`]+)`([^—–]*)/);
|
|
58
|
+
if (m) {
|
|
59
|
+
// The scope marker must appear between the package ref and the
|
|
60
|
+
// description dash — "(global)" inside description prose is ignored.
|
|
61
|
+
const scopeM = m[2].match(/\((global|local)\)/);
|
|
62
|
+
packages.push({ source: m[1], scope: scopeM?.[1] === "global" ? "global" : "local" });
|
|
63
|
+
}
|
|
41
64
|
}
|
|
42
65
|
}
|
|
43
66
|
return packages;
|
|
@@ -50,74 +73,86 @@ export interface Provenance {
|
|
|
50
73
|
lastUpdated: string;
|
|
51
74
|
}
|
|
52
75
|
|
|
53
|
-
/** Parse the provenance header from a harness.md content.
|
|
76
|
+
/** Parse the provenance header from a harness.md content. Fields are matched
|
|
77
|
+
* independently within the provenance comment block, so their order (and any
|
|
78
|
+
* extra fields an agent may have added) doesn't matter. */
|
|
54
79
|
export function parseProvenance(content: string): Provenance | null {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Synchronous catalogue name read for autocomplete (best-effort).
|
|
80
|
+
const block = content.match(/<!--\s*pi-distro provenance([\s\S]*?)-->/);
|
|
81
|
+
if (!block) return null;
|
|
82
|
+
const field = (name: string): string | undefined =>
|
|
83
|
+
block[1].match(new RegExp(`${name}:[ \\t]*(.+)`))?.[1].trim();
|
|
84
|
+
const appliedHarness = field("appliedHarness");
|
|
85
|
+
const appliedVersion = field("appliedVersion");
|
|
86
|
+
const sourceCatalogue = field("sourceCatalogue");
|
|
87
|
+
const lastUpdated = field("lastUpdated");
|
|
88
|
+
if (!appliedHarness || !appliedVersion || !sourceCatalogue || !lastUpdated) return null;
|
|
89
|
+
return { appliedHarness, appliedVersion, sourceCatalogue, lastUpdated };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Synchronous catalogue name read for autocomplete (best-effort).
|
|
93
|
+
* User distros are read from disk (by frontmatter name, falling back to the
|
|
94
|
+
* directory name). Official distros live on GitHub and can't be fetched
|
|
95
|
+
* synchronously — but the last successful async listing is cached to
|
|
96
|
+
* `.official-cache.json` (see github.ts), so their names complete too once
|
|
97
|
+
* any command has read the catalogue. */
|
|
68
98
|
export function getCatalogueNamesSync(): string[] {
|
|
99
|
+
const dir = getUserHarnessesDir();
|
|
69
100
|
const names: string[] = [];
|
|
70
|
-
|
|
71
|
-
if (!existsSync(dir)) continue;
|
|
101
|
+
if (existsSync(dir)) {
|
|
72
102
|
try {
|
|
73
103
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
74
|
-
if (entry.isDirectory() && entry.name !== ".trash"
|
|
75
|
-
|
|
104
|
+
if (entry.isDirectory() && entry.name !== ".trash") {
|
|
105
|
+
const harnessMdPath = join(dir, entry.name, "harness.md");
|
|
106
|
+
if (!existsSync(harnessMdPath)) continue;
|
|
107
|
+
try {
|
|
108
|
+
const fm = parseFrontmatter(readFileSync(harnessMdPath, "utf-8"));
|
|
109
|
+
names.push(fm?.name ?? entry.name);
|
|
110
|
+
} catch {
|
|
111
|
+
names.push(entry.name);
|
|
112
|
+
}
|
|
76
113
|
}
|
|
77
114
|
}
|
|
78
115
|
} catch { /* ignore */ }
|
|
79
116
|
}
|
|
117
|
+
try {
|
|
118
|
+
const cached = JSON.parse(readFileSync(join(dir, ".official-cache.json"), "utf-8"));
|
|
119
|
+
if (Array.isArray(cached)) {
|
|
120
|
+
for (const n of cached) if (typeof n === "string") names.push(n);
|
|
121
|
+
}
|
|
122
|
+
} catch { /* no cache yet — officials appear after the first catalogue read */ }
|
|
80
123
|
return [...new Set(names)].sort();
|
|
81
124
|
}
|
|
82
125
|
|
|
83
|
-
/**
|
|
84
|
-
* Resolve the package root directory (the dir containing package.json).
|
|
85
|
-
* Uses import.meta.url — in extensions/index.ts this resolves to the
|
|
86
|
-
* extensions/ dir, so the package dir is one level up.
|
|
87
|
-
*/
|
|
88
|
-
export function getPackageDir(): string {
|
|
89
|
-
const extensionsDir = dirname(fileURLToPath(import.meta.url));
|
|
90
|
-
return dirname(extensionsDir);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Directory containing seed harnesses shipped with the package. */
|
|
94
|
-
export function getSeedHarnessesDir(): string {
|
|
95
|
-
return join(getPackageDir(), "harnesses");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
126
|
/** Directory containing user-saved harnesses (~/.pi/harnesses/). */
|
|
99
127
|
export function getUserHarnessesDir(): string {
|
|
100
128
|
return join(homedir(), ".pi", "harnesses");
|
|
101
129
|
}
|
|
102
130
|
|
|
103
131
|
/**
|
|
104
|
-
* Read the effective catalogue:
|
|
132
|
+
* Read the effective catalogue: official distros (GitHub, dynamic) ∪ user harnesses.
|
|
105
133
|
* On name collision, user takes precedence.
|
|
106
|
-
* Returns sorted:
|
|
134
|
+
* Returns sorted: official first (alphabetical), then user (alphabetical).
|
|
135
|
+
* Never throws on network failure — official distros are simply omitted and the
|
|
136
|
+
* catalogue degrades to local-only.
|
|
107
137
|
*/
|
|
108
138
|
export async function readCatalogue(): Promise<HarnessEntry[]> {
|
|
109
|
-
const
|
|
139
|
+
const official = await listOfficialDistros();
|
|
110
140
|
const users = readHarnessesFromDir(getUserHarnessesDir(), "user");
|
|
111
141
|
|
|
112
142
|
// User takes precedence on collision
|
|
113
143
|
const userNames = new Set(users.map((u) => u.name));
|
|
114
|
-
const
|
|
144
|
+
const filteredOfficial = official.filter((o) => !userNames.has(o.name));
|
|
115
145
|
|
|
116
|
-
|
|
117
|
-
filteredSeeds.sort((a, b) => a.name.localeCompare(b.name));
|
|
146
|
+
filteredOfficial.sort((a, b) => a.name.localeCompare(b.name));
|
|
118
147
|
users.sort((a, b) => a.name.localeCompare(b.name));
|
|
119
148
|
|
|
120
|
-
return [...
|
|
149
|
+
return [...filteredOfficial, ...users];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Read only local (user) harnesses — no network. Used by update/status local
|
|
153
|
+
* resolution and anywhere a network call is undesirable. */
|
|
154
|
+
export function readLocalCatalogue(): HarnessEntry[] {
|
|
155
|
+
return readHarnessesFromDir(getUserHarnessesDir(), "user");
|
|
121
156
|
}
|
|
122
157
|
|
|
123
158
|
/** Find a harness by name in the catalogue. */
|
|
@@ -156,7 +191,7 @@ export async function readFullHarnessMd(harnessMdPath: string): Promise<string>
|
|
|
156
191
|
|
|
157
192
|
function readHarnessesFromDir(
|
|
158
193
|
dir: string,
|
|
159
|
-
source: "
|
|
194
|
+
source: "user",
|
|
160
195
|
): HarnessEntry[] {
|
|
161
196
|
if (!existsSync(dir)) return [];
|
|
162
197
|
|
|
@@ -219,3 +254,13 @@ function walkDir(
|
|
|
219
254
|
}
|
|
220
255
|
}
|
|
221
256
|
}
|
|
257
|
+
|
|
258
|
+
// --- Source labelling (for selectors / list / status) ---
|
|
259
|
+
|
|
260
|
+
/** A short human label for a distro source, shown in selectors and lists. */
|
|
261
|
+
export function sourceLabel(source: string): string {
|
|
262
|
+
if (isOfficialSource(source)) return "Official";
|
|
263
|
+
if (source === "user") return "Local";
|
|
264
|
+
if (source.startsWith("github:")) return `GitHub (${source.slice("github:".length)})`;
|
|
265
|
+
return source;
|
|
266
|
+
}
|