@msdavid/pi-distro 0.2.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 +54 -0
- package/LICENSE +21 -0
- package/README.md +394 -0
- package/docs/authoring.md +238 -0
- package/docs/preview.png +0 -0
- package/docs/preview.svg +58 -0
- package/extensions/catalogue.ts +221 -0
- package/extensions/deploy.ts +141 -0
- package/extensions/frontmatter.ts +158 -0
- package/extensions/github.ts +110 -0
- package/extensions/index.ts +86 -0
- package/extensions/info.ts +133 -0
- package/extensions/pick.ts +132 -0
- package/extensions/resolve.ts +70 -0
- package/extensions/save.ts +217 -0
- package/extensions/show.ts +96 -0
- package/extensions/undeploy.ts +124 -0
- package/extensions/update.ts +109 -0
- package/extensions/util.ts +44 -0
- package/harnesses/minimal/README.md +21 -0
- package/harnesses/minimal/files/AGENTS.md +20 -0
- package/harnesses/minimal/files/settings.json +4 -0
- package/harnesses/minimal/harness.md +24 -0
- package/harnesses/pi-distro-one/README.md +50 -0
- package/harnesses/pi-distro-one/files/.pi/extensions/claude-statusline.ts +220 -0
- package/harnesses/pi-distro-one/files/AGENTS.md +166 -0
- package/harnesses/pi-distro-one/files/settings.json +9 -0
- package/harnesses/pi-distro-one/harness.md +79 -0
- package/harnesses/web-fullstack/README.md +25 -0
- package/harnesses/web-fullstack/files/.pi/prompts/review.md +12 -0
- package/harnesses/web-fullstack/files/AGENTS.md +37 -0
- package/harnesses/web-fullstack/files/settings.json +11 -0
- package/harnesses/web-fullstack/harness.md +40 -0
- package/package.json +65 -0
- package/skills/pi-distro/SKILL.md +359 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/pi-distro save` — snapshot the current project config as a reusable distro.
|
|
3
|
+
* Agent-driven; see skills/pi-distro/SKILL.md for the collaborative authoring flow.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ExtensionAPI,
|
|
8
|
+
ExtensionCommandContext,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { readFileSync, statSync, existsSync, readdirSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
import { getUserHarnessesDir } from "./catalogue.ts";
|
|
14
|
+
import { display } from "./util.ts";
|
|
15
|
+
|
|
16
|
+
export async function handleSave(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> {
|
|
17
|
+
const snapshot = ctx.getSystemPromptOptions();
|
|
18
|
+
const cwd = snapshot.cwd;
|
|
19
|
+
|
|
20
|
+
// Enumerate ALL project-local config files so nothing is missed — including per-extension/skill
|
|
21
|
+
// config files, .pi/SYSTEM.md & .pi/APPEND_SYSTEM.md, theme files, subdirectory extensions,
|
|
22
|
+
// nested skill dirs, pi-crew's project-local agents/teams/workflows, and the cross-tool
|
|
23
|
+
// .agents/skills/ dir. Data/runtime dirs and our own provenance file are skipped. Ancestor
|
|
24
|
+
// (parent-dir) AGENTS.md / .agents/skills are out of scope for a single-project distro and
|
|
25
|
+
// are only surfaced via the snapshot's contextFiles list.
|
|
26
|
+
// NOTE: .crew/ is handled separately below — it mixes authored config (agents/teams/workflows)
|
|
27
|
+
// with runtime state (state/artifacts/worktrees/imports/audit/cache/graphs), so only its three
|
|
28
|
+
// config subdirs are captured, not the whole tree.
|
|
29
|
+
const SKIP_NAMES = new Set(["npm", "git", "sessions", "state", "tmp", "node_modules"]);
|
|
30
|
+
const MAX_DUMP_BYTES = 16384;
|
|
31
|
+
const configFiles: string[] = [];
|
|
32
|
+
const pushFile = (fp: string) => {
|
|
33
|
+
let st;
|
|
34
|
+
try { st = statSync(fp); } catch { return; }
|
|
35
|
+
if (st.size > MAX_DUMP_BYTES) {
|
|
36
|
+
configFiles.push(`### ${fp}\n_(${st.size} bytes — read with the \`read\` tool if needed)_`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
configFiles.push(`### ${fp}\n\`\`\`\n${readFileSync(fp, "utf-8")}\n\`\`\``);
|
|
41
|
+
} catch { /* unreadable (binary?) — skip */ }
|
|
42
|
+
};
|
|
43
|
+
const walk = (dir: string, isPiRoot: boolean) => {
|
|
44
|
+
let entries;
|
|
45
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (isPiRoot && entry.name === "harness.md") continue; // provenance — never bundle
|
|
48
|
+
const fp = join(dir, entry.name);
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
if (SKIP_NAMES.has(entry.name)) continue;
|
|
51
|
+
walk(fp, false);
|
|
52
|
+
} else if (entry.isFile()) {
|
|
53
|
+
pushFile(fp);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
// Root context files — pi tries all four variants; only exact AGENTS.md was captured before.
|
|
58
|
+
for (const name of ["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]) {
|
|
59
|
+
const p = join(cwd, name);
|
|
60
|
+
if (existsSync(p)) pushFile(p);
|
|
61
|
+
}
|
|
62
|
+
// Everything under .pi/: settings.json, SYSTEM.md, APPEND_SYSTEM.md, extensions/ (incl.
|
|
63
|
+
// subdir extensions), skills/ (incl. nested), prompts/, themes/, and any per-extension/skill
|
|
64
|
+
// config files (e.g. .pi/<name>.json) that we cannot predict by name.
|
|
65
|
+
const piDir = join(cwd, ".pi");
|
|
66
|
+
if (existsSync(piDir)) walk(piDir, true);
|
|
67
|
+
// Cross-tool skills at project root only (ancestor .agents/skills/ are out of scope).
|
|
68
|
+
const agentsSkills = join(cwd, ".agents", "skills");
|
|
69
|
+
if (existsSync(agentsSkills)) walk(agentsSkills, false);
|
|
70
|
+
// pi-crew project-local authored config: .crew/{agents,teams,workflows}/ (the legacy pi-crew
|
|
71
|
+
// project root, which projectCrewRoot prefers over .pi/teams/ when it exists). Only these
|
|
72
|
+
// three config subdirs are captured — the rest of .crew/ is runtime state (state/artifacts/
|
|
73
|
+
// worktrees/imports/audit/cache/graphs). The .pi/teams/ and .pi/agents/ equivalents are
|
|
74
|
+
// already captured by the .pi/ walk above. Files that are --copy-builtins copies of pi-crew's
|
|
75
|
+
// shipped builtins should be deduped by the agent (see procedure) rather than bundled.
|
|
76
|
+
const crewDir = join(cwd, ".crew");
|
|
77
|
+
if (existsSync(crewDir)) {
|
|
78
|
+
for (const sub of ["agents", "teams", "workflows"]) {
|
|
79
|
+
const subDir = join(crewDir, sub);
|
|
80
|
+
if (existsSync(subDir)) walk(subDir, false);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Existing user harnesses (so the agent can offer update-existing)
|
|
85
|
+
const userDir = getUserHarnessesDir();
|
|
86
|
+
const existing: string[] = [];
|
|
87
|
+
if (existsSync(userDir)) {
|
|
88
|
+
try {
|
|
89
|
+
for (const entry of readdirSync(userDir, { withFileTypes: true })) {
|
|
90
|
+
if (entry.isDirectory() && entry.name !== ".trash"
|
|
91
|
+
&& existsSync(join(userDir, entry.name, "harness.md"))) {
|
|
92
|
+
existing.push(entry.name);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch { /* ignore */ }
|
|
96
|
+
}
|
|
97
|
+
existing.sort();
|
|
98
|
+
|
|
99
|
+
const tools = snapshot.selectedTools?.join(", ") ?? "(default)";
|
|
100
|
+
const skillsList = snapshot.skills?.map((s) => `- ${s.name}: ${s.description}`).join("\n") ?? "_(none)_";
|
|
101
|
+
const contextList = snapshot.contextFiles?.map((c) => `- ${c.path}`).join("\n") ?? "_(none)_";
|
|
102
|
+
const existingList = existing.length > 0 ? existing.map((n) => `- \`${n}\``).join("\n") : "_(none yet)_";
|
|
103
|
+
|
|
104
|
+
pi.sendUserMessage(`## Save current configuration as a distro
|
|
105
|
+
|
|
106
|
+
A live snapshot of this project's pi configuration follows. Draft a \`harness.md\` that
|
|
107
|
+
reproduces it, confirm with the user, then save it to the catalogue.
|
|
108
|
+
|
|
109
|
+
### Live snapshot
|
|
110
|
+
- **Working dir:** ${cwd}
|
|
111
|
+
- **Active tools:** ${tools}
|
|
112
|
+
- **Skills:**
|
|
113
|
+
${skillsList}
|
|
114
|
+
- **Context files:**
|
|
115
|
+
${contextList}
|
|
116
|
+
|
|
117
|
+
### Raw config files
|
|
118
|
+
${configFiles.length > 0 ? configFiles.join("\n\n") : "_(none found)_"}
|
|
119
|
+
|
|
120
|
+
### Existing user distros in the catalogue
|
|
121
|
+
${existingList}
|
|
122
|
+
|
|
123
|
+
Catalogue dir: \`${userDir}\`
|
|
124
|
+
|
|
125
|
+
### Procedure (follow exactly, collaborating with the user)
|
|
126
|
+
|
|
127
|
+
**1. Draft.** Analyse the snapshot and write a \`harness.md\` that reproduces this
|
|
128
|
+
configuration. Frontmatter MUST include: \`name\` (slug: lowercase a-z/0-9/hyphens, no
|
|
129
|
+
leading/trailing/consecutive hyphens), \`title\` (human-readable), \`description\`
|
|
130
|
+
(one-liner, <=300 chars), \`version\` (semver). Optionally \`author\` and \`tags\`.
|
|
131
|
+
The body should have directive sections (Bundled files, pi packages to install, System
|
|
132
|
+
prompt/SYSTEM.md if present, Themes, Context, Skills/prompts, and any per-extension/skill
|
|
133
|
+
config files) mirroring the live config.
|
|
134
|
+
|
|
135
|
+
**2. Propose & confirm.** Show the user the proposed \`name\`, \`title\`, \`description\`,
|
|
136
|
+
and the full draft. Ask for confirmation or edits. Do NOT proceed until the user confirms.
|
|
137
|
+
|
|
138
|
+
**3. Choose save target.** Ask the user whether to:
|
|
139
|
+
- **(a) Save as a new distro** — ask for a name (validate the slug; if it collides with an
|
|
140
|
+
existing user distro or a seed name, warn and ask whether to overwrite), then create
|
|
141
|
+
\`~/.pi/harnesses/<name>/\`.
|
|
142
|
+
- **(b) Update an existing distro** — let the user pick from the existing user distros
|
|
143
|
+
above. Refuse to update a name that is NOT in the existing-user list (those are package
|
|
144
|
+
seeds and are read-only). Before overwriting, back the old distro up: copy
|
|
145
|
+
\`~/.pi/harnesses/<name>/\` to
|
|
146
|
+
\`~/.pi/harnesses/.trash/<name>-<timestamp>/\` (create \`.trash/\` if needed).
|
|
147
|
+
**Bump the version:** read the old distro's \`version\` field and increment it using
|
|
148
|
+
semver: **patch** (\`0.1.0\` -> \`0.1.1\`) for small tweaks/bug fixes, **minor**
|
|
149
|
+
(\`0.1.0\` -> \`0.2.0\`) for new capabilities or config additions, **major**
|
|
150
|
+
(\`0.1.0\` -> \`1.0.0\`) for breaking changes (removed packages, changed conventions). Never
|
|
151
|
+
keep the same version when updating — a distro's version should always reflect that
|
|
152
|
+
something changed. Propose the bumped version to the user and let them confirm or adjust.
|
|
153
|
+
|
|
154
|
+
**4. Write the harness.** Write \`~/.pi/harnesses/<name>/harness.md\` with the confirmed
|
|
155
|
+
frontmatter + directives. Then copy the project's config files into
|
|
156
|
+
\`~/.pi/harnesses/<name>/files/\` so the harness is self-contained and reproducible. Copy
|
|
157
|
+
EVERY file listed under "Raw config files" above, preserving its path relative to the
|
|
158
|
+
project root:
|
|
159
|
+
- root context (\`AGENTS.md\` / \`CLAUDE.md\` / case variants) -> \`files/<name>\`
|
|
160
|
+
- \`.pi/settings.json\` -> \`files/.pi/settings.json\`
|
|
161
|
+
- \`.pi/SYSTEM.md\` / \`.pi/APPEND_SYSTEM.md\` -> \`files/.pi/\` (same name)
|
|
162
|
+
- \`.pi/extensions/**\` (incl. subdirectory extensions with \`index.ts\`/\`package.json\`)
|
|
163
|
+
-> \`files/.pi/extensions/\` (preserve structure)
|
|
164
|
+
- \`.pi/skills/**\` (incl. nested skill dirs) -> \`files/.pi/skills/\`
|
|
165
|
+
- \`.pi/prompts/**\` -> \`files/.pi/prompts/\`
|
|
166
|
+
- \`.pi/themes/**\` -> \`files/.pi/themes/\` (see theme dedup below)
|
|
167
|
+
- \`.agents/skills/**\` (project root only) -> \`files/.agents/skills/\`
|
|
168
|
+
- \`.crew/{agents,teams,workflows}/**\` (pi-crew project-local authored definitions)
|
|
169
|
+
-> \`files/.crew/<sub>/\` (preserve structure). Do NOT copy the rest of \`.crew/\`
|
|
170
|
+
(state/artifacts/worktrees/imports/audit/cache/graphs = runtime data).
|
|
171
|
+
- any per-extension/skill config files (e.g. \`.pi/<name>.json\`) -> \`files/.pi/\` (same path)
|
|
172
|
+
Use \`cp -r\` for directory trees. Only copy files that actually exist. Do NOT copy:
|
|
173
|
+
\`./.pi/harness.md\` (provenance), \`./.pi/npm/\`, \`./.pi/git/\`, \`./.pi/sessions/\`,
|
|
174
|
+
\`./.pi/state/\`, \`./.pi/tmp/\`, \`./.pi/.crew/\`, or any \`node_modules/\`.
|
|
175
|
+
|
|
176
|
+
**Theme dedup:** before bundling a \`.pi/themes/<name>.json\`, check whether that theme
|
|
177
|
+
name is already provided by an installed package (run \`pi list\`; package themes surface
|
|
178
|
+
at \`~/.pi/agent/themes/\`). If a package provides it, do NOT bundle the file — reference
|
|
179
|
+
it via the package install directive instead (a bundled copy would collide on deploy,
|
|
180
|
+
exactly like the pi-crew \`crew-*\` themes). Only bundle genuinely custom themes not
|
|
181
|
+
available from any package.
|
|
182
|
+
|
|
183
|
+
**pi-crew agents/teams/workflows dedup:** \`.crew/agents/foo.md\` (or \`.pi/teams/agents/\`,
|
|
184
|
+
\`.pi/agents/\`) may be a \`--copy-builtins\` copy of a pi-crew shipped builtin rather than
|
|
185
|
+
custom-authored. Before bundling, compare against pi-crew's package builtins at
|
|
186
|
+
\`<pkg>/agents/\` (and \`teams/\`, \`workflows/\`). If a file is an unmodified builtin copy,
|
|
187
|
+
do NOT bundle it — the \`pi-crew\` install directive already provides it. Bundle only
|
|
188
|
+
genuinely custom/authored definitions.
|
|
189
|
+
|
|
190
|
+
**Extension/skill config:** some extensions or skills read their own config file (e.g.
|
|
191
|
+
\`.pi/<extname>.json\`). These appear in the raw config files above. Bundle any that are
|
|
192
|
+
project-local authored config; if a file is machine-specific runtime state rather than
|
|
193
|
+
authored config, skip it and tell the user why.
|
|
194
|
+
|
|
195
|
+
**Write a README.md:** also write \`~/.pi/harnesses/<name>/README.md\` — a
|
|
196
|
+
human-readable description of the distro (a few paragraphs: what it sets up, which
|
|
197
|
+
packages it installs and why, what workflow it targets, and any prerequisites). This is
|
|
198
|
+
the extended description that complements the one-liner \`description\` in the
|
|
199
|
+
frontmatter. The README is not consumed by the extension; it is for users browsing the
|
|
200
|
+
catalogue or reading the distro on disk. Do NOT copy it into the target project on
|
|
201
|
+
deploy — it lives only in the catalogue.
|
|
202
|
+
|
|
203
|
+
**5. Update provenance.** Update \`./.pi/harness.md\` in the project to reflect the saved
|
|
204
|
+
harness. The provenance file is a valid \`harness.md\` (the saved frontmatter + directives)
|
|
205
|
+
with this header at the top of the body:
|
|
206
|
+
\`\`\`
|
|
207
|
+
<!-- pi-distro provenance
|
|
208
|
+
appliedHarness: <name>
|
|
209
|
+
appliedVersion: <version>
|
|
210
|
+
sourceCatalogue: user
|
|
211
|
+
lastUpdated: <ISO8601 now>
|
|
212
|
+
-->
|
|
213
|
+
\`\`\`
|
|
214
|
+
|
|
215
|
+
**6. Report.** Tell the user: "Saved distro '<name>'. Run \`/pi-distro deploy\` elsewhere to
|
|
216
|
+
deploy it."`);
|
|
217
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/pi-distro show` — dry-run preview of a distro (local or GitHub).
|
|
3
|
+
* Also exports buildShowPreview(), reused by deploy/pick/update.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
import { readFullHarnessMd, listBundledFiles, readCatalogue, findHarness, parsePackageList } from "./catalogue.ts";
|
|
11
|
+
import type { HarnessEntry } from "./catalogue.ts";
|
|
12
|
+
import { parseFrontmatter, extractBody } from "./frontmatter.ts";
|
|
13
|
+
import { looksLikeGithubRef, parseGithubRef, fetchGithubDistro } from "./github.ts";
|
|
14
|
+
import { display } from "./util.ts";
|
|
15
|
+
|
|
16
|
+
/** Build the dry-run preview for a harness (used by local and GitHub show). */
|
|
17
|
+
export async function buildShowPreview(harness: HarnessEntry): Promise<string> {
|
|
18
|
+
const fullMd = await readFullHarnessMd(harness.harnessMdPath);
|
|
19
|
+
const fm = parseFrontmatter(fullMd);
|
|
20
|
+
const body = extractBody(fullMd);
|
|
21
|
+
const packages = parsePackageList(body);
|
|
22
|
+
|
|
23
|
+
let settingsPreview = "_(none)_";
|
|
24
|
+
if (harness.filesDir) {
|
|
25
|
+
const sp = join(harness.filesDir, "settings.json");
|
|
26
|
+
if (existsSync(sp)) settingsPreview = "```json\n" + readFileSync(sp, "utf-8") + "\n```";
|
|
27
|
+
}
|
|
28
|
+
const files = harness.filesDir ? await listBundledFiles(harness.filesDir) : [];
|
|
29
|
+
const fileList = files.length > 0
|
|
30
|
+
? files.map((f) => `- \`${f.source}\` → \`./${f.target}\``).join("\n")
|
|
31
|
+
: "_(none)_";
|
|
32
|
+
const tags = fm?.tags
|
|
33
|
+
? Array.isArray(fm.tags) ? fm.tags.join(", ") : String(fm.tags)
|
|
34
|
+
: "_(none)_";
|
|
35
|
+
|
|
36
|
+
return `## Distro preview: ${harness.name}
|
|
37
|
+
|
|
38
|
+
### Frontmatter
|
|
39
|
+
- **Name:** ${fm?.name ?? harness.name}
|
|
40
|
+
- **Title:** ${fm?.title ?? harness.title}
|
|
41
|
+
- **Description:** ${fm?.description ?? harness.description}
|
|
42
|
+
- **Version:** ${fm?.version ?? harness.version}
|
|
43
|
+
- **Tags:** ${tags}
|
|
44
|
+
- **Source:** ${harness.source}
|
|
45
|
+
|
|
46
|
+
### pi packages to install
|
|
47
|
+
${packages.length > 0 ? packages.map((p) => `- \`${p}\``).join("\n") : "_(none)_"}
|
|
48
|
+
|
|
49
|
+
### Settings (would be merged)
|
|
50
|
+
${settingsPreview}
|
|
51
|
+
|
|
52
|
+
### Bundled files (target paths)
|
|
53
|
+
${fileList}
|
|
54
|
+
|
|
55
|
+
### Full directives
|
|
56
|
+
${body}
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
Nothing is applied. Run \`/pi-distro deploy\` to apply.`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function handleShow(pi: ExtensionAPI, name?: string): Promise<void> {
|
|
63
|
+
if (!name) {
|
|
64
|
+
display(pi, "Usage: \`/pi-distro show <name|gh-repo>\`\n\nExamples:\n /pi-distro show minimal\n /pi-distro show owner/repo\n /pi-distro show owner/repo/subpath");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// GitHub ref?
|
|
69
|
+
if (looksLikeGithubRef(name)) {
|
|
70
|
+
const ref = parseGithubRef(name)!;
|
|
71
|
+
pi.sendMessage({ customType: "pi-distro", content: `Fetching \`${ref.displayRef}\` from GitHub…`, display: true });
|
|
72
|
+
let fetched;
|
|
73
|
+
try {
|
|
74
|
+
fetched = fetchGithubDistro(ref);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
display(pi, `**Error:** ${err instanceof Error ? err.message : String(err)}`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
display(pi, await buildShowPreview(fetched.entry));
|
|
81
|
+
} finally {
|
|
82
|
+
fetched.cleanup();
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Local catalogue
|
|
88
|
+
const catalogue = await readCatalogue();
|
|
89
|
+
const harness = findHarness(name, catalogue);
|
|
90
|
+
if (!harness) {
|
|
91
|
+
const available = catalogue.map((h) => h.name).join(", ") || "(none)";
|
|
92
|
+
display(pi, `Distro '${name}' not found. Available: ${available}`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
display(pi, await buildShowPreview(harness));
|
|
96
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/pi-distro undeploy` — remove an applied distro from the current project
|
|
3
|
+
* (the reverse of deploy).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ExtensionAPI,
|
|
8
|
+
ExtensionCommandContext,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
import { parseProvenance, parsePackageList } from "./catalogue.ts";
|
|
14
|
+
import { extractBody } from "./frontmatter.ts";
|
|
15
|
+
import { readProjectPackages, USER_INVOLVEMENT_RULE } from "./util.ts";
|
|
16
|
+
|
|
17
|
+
export async function handleUndeploy(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> {
|
|
18
|
+
const snapshot = ctx.getSystemPromptOptions();
|
|
19
|
+
const cwd = snapshot.cwd;
|
|
20
|
+
const provenancePath = join(cwd, ".pi", "harness.md");
|
|
21
|
+
if (!existsSync(provenancePath)) {
|
|
22
|
+
ctx.ui.notify("No distro applied in this project. (Nothing to undeploy.)", "warning");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const provContent = readFileSync(provenancePath, "utf-8");
|
|
26
|
+
const prov = parseProvenance(provContent);
|
|
27
|
+
if (!prov) {
|
|
28
|
+
ctx.ui.notify("Could not parse provenance in .pi/harness.md.", "error");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The provenance file contains the full directives body (after the provenance header).
|
|
33
|
+
// Reuse it to identify what the distro intended to install.
|
|
34
|
+
const directives = extractBody(provContent);
|
|
35
|
+
const packages = parsePackageList(directives);
|
|
36
|
+
|
|
37
|
+
// Read current project state to compare against the distro's directives.
|
|
38
|
+
const installedPackages = readProjectPackages(cwd);
|
|
39
|
+
|
|
40
|
+
// Which of the distro's packages are still installed?
|
|
41
|
+
const distroPackagesInstalled = packages.filter((p) => installedPackages.includes(p));
|
|
42
|
+
const distroPackagesMissing = packages.filter((p) => !installedPackages.includes(p));
|
|
43
|
+
|
|
44
|
+
// Check for the AGENTS.md delimited section.
|
|
45
|
+
const agentsMdPath = join(cwd, "AGENTS.md");
|
|
46
|
+
let agentsMdSection = "not present";
|
|
47
|
+
if (existsSync(agentsMdPath)) {
|
|
48
|
+
const content = readFileSync(agentsMdPath, "utf-8");
|
|
49
|
+
if (content.includes(`<!-- pi-distro: ${prov.appliedHarness} -->`)) {
|
|
50
|
+
agentsMdSection = "present";
|
|
51
|
+
} else {
|
|
52
|
+
agentsMdSection = "not found (already removed or never added)";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const activeTools = snapshot.selectedTools?.length
|
|
57
|
+
? snapshot.selectedTools.map((t) => `- \`${t}\``).join("\n")
|
|
58
|
+
: "_(none / default set)_";
|
|
59
|
+
|
|
60
|
+
const packageList = distroPackagesInstalled.length > 0
|
|
61
|
+
? distroPackagesInstalled.map((p) => `- \`${p}\` — installed (can be removed with \`pi remove -l\`)`).join("\n")
|
|
62
|
+
: "_(none of the distro's packages are currently installed)_";
|
|
63
|
+
const missingNote = distroPackagesMissing.length > 0
|
|
64
|
+
? `\n\n**Packages from this distro not currently installed (already removed):**\n${distroPackagesMissing.map((p) => `- \`${p}\``).join("\n")}`
|
|
65
|
+
: "";
|
|
66
|
+
|
|
67
|
+
pi.sendUserMessage(`## Undeploying distro: ${prov.appliedHarness} (v${prov.appliedVersion})
|
|
68
|
+
|
|
69
|
+
The user wants to **remove** the applied distro from this project. This is the reverse of
|
|
70
|
+
\`/pi-distro deploy\`. Walk them through removing each component, asking at every step —
|
|
71
|
+
never silently delete anything. Provenance records the distro's *intentions* (directives),
|
|
72
|
+
not the exact outcome of the interactive deploy, so you must compare the directives against
|
|
73
|
+
the *current* project state and let the user decide what to remove.
|
|
74
|
+
|
|
75
|
+
### Distro directives (for reference — what was intended to be applied)
|
|
76
|
+
${directives}
|
|
77
|
+
|
|
78
|
+
### Current project state
|
|
79
|
+
**Distro packages still installed in this project (${distroPackagesInstalled.length} of ${packages.length}):**
|
|
80
|
+
${packageList}${missingNote}
|
|
81
|
+
|
|
82
|
+
**AGENTS.md delimited section:** ${agentsMdSection}
|
|
83
|
+
|
|
84
|
+
**Active tools in this session:**
|
|
85
|
+
${activeTools}
|
|
86
|
+
|
|
87
|
+
**Installed packages in .pi/settings.json:**
|
|
88
|
+
${installedPackages.length > 0 ? installedPackages.map((p) => `- \`${p}\``).join("\n") : "_(none)_"}
|
|
89
|
+
|
|
90
|
+
### Removal procedure (follow exactly, collaborating with the user)
|
|
91
|
+
|
|
92
|
+
**0. User involvement rule** — ${USER_INVOLVEMENT_RULE} This is especially critical here:
|
|
93
|
+
removal is destructive. Never silently delete a file, remove a package, or strip a settings
|
|
94
|
+
key. The user may have customized files after deploy, or may want to keep some components.
|
|
95
|
+
|
|
96
|
+
**1. Walk the user through each removal category, one at a time:**
|
|
97
|
+
|
|
98
|
+
**(a) Packages** — for each distro package still installed, offer to remove it with
|
|
99
|
+
\`pi remove -l <pkg>\`. **Ask per package** — the user may want to keep some. Warn if
|
|
100
|
+
removing a package that other components depend on (e.g. if an extension relies on a
|
|
101
|
+
package's theme). If the user skips a package, note it.
|
|
102
|
+
|
|
103
|
+
**(b) Bundled files** — the directives list bundled files (e.g. \`AGENTS.md\`,
|
|
104
|
+
\`settings.json\`, \`extensions/claude-statusline.ts\`). For each, check if the file exists
|
|
105
|
+
in the project. If it does, **show the user the file (or a summary) before removing** —
|
|
106
|
+
they may have customized it and want to keep it. Offer: remove / keep. Never silently
|
|
107
|
+
delete. For \`settings.json\`, offer to remove specific keys the distro merged (e.g.
|
|
108
|
+
\`theme\`, \`defaultThinkingLevel\`) rather than deleting the whole file — and warn about
|
|
109
|
+
user customizations.
|
|
110
|
+
|
|
111
|
+
**(c) AGENTS.md delimited section** — if the \`<!-- pi-distro: ${prov.appliedHarness} -->\` ...
|
|
112
|
+
\`<!-- /pi-distro: ${prov.appliedHarness} -->\` block exists, offer to remove it. If the
|
|
113
|
+
user has a standalone (non-delimited) AGENTS.md that wasn't added by the distro, leave it.
|
|
114
|
+
|
|
115
|
+
**(d) Extensions / skills / prompts / themes** — if the directives describe these and they
|
|
116
|
+
exist in the project, offer to remove each (with the same show-before-delete rule).
|
|
117
|
+
|
|
118
|
+
**2. Remove provenance last.** Only after the user has confirmed the component removals,
|
|
119
|
+
remove \`./.pi/harness.md\` (the provenance file). Confirm with the user before deleting it.
|
|
120
|
+
|
|
121
|
+
**3. Report and recommend a restart.** Summarise what was removed and what was kept (the user
|
|
122
|
+
may have chosen to keep some components). Tell the user to **restart pi** — removed packages
|
|
123
|
+
and extensions won't fully unload until the next session start.`);
|
|
124
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/pi-distro update` — upgrade the applied distro if a newer version exists.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
ExtensionAPI,
|
|
7
|
+
ExtensionCommandContext,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
import { parseProvenance } from "./catalogue.ts";
|
|
13
|
+
import type { HarnessEntry } from "./catalogue.ts";
|
|
14
|
+
import { parseGithubRef, fetchGithubDistro } from "./github.ts";
|
|
15
|
+
import { resolveCatalogueEntry } from "./resolve.ts";
|
|
16
|
+
import { buildShowPreview } from "./show.ts";
|
|
17
|
+
import { sendDeployKickoff } from "./deploy.ts";
|
|
18
|
+
import { display, compareVersions } from "./util.ts";
|
|
19
|
+
|
|
20
|
+
export async function handleUpdate(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> {
|
|
21
|
+
// 1. Read provenance to find the applied distro.
|
|
22
|
+
const snapshot = ctx.getSystemPromptOptions();
|
|
23
|
+
const cwd = snapshot.cwd;
|
|
24
|
+
const provenancePath = join(cwd, ".pi", "harness.md");
|
|
25
|
+
if (!existsSync(provenancePath)) {
|
|
26
|
+
ctx.ui.notify("No distro applied in this project. Run /pi-distro deploy <name> first.", "warning");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const prov = parseProvenance(readFileSync(provenancePath, "utf-8"));
|
|
30
|
+
if (!prov) {
|
|
31
|
+
ctx.ui.notify("Could not parse provenance in .pi/harness.md.", "error");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. Resolve the current version from the source.
|
|
36
|
+
const source = prov.sourceCatalogue;
|
|
37
|
+
let current: HarnessEntry | undefined;
|
|
38
|
+
let fetchedCleanup: (() => void) | undefined;
|
|
39
|
+
|
|
40
|
+
if (source.startsWith("github:")) {
|
|
41
|
+
// GitHub source: re-fetch to get the latest.
|
|
42
|
+
const refStr = source.slice("github:".length);
|
|
43
|
+
const ref = parseGithubRef(refStr);
|
|
44
|
+
if (!ref) {
|
|
45
|
+
ctx.ui.notify(`Could not parse GitHub source '${refStr}' from provenance.`, "error");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
ctx.ui.notify(`Fetching ${ref.displayRef} from GitHub to check for updates…`, "info");
|
|
49
|
+
try {
|
|
50
|
+
const fetched = fetchGithubDistro(ref);
|
|
51
|
+
current = fetched.entry;
|
|
52
|
+
fetchedCleanup = fetched.cleanup;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
ctx.ui.notify(err instanceof Error ? err.message : String(err), "error");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
// Local catalogue (seed or user).
|
|
59
|
+
current = await resolveCatalogueEntry(prov.appliedHarness);
|
|
60
|
+
if (!current) {
|
|
61
|
+
ctx.ui.notify(
|
|
62
|
+
`Distro '${prov.appliedHarness}' is no longer in the catalogue (it may have been removed or renamed). Applied version was v${prov.appliedVersion}.`,
|
|
63
|
+
"warning",
|
|
64
|
+
);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 3. Compare versions.
|
|
70
|
+
const cmp = compareVersions(current.version, prov.appliedVersion);
|
|
71
|
+
if (cmp === 0) {
|
|
72
|
+
if (fetchedCleanup) fetchedCleanup();
|
|
73
|
+
ctx.ui.notify(
|
|
74
|
+
`Already up to date: ${prov.appliedHarness} v${prov.appliedVersion}. To force a re-deploy, run /pi-distro deploy ${prov.appliedHarness}.`,
|
|
75
|
+
"info",
|
|
76
|
+
);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const direction = cmp > 0 ? "upgrade" : "downgrade";
|
|
81
|
+
if (direction === "downgrade") {
|
|
82
|
+
if (fetchedCleanup) fetchedCleanup();
|
|
83
|
+
ctx.ui.notify(
|
|
84
|
+
`Downgrade warning: ${prov.appliedHarness} is at v${prov.appliedVersion} in this project, but the catalogue has v${current.version} (older). Updates are for moving to a newer version. To downgrade, run /pi-distro deploy ${prov.appliedHarness} and confirm.`,
|
|
85
|
+
"warning",
|
|
86
|
+
);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 4. Upgrade available — surface it and ask the user before proceeding.
|
|
91
|
+
const preview = await buildShowPreview(current);
|
|
92
|
+
display(pi, `## Update available
|
|
93
|
+
|
|
94
|
+
${preview}`);
|
|
95
|
+
|
|
96
|
+
const confirmed = await ctx.ui.confirm(
|
|
97
|
+
"Apply the update?",
|
|
98
|
+
`An update is available for ${prov.appliedHarness}: v${prov.appliedVersion} → v${current!.version}. This will re-deploy the distro (merge-don't-clobber; your existing config and customisations are preserved). Proceed?`,
|
|
99
|
+
);
|
|
100
|
+
if (!confirmed) {
|
|
101
|
+
if (fetchedCleanup) fetchedCleanup();
|
|
102
|
+
ctx.ui.notify("Update cancelled.", "info");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 5. Run the standard deploy (carries the version note, merge rule, user-involvement rule, etc.).
|
|
107
|
+
await sendDeployKickoff(pi, ctx, current!);
|
|
108
|
+
// GitHub temp dir left in /tmp for the agent to read bundled files (ephemeral).
|
|
109
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers and rule strings for the pi-distro command handlers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
/** Display markdown content in the TUI as a custom message (no LLM turn). */
|
|
10
|
+
export function display(pi: ExtensionAPI, content: string): void {
|
|
11
|
+
pi.sendMessage({ customType: "pi-distro", content, display: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Compare two semver-ish version strings. Returns >0 if a>b, <0 if a<b, 0 if equal. */
|
|
15
|
+
export function compareVersions(a: string, b: string): number {
|
|
16
|
+
const pa = (a || "0").split(".").map((n) => parseInt(n, 10) || 0);
|
|
17
|
+
const pb = (b || "0").split(".").map((n) => parseInt(n, 10) || 0);
|
|
18
|
+
const len = Math.max(pa.length, pb.length);
|
|
19
|
+
for (let i = 0; i < len; i++) {
|
|
20
|
+
const va = pa[i] ?? 0;
|
|
21
|
+
const vb = pb[i] ?? 0;
|
|
22
|
+
if (va !== vb) return va - vb;
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Read the packages array from <cwd>/.pi/settings.json, normalised to source strings. */
|
|
28
|
+
export function readProjectPackages(cwd: string): string[] {
|
|
29
|
+
const settingsPath = join(cwd, ".pi", "settings.json");
|
|
30
|
+
if (!existsSync(settingsPath)) return [];
|
|
31
|
+
try {
|
|
32
|
+
const s = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
33
|
+
if (!Array.isArray(s.packages)) return [];
|
|
34
|
+
return s.packages.map((p: string | { source: string }) => (typeof p === "string" ? p : p.source));
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const MERGE_RULE = `**Merge, don't clobber.** For any target file that already exists, do not overwrite silently — show a diff and ask the user whether to overwrite, keep theirs, or merge. Merge JSON settings field-by-field. Append AGENTS.md content under a delimited section. Install pi packages with \`pi install -l\` (project-local) only after confirming with the user; do NOT pre-add packages to ./.pi/settings.json by hand — \`pi install -l\` registers them on success (and leaves settings untouched on failure).`;
|
|
41
|
+
|
|
42
|
+
export const USER_INVOLVEMENT_RULE = `**The user is in the loop for every decision.** pi-distro is a collaborative, agent-driven tool — never make a state-changing decision for the user. Before overwriting a file, installing a package, skipping a step, replacing a tool, applying an upgrade, or resolving any conflict (merge, redundancy, version downgrade, etc.), the agent must (1) surface the decision clearly (what it's about to do and why), (2) present the available options, and (3) wait for the user's explicit choice. Never silently skip, silently overwrite, silently substitute, or proceed on assumption. If a decision is ambiguous or the user is unsure, explain the tradeoffs and let them choose. The agent proposes; the user disposes.`;
|
|
43
|
+
|
|
44
|
+
export const PACKAGE_CONFLICT_RULE = `**Evaluate tool redundancy/conflicts before installing.** When you install a distro's packages, some may provide tools that overlap with already-active tools — either an **exact name collision** (two tools with the same name) or **semantic redundancy** (different names, but doing very similar things, e.g. two web-search tools, two browser tools, two todo tools). pi handles exact name collisions by load order (project-local tools shadow global ones; the conflict is a non-fatal diagnostic, not a fatal error — pi still starts), but redundancy leaves the user with duplicate capability and a confusing tool set. Before installing each package, the **agent must evaluate** whether its purpose overlaps an already-active tool (read the already-active tools and project packages above, and run \`pi list\` to see globally-installed packages). If redundancy or a conflict is detected, do NOT install blindly — present the user a choice: (a) **skip** installing it (the capability already exists); (b) **replace** — \`pi remove -l <old>\` the overlapping package then \`pi install -l <new>\`; (c) **keep both** — install it anyway (e.g. the user prefers the new tool, or they serve subtly different purposes); (d) **cancel**. Only proceed with the user's choice.`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# minimal
|
|
2
|
+
|
|
3
|
+
A clean starting point for any project. Drops in a basic `AGENTS.md` and a sensible
|
|
4
|
+
`.pi/settings.json` — nothing more, nothing less.
|
|
5
|
+
|
|
6
|
+
## What it sets up
|
|
7
|
+
|
|
8
|
+
- **`AGENTS.md`** — a minimal explore-before-acting methodology: investigate before
|
|
9
|
+
implementing, make surgical changes, keep solutions simple.
|
|
10
|
+
- **`.pi/settings.json`** — a sensible default thinking level (pi's default
|
|
11
|
+
one-at-a-time steering already applies).
|
|
12
|
+
|
|
13
|
+
No pi packages are installed. Use this as a blank canvas — tailor the `AGENTS.md` to your
|
|
14
|
+
project's build/test commands and conventions, then layer on packages and skills as you go.
|
|
15
|
+
|
|
16
|
+
## When to use
|
|
17
|
+
|
|
18
|
+
- You want a clean baseline without opinions about which packages to install.
|
|
19
|
+
- You're starting a new project and just want the agent oriented with good defaults.
|
|
20
|
+
- You want to build your own distro from a minimal starting point (deploy this, then
|
|
21
|
+
`/pi-distro save` after customizing).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Project Instructions
|
|
2
|
+
|
|
3
|
+
Brief guidance for working in this project. Add your own rules below.
|
|
4
|
+
|
|
5
|
+
## Build & Test Commands
|
|
6
|
+
|
|
7
|
+
<!-- e.g. npm run build, npm test, npm run lint -->
|
|
8
|
+
- Build:
|
|
9
|
+
- Test:
|
|
10
|
+
- Lint:
|
|
11
|
+
|
|
12
|
+
## Code Style
|
|
13
|
+
|
|
14
|
+
<!-- e.g. use TypeScript, 2-space indent, prefer named exports -->
|
|
15
|
+
-
|
|
16
|
+
|
|
17
|
+
## Project Structure Notes
|
|
18
|
+
|
|
19
|
+
<!-- e.g. src/ for source, tests/ for tests, docs/ for documentation -->
|
|
20
|
+
-
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: minimal
|
|
3
|
+
title: Minimal
|
|
4
|
+
description: Clean starting point: a basic AGENTS.md and .pi/settings.json.
|
|
5
|
+
version: 0.1.0
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Minimal
|
|
9
|
+
|
|
10
|
+
## Bundled files
|
|
11
|
+
The following bundled files are provided under `files/` and should be placed into the
|
|
12
|
+
target project. For any path that already exists, do NOT overwrite — show the user a diff
|
|
13
|
+
and ask whether to overwrite, keep theirs, or merge. Merge JSON settings objects field by
|
|
14
|
+
field. Append (not replace) `AGENTS.md` content under a clearly-delimited section.
|
|
15
|
+
|
|
16
|
+
- `files/AGENTS.md` → `./AGENTS.md`
|
|
17
|
+
- `files/settings.json` → `./.pi/settings.json` (merge with existing settings)
|
|
18
|
+
|
|
19
|
+
## Context
|
|
20
|
+
This harness provides a minimal starting point for any project. After placing the bundled
|
|
21
|
+
files, ensure the `AGENTS.md` is tailored to the project's actual build/test commands and
|
|
22
|
+
conventions. The `settings.json` sets a sensible default thinking level — adjust as needed.
|
|
23
|
+
|
|
24
|
+
No additional pi packages are required for this harness.
|