pi-lazy-setup 0.0.0-stage → 3.0.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/LICENSE +21 -0
- package/README.md +59 -2
- package/bin/pi-setup.mjs +40 -0
- package/configs/pi/AGENTS.md +7 -0
- package/configs/pi/extensions/pi-setup.js +228 -0
- package/configs/pi/mcp.json +5 -0
- package/configs/pi/models.json +25 -0
- package/configs/pi/prompts/review.md +14 -0
- package/configs/pi/prompts/wr.md +11 -0
- package/configs/pi/settings.json +18 -0
- package/configs/pi/web-config.json +23 -0
- package/configs/reference/best-practices.md +220 -0
- package/configs/reference/git-guidelines.md +63 -0
- package/package.json +35 -5
- package/src/agents/pi.mjs +24 -0
- package/src/agents/types.mjs +23 -0
- package/src/commands/init.mjs +91 -0
- package/src/commands/select.mjs +51 -0
- package/src/lib/fs.mjs +111 -0
- package/src/lib/log.mjs +39 -0
- package/src/lib/paths.mjs +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dung Huynh Duc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pi-setup
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Initialize the **Pi** agent environment from a curated snapshot.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx pi-lazy-setup
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
1. **Deploys configs** — overwrites `~/.pi/agent` from bundled `configs/pi/` (binary install is out of scope: `pi` must already be on PATH).
|
|
12
|
+
2. **Deploys the `/pi-setup` extension** — copies `configs/pi/extensions/pi-setup.js` into `~/.pi/agent/extensions/` so Pi auto-discovers it on the next start.
|
|
13
|
+
3. **Deploys reference docs** — copies into `~/.pi/agent/pi-setup/`.
|
|
14
|
+
|
|
15
|
+
Then, inside Pi, run `/pi-setup`: it interactively installs **packages** (`pi install <source>`), **skills** (`npx skills add`, https://skills.sh/), and **CLI tools** (officecli: skill file from <https://officecli.ai/SKILL.md> + binary via `curl -fsSL https://d.officecli.ai/install.sh | bash`). Every item is confirmed individually, and already-installed items are skipped. Pattern follows the `/rpiv-setup` command of `@juicesharp/rpiv-pi`.
|
|
16
|
+
|
|
17
|
+
The candidate lists (packages, skills, CLI tools) live as snapshot data inside the extension file itself.
|
|
18
|
+
|
|
19
|
+
## Flags
|
|
20
|
+
|
|
21
|
+
| Flag | Description |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `[agents...]` | `pi`. Omit to run directly. |
|
|
24
|
+
| `--dry-run` | Preview planned actions, touch nothing. |
|
|
25
|
+
| `--backup` | Tar the existing Pi home before overwrite. |
|
|
26
|
+
|
|
27
|
+
## Snapshot Philosophy
|
|
28
|
+
|
|
29
|
+
This is a **snapshot**, not a generic installer. Configs ship exactly as the author uses them — personal proxies, extension lists, etc. No templating.
|
|
30
|
+
|
|
31
|
+
## Scope
|
|
32
|
+
|
|
33
|
+
**Pi** (<https://github.com/mariozechner/pi-coding-agent>). The CLI installs nothing: it only deploys the declaration file (`mcp.json`) and config files (`settings.json`, `models.json`, `AGENTS.md`, `prompts/`, `extensions/pi-setup.js`). MCP server binaries are not installed either. Everything interactive (packages, skills, CLI tools) happens via `/pi-setup` inside Pi.
|
|
34
|
+
|
|
35
|
+
For skill management, see [@kkmila/isc](https://github.com/machuqiji/isc).
|
|
36
|
+
|
|
37
|
+
## Development
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install # required for local development
|
|
41
|
+
node bin/pi-setup.mjs --dry-run # preview
|
|
42
|
+
node bin/pi-setup.mjs # real run
|
|
43
|
+
npm test # process-seam tests
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Releasing
|
|
47
|
+
|
|
48
|
+
Create a GitHub Release to trigger npm publish:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm version <major|minor|patch>
|
|
52
|
+
git push
|
|
53
|
+
gh release create v1.2.0 --generate-notes
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The workflow runs tests (`continue-on-error: true`) then `npm publish`. Requires `NPM_TOKEN` secret.
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
package/bin/pi-setup.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// pi-setup — initialize the Pi agent environment from a snapshot.
|
|
4
|
+
//
|
|
5
|
+
// Deploy-only: no binaries, packages, or skills are installed here. Instead
|
|
6
|
+
// the /pi-setup extension lands in ~/.pi/agent/extensions/ — run `/pi-setup`
|
|
7
|
+
// inside Pi to install packages, skills, and CLI tools interactively
|
|
8
|
+
// (each item confirmed individually).
|
|
9
|
+
//
|
|
10
|
+
// Usage:
|
|
11
|
+
// pi-setup run directly (all = pi)
|
|
12
|
+
// pi-setup pi explicit
|
|
13
|
+
// Flags: --dry-run, --backup, --help, --version.
|
|
14
|
+
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
import { program } from "commander";
|
|
17
|
+
import { AGENTS, initialize } from "../src/commands/init.mjs";
|
|
18
|
+
import { selectAgents } from "../src/commands/select.mjs";
|
|
19
|
+
|
|
20
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.name("pi-setup")
|
|
24
|
+
.description("Initialize the Pi agent environment from a curated snapshot")
|
|
25
|
+
.version(version)
|
|
26
|
+
.argument("[agents...]", "agent id to configure (pi); omit to run directly")
|
|
27
|
+
.option("--dry-run", "print planned actions, execute nothing")
|
|
28
|
+
.option("--backup", "back up existing agent home before overwriting")
|
|
29
|
+
.action(async (positional, opts) => {
|
|
30
|
+
const ids = await selectAgents(positional, { tty: process.stdin.isTTY });
|
|
31
|
+
const chosen = AGENTS.filter((a) => ids.includes(a.id));
|
|
32
|
+
const code = await initialize({
|
|
33
|
+
dryRun: !!opts.dryRun,
|
|
34
|
+
backup: !!opts.backup,
|
|
35
|
+
agents: chosen,
|
|
36
|
+
});
|
|
37
|
+
process.exit(code);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
program.parse();
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// /pi-setup — interactive installer for Pi packages, skills, and CLI tools.
|
|
2
|
+
//
|
|
3
|
+
// Deployed by the pi-setup CLI (npx pi-setup) into ~/.pi/agent/extensions/pi-setup.js.
|
|
4
|
+
// Run `/pi-setup` inside Pi: every candidate is confirmed individually, and
|
|
5
|
+
// already-installed items are skipped automatically. Pattern follows the
|
|
6
|
+
// /rpiv-setup command of @juicesharp/rpiv-pi (serial installs, per-item
|
|
7
|
+
// confirm, succeeded/failed report, restart hint).
|
|
8
|
+
//
|
|
9
|
+
// Candidates (snapshot data, kept in sync with the owner's setup):
|
|
10
|
+
// packages → `pi install <source>` (writes into ~/.pi/agent/settings.json)
|
|
11
|
+
// skills → `npx --yes skills add <repo> --skill <name> -g --agent pi -y`
|
|
12
|
+
// clis → officecli: skill file from https://officecli.ai/SKILL.md plus
|
|
13
|
+
// binary via `curl -fsSL https://d.officecli.ai/install.sh | bash`
|
|
14
|
+
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
19
|
+
// Resolved lazily (not at module load) so tests can redirect HOME.
|
|
20
|
+
function piSettingsPath() {
|
|
21
|
+
return join(homedir(), ".pi", "agent", "settings.json");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function globalSkillsDir() {
|
|
25
|
+
return join(homedir(), ".agents", "skills");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const INSTALL_TIMEOUT_MS = 300_000;
|
|
29
|
+
const BIN_CHECK_TIMEOUT_MS = 15_000;
|
|
30
|
+
const STDERR_SNIPPET_CHARS = 300;
|
|
31
|
+
|
|
32
|
+
/** Pi extension packages offered via `pi install <source>`. */
|
|
33
|
+
const PACKAGES = [
|
|
34
|
+
{ source: "npm:pi-footer", description: "Footer status line (token usage, model, cost)." },
|
|
35
|
+
{ source: "npm:pi-tps-meter", description: "Tokens-per-second meter for streaming responses." },
|
|
36
|
+
{ source: "npm:@juicesharp/rpiv-pi", description: "RPIV pipeline skills (code review, commit, handoff, diagrams)." },
|
|
37
|
+
{ source: "https://github.com/cathrynlavery/diagram-design", description: "Branded diagram/chart generation skills." },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/** Skills offered via `npx skills add` (https://skills.sh/). */
|
|
41
|
+
const SKILLS = [
|
|
42
|
+
{
|
|
43
|
+
repo: "mattpocock/skills",
|
|
44
|
+
name: "grill-me",
|
|
45
|
+
description: "Matt Pocock — a relentless interview to sharpen a plan or design.",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
repo: "microsoft/playwright-cli",
|
|
49
|
+
name: "playwright-cli",
|
|
50
|
+
description: "Microsoft — browser automation and Playwright test driving.",
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/** CLI tools. Each entry installs two parts: a skill file and the binary itself. */
|
|
55
|
+
const CLIS = [
|
|
56
|
+
{
|
|
57
|
+
id: "officecli",
|
|
58
|
+
bin: "officecli",
|
|
59
|
+
description: "AI-friendly CLI for Office documents (.docx/.xlsx/.pptx).",
|
|
60
|
+
skillFile: { name: "officecli", url: "https://officecli.ai/SKILL.md" },
|
|
61
|
+
installCommand: "curl -fsSL https://d.officecli.ai/install.sh | bash",
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
export default function piSetupExtension(pi) {
|
|
66
|
+
pi.registerCommand("pi-setup", {
|
|
67
|
+
description: "Install packages, skills, and CLI tools (each confirmed individually)",
|
|
68
|
+
handler: async (_args, ctx) => {
|
|
69
|
+
if (!ctx.hasUI) {
|
|
70
|
+
ctx.ui.notify("/pi-setup requires interactive mode", "error");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const report = { installed: [], skipped: [], failed: [] };
|
|
74
|
+
await installPackages(pi, ctx, report);
|
|
75
|
+
await installSkills(pi, ctx, report);
|
|
76
|
+
await installClis(pi, ctx, report);
|
|
77
|
+
ctx.ui.notify(buildReport(report), report.failed.length > 0 ? "warning" : "info");
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Packages section: offer each undeclared package via confirm, then `pi install`. */
|
|
83
|
+
async function installPackages(pi, ctx, report) {
|
|
84
|
+
const declared = declaredPackageSources();
|
|
85
|
+
for (const p of PACKAGES) {
|
|
86
|
+
if (declared.has(p.source)) {
|
|
87
|
+
report.skipped.push(p.source);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const confirmed = await ctx.ui.confirm("Install Pi package?", `${p.source}\n${p.description}`);
|
|
91
|
+
if (!confirmed) continue;
|
|
92
|
+
ctx.ui.notify(`Installing ${p.source}…`, "info");
|
|
93
|
+
const result = await runExec(pi, "pi", ["install", p.source]);
|
|
94
|
+
settle(result, p.source, report);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Skills section: offer each missing skill via confirm, then `npx skills add`. */
|
|
99
|
+
async function installSkills(pi, ctx, report) {
|
|
100
|
+
for (const s of SKILLS) {
|
|
101
|
+
if (skillInstalled(s.name)) {
|
|
102
|
+
report.skipped.push(`skill:${s.name}`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const confirmed = await ctx.ui.confirm("Install skill?", `${s.repo}@${s.name}\n${s.description}`);
|
|
106
|
+
if (!confirmed) continue;
|
|
107
|
+
ctx.ui.notify(`Installing skill ${s.name}…`, "info");
|
|
108
|
+
const result = await runExec(pi, "npx", [
|
|
109
|
+
"--yes",
|
|
110
|
+
"skills",
|
|
111
|
+
"add",
|
|
112
|
+
s.repo,
|
|
113
|
+
"--skill",
|
|
114
|
+
s.name,
|
|
115
|
+
"-g",
|
|
116
|
+
"--agent",
|
|
117
|
+
"pi",
|
|
118
|
+
"-y",
|
|
119
|
+
]);
|
|
120
|
+
settle(result, `skill:${s.name}`, report);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* CLI tools section. Each entry installs two parts — the skill file and the
|
|
126
|
+
* binary — and each part is confirmed individually.
|
|
127
|
+
*/
|
|
128
|
+
async function installClis(pi, ctx, report) {
|
|
129
|
+
for (const c of CLIS) {
|
|
130
|
+
if (c.skillFile) {
|
|
131
|
+
const { name, url } = c.skillFile;
|
|
132
|
+
if (skillInstalled(name)) {
|
|
133
|
+
report.skipped.push(`skill:${name}`);
|
|
134
|
+
} else {
|
|
135
|
+
const confirmed = await ctx.ui.confirm(
|
|
136
|
+
"Install skill file?",
|
|
137
|
+
`${url} → ~/.agents/skills/${name}/SKILL.md\n${c.description}`,
|
|
138
|
+
);
|
|
139
|
+
if (confirmed) {
|
|
140
|
+
ctx.ui.notify(`Downloading ${url}…`, "info");
|
|
141
|
+
try {
|
|
142
|
+
await downloadSkillFile(name, url);
|
|
143
|
+
report.installed.push(`skill:${name}`);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
report.failed.push({ item: `skill:${name}`, error: toMessage(err) });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (await binaryAvailable(pi, c.bin)) {
|
|
151
|
+
report.skipped.push(`cli:${c.bin}`);
|
|
152
|
+
} else {
|
|
153
|
+
const confirmed = await ctx.ui.confirm("Install CLI tool?", `${c.id} — ${c.description}\n${c.installCommand}`);
|
|
154
|
+
if (confirmed) {
|
|
155
|
+
ctx.ui.notify(`Installing ${c.id}…`, "info");
|
|
156
|
+
const result = await runExec(pi, "bash", ["-c", c.installCommand]);
|
|
157
|
+
settle(result, `cli:${c.bin}`, report);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Run a command via pi.exec and normalize into { ok, error }. */
|
|
164
|
+
async function runExec(pi, command, args, timeoutMs = INSTALL_TIMEOUT_MS) {
|
|
165
|
+
try {
|
|
166
|
+
const result = await pi.exec(command, args, { timeout: timeoutMs });
|
|
167
|
+
if (result.code === 0) return { ok: true };
|
|
168
|
+
const error = (result.stderr || result.stdout || `exit ${result.code}`).trim().slice(0, STDERR_SNIPPET_CHARS);
|
|
169
|
+
return { ok: false, error };
|
|
170
|
+
} catch (err) {
|
|
171
|
+
return { ok: false, error: toMessage(err) };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function settle(result, item, report) {
|
|
176
|
+
if (result.ok) report.installed.push(item);
|
|
177
|
+
else report.failed.push({ item, error: result.error });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function declaredPackageSources() {
|
|
181
|
+
try {
|
|
182
|
+
const settings = JSON.parse(readFileSync(piSettingsPath(), "utf8"));
|
|
183
|
+
return new Set((settings.packages ?? []).map((p) => (typeof p === "string" ? p : p.source)));
|
|
184
|
+
} catch {
|
|
185
|
+
return new Set();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Whether a skill directory with SKILL.md exists in ~/.agents/skills/. */
|
|
190
|
+
function skillInstalled(name) {
|
|
191
|
+
return existsSync(join(globalSkillsDir(), name, "SKILL.md"));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Whether a binary responds to --version. */
|
|
195
|
+
async function binaryAvailable(pi, name) {
|
|
196
|
+
const result = await runExec(pi, name, ["--version"], BIN_CHECK_TIMEOUT_MS);
|
|
197
|
+
return result.ok;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Download a SKILL.md into ~/.agents/skills/<name>/SKILL.md. */
|
|
201
|
+
async function downloadSkillFile(name, url) {
|
|
202
|
+
const res = await fetch(url);
|
|
203
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
|
|
204
|
+
const text = await res.text();
|
|
205
|
+
const dir = join(globalSkillsDir(), name);
|
|
206
|
+
mkdirSync(dir, { recursive: true });
|
|
207
|
+
writeFileSync(join(dir, "SKILL.md"), text);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function toMessage(err) {
|
|
211
|
+
return String(err?.message ?? err);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function buildReport(report) {
|
|
215
|
+
const lines = [];
|
|
216
|
+
if (report.installed.length > 0) lines.push(`✓ Installed: ${report.installed.join(", ")}`);
|
|
217
|
+
if (report.skipped.length > 0) lines.push(`• Already installed (skipped): ${report.skipped.join(", ")}`);
|
|
218
|
+
if (report.failed.length > 0) {
|
|
219
|
+
lines.push("✗ Failed:");
|
|
220
|
+
for (const f of report.failed) lines.push(` ${f.item}: ${f.error}`);
|
|
221
|
+
}
|
|
222
|
+
if (report.installed.length > 0) {
|
|
223
|
+
lines.push("");
|
|
224
|
+
lines.push("Restart Pi (or run /reload) to load the newly installed packages and skills.");
|
|
225
|
+
}
|
|
226
|
+
if (lines.length === 0) lines.push("Nothing to install.");
|
|
227
|
+
return lines.join("\n");
|
|
228
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://pi.dev/schemas/models.schema.json",
|
|
3
|
+
"providers": {
|
|
4
|
+
"magpie": {
|
|
5
|
+
"baseUrl": "http://192.168.1.235:3425/v1",
|
|
6
|
+
"api": "openai-completions",
|
|
7
|
+
"apiKey": "magpie-key",
|
|
8
|
+
"models": [
|
|
9
|
+
{
|
|
10
|
+
"id": "group/vision-text",
|
|
11
|
+
"name": "vision-text",
|
|
12
|
+
"reasoning": true,
|
|
13
|
+
"input": ["text", "image"],
|
|
14
|
+
"contextWindow": 1048576,
|
|
15
|
+
"maxTokens": 256000,
|
|
16
|
+
"thinkingLevelMap": {
|
|
17
|
+
"low": "low",
|
|
18
|
+
"high": "high",
|
|
19
|
+
"max": "max"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Review staged git changes
|
|
3
|
+
argument-hint: "[path or file]"
|
|
4
|
+
---
|
|
5
|
+
Review the staged changes (`git diff --cached${1:+ -- "$1"}`). Focus on:
|
|
6
|
+
|
|
7
|
+
- Bugs and logic errors
|
|
8
|
+
- Security issues
|
|
9
|
+
- Error handling gaps
|
|
10
|
+
- Naming, readability, and dead code
|
|
11
|
+
- Tests covering the changed behavior
|
|
12
|
+
|
|
13
|
+
Report findings as a concise checklist grouped by file. If nothing is staged,
|
|
14
|
+
say so and stop — do not invent changes.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Finish the current task end-to-end
|
|
3
|
+
argument-hint: "[instructions]"
|
|
4
|
+
---
|
|
5
|
+
Finish the current task end-to-end. ${1:-No additional instructions.}
|
|
6
|
+
|
|
7
|
+
Steps:
|
|
8
|
+
1. Re-read the goal and any open TODO items.
|
|
9
|
+
2. Implement the remaining work, committing in logical chunks.
|
|
10
|
+
3. Run the test suite (`npm test`) and linter (`npx @biomejs/biome check src bin tests`).
|
|
11
|
+
4. If something is genuinely blocked, stop and explain — do not fake completion.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"collapseChangelog": true,
|
|
3
|
+
"defaultModel": "group/vision-text",
|
|
4
|
+
"defaultProvider": "magpie",
|
|
5
|
+
"defaultThinkingLevel": "high",
|
|
6
|
+
"editorPaddingX": 1,
|
|
7
|
+
"hideThinkingBlock": true,
|
|
8
|
+
"lastChangelogVersion": "1.5.0",
|
|
9
|
+
"permissionLevel": "high",
|
|
10
|
+
"quietStartup": true,
|
|
11
|
+
"theme": "light",
|
|
12
|
+
"treeFilterMode": "default",
|
|
13
|
+
"terminal": {
|
|
14
|
+
"showTerminalProgress": true
|
|
15
|
+
},
|
|
16
|
+
"enableInstallTelemetry": false,
|
|
17
|
+
"defaultProjectTrust": "always"
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"provider": "exa",
|
|
3
|
+
"apiKeys": {
|
|
4
|
+
"exa": "ac8d8a9a-0bdf-4cba-818b-4e67cc6891d2",
|
|
5
|
+
"tavily": "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"
|
|
6
|
+
},
|
|
7
|
+
"interceptors": {
|
|
8
|
+
"github": true
|
|
9
|
+
},
|
|
10
|
+
"guidance": {
|
|
11
|
+
"web_search": {
|
|
12
|
+
"promptSnippet": "Search the web for current docs and library versions",
|
|
13
|
+
"promptGuidelines": [
|
|
14
|
+
"Only call web_search when training-data answers may be stale.",
|
|
15
|
+
"Always include a Sources: section with markdown hyperlinks."
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"web_fetch": {
|
|
19
|
+
"promptSnippet": "Fetch a specific URL and read its content"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Software Development Best Practices
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
Comprehensive development guidelines for Agent OS projects, based on Kent Beck's "Tidy First?" principles and Kent C. Dodds' programming wisdom.
|
|
6
|
+
|
|
7
|
+
> "Software design is an exercise in human relationships" - Kent Beck
|
|
8
|
+
|
|
9
|
+
## 🎯 Core Principles
|
|
10
|
+
|
|
11
|
+
### 🧹 Tidy First Philosophy
|
|
12
|
+
|
|
13
|
+
- **Small, Safe Steps**: Make big changes through small, reversible steps
|
|
14
|
+
- **Human Relationships**: Code is communication between humans
|
|
15
|
+
- **Economic Thinking**: Balance current effort against future options
|
|
16
|
+
- **Eliminate Problems**: Remove complexity rather than managing it
|
|
17
|
+
|
|
18
|
+
### ✨ Code Quality Fundamentals
|
|
19
|
+
|
|
20
|
+
- **Don't Solve Problems, Eliminate Them**: Look for ways to reduce complexity
|
|
21
|
+
- **Optimize for Readability**: Prioritize code clarity over clever solutions
|
|
22
|
+
- **Self-Documenting Code**: Use meaningful names and clear structure
|
|
23
|
+
- **Test for Confidence**: Write tests that give confidence to change
|
|
24
|
+
|
|
25
|
+
### 🔄 Change-Friendly Design
|
|
26
|
+
|
|
27
|
+
- **Separate Tidying from Behavior Changes**: Keep refactoring separate from new features
|
|
28
|
+
- **Build for the Next Developer**: Consider maintainability and understanding
|
|
29
|
+
- **Options Over Things**: Create flexibility for uncertain future requirements
|
|
30
|
+
- **Progressive Enhancement**: Start simple, add complexity when needed
|
|
31
|
+
|
|
32
|
+
## 📦 Dependencies
|
|
33
|
+
|
|
34
|
+
### 🧠 Choose Libraries Wisely
|
|
35
|
+
|
|
36
|
+
When adding third-party dependencies:
|
|
37
|
+
|
|
38
|
+
- Select the most popular and actively maintained option
|
|
39
|
+
- Check the library's GitHub repository for:
|
|
40
|
+
- Recent commits (within last 6 months)
|
|
41
|
+
- Active issue resolution
|
|
42
|
+
- Number of stars/downloads
|
|
43
|
+
- Clear documentation
|
|
44
|
+
|
|
45
|
+
## 🧹 Tidying Practices
|
|
46
|
+
|
|
47
|
+
### ⏰ When to Tidy
|
|
48
|
+
|
|
49
|
+
**Before making a change**:
|
|
50
|
+
|
|
51
|
+
- Will tidying make the change easier? → Yes → Tidy first
|
|
52
|
+
- Will tidying take longer than the change? → No → Make change first, tidy after
|
|
53
|
+
- Is the change urgent? → Skip tidying for now, schedule for later
|
|
54
|
+
|
|
55
|
+
### 🔧 Core Tidying Techniques
|
|
56
|
+
|
|
57
|
+
#### 🛡️ Guard Clauses
|
|
58
|
+
|
|
59
|
+
Move preconditions to the top and return early:
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
// ❌ Nested conditions
|
|
63
|
+
function processUser(user) {
|
|
64
|
+
if (user) {
|
|
65
|
+
if (user.isActive) {
|
|
66
|
+
if (user.hasPermission) {
|
|
67
|
+
// main logic
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ✅ Guard clauses
|
|
74
|
+
function processUser(user) {
|
|
75
|
+
if (!user) return;
|
|
76
|
+
if (!user.isActive) return;
|
|
77
|
+
if (!user.hasPermission) return;
|
|
78
|
+
|
|
79
|
+
// main logic
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### 🗑️ Dead Code Elimination
|
|
84
|
+
|
|
85
|
+
Delete code that isn't executed or referenced.
|
|
86
|
+
|
|
87
|
+
#### ⚖️ Normalize Symmetries
|
|
88
|
+
|
|
89
|
+
Use consistent patterns throughout the codebase.
|
|
90
|
+
|
|
91
|
+
#### 🆘 Helper Variables and Functions
|
|
92
|
+
|
|
93
|
+
Extract complex expressions into well-named variables:
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
// ❌ Complex expression
|
|
97
|
+
if (
|
|
98
|
+
user.subscription.plan.tier === "premium" &&
|
|
99
|
+
user.subscription.status === "active" &&
|
|
100
|
+
user.subscription.expiresAt > new Date()
|
|
101
|
+
) {
|
|
102
|
+
// logic
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ✅ Helper variable
|
|
106
|
+
const hasActivePremiumSubscription =
|
|
107
|
+
user.subscription.plan.tier === "premium" &&
|
|
108
|
+
user.subscription.status === "active" &&
|
|
109
|
+
user.subscription.expiresAt > new Date();
|
|
110
|
+
|
|
111
|
+
if (hasActivePremiumSubscription) {
|
|
112
|
+
// logic
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 🧪 Testing Strategy
|
|
117
|
+
|
|
118
|
+
### 🏆 The Testing Trophy Approach
|
|
119
|
+
|
|
120
|
+
Based on Kent C. Dodds' testing philosophy:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
🏆 End-to-End (E2E)
|
|
124
|
+
↑ High confidence, slow, expensive
|
|
125
|
+
🥉 Integration Tests
|
|
126
|
+
↑ Good confidence, moderate speed
|
|
127
|
+
🥈 Unit Tests
|
|
128
|
+
↑ Low confidence, fast, cheap
|
|
129
|
+
🏅 Static Analysis
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 📝 Testing Guidelines
|
|
133
|
+
|
|
134
|
+
#### 💪 Write Tests That Give Confidence
|
|
135
|
+
|
|
136
|
+
- Test behavior, not implementation details
|
|
137
|
+
- Focus on user-facing functionality
|
|
138
|
+
- Prefer integration tests over isolated unit tests
|
|
139
|
+
|
|
140
|
+
#### 🏗️ Test Structure
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
// ✅ Clear test structure
|
|
144
|
+
test("should increment counter when button is clicked", () => {
|
|
145
|
+
// Arrange
|
|
146
|
+
render(<Counter />);
|
|
147
|
+
const button = screen.getByRole("button", { name: /increment/i });
|
|
148
|
+
const counter = screen.getByTestId("counter-value");
|
|
149
|
+
|
|
150
|
+
// Act
|
|
151
|
+
expect(counter).toHaveTextContent("0");
|
|
152
|
+
fireEvent.click(button);
|
|
153
|
+
|
|
154
|
+
// Assert
|
|
155
|
+
expect(counter).toHaveTextContent("1");
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### 🙈 Avoid Testing Implementation Details
|
|
160
|
+
|
|
161
|
+
- Don't test internal state or private methods
|
|
162
|
+
- Test the component's public interface
|
|
163
|
+
- Mock at the network boundary, not internal functions
|
|
164
|
+
|
|
165
|
+
## ⚡ Performance Practices
|
|
166
|
+
|
|
167
|
+
### 📊 Optimize for the Right Metrics
|
|
168
|
+
|
|
169
|
+
- Focus on user-centric performance (loading, interaction)
|
|
170
|
+
- Measure before optimizing
|
|
171
|
+
- Avoid premature micro-optimizations
|
|
172
|
+
|
|
173
|
+
### 📈 Progressive Enhancement
|
|
174
|
+
|
|
175
|
+
- Build core functionality that works without JavaScript
|
|
176
|
+
- Enhance with client-side features
|
|
177
|
+
- Use lazy loading for non-critical resources
|
|
178
|
+
|
|
179
|
+
### 🚀 Performance Patterns
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
// ✅ Lazy loading with Suspense
|
|
183
|
+
const HeavyComponent = lazy(() => import("./heavy-component"));
|
|
184
|
+
|
|
185
|
+
function App() {
|
|
186
|
+
return (
|
|
187
|
+
<Suspense fallback={<Skeleton />}>
|
|
188
|
+
<HeavyComponent />
|
|
189
|
+
</Suspense>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## 🤝 Collaboration Practices
|
|
195
|
+
|
|
196
|
+
### 💬 Code as Communication
|
|
197
|
+
|
|
198
|
+
- Express intent clearly through naming and structure
|
|
199
|
+
- Document decisions, not implementation
|
|
200
|
+
- Consider the next developer who will read this code
|
|
201
|
+
|
|
202
|
+
### 🔍 Pull Request Guidelines
|
|
203
|
+
|
|
204
|
+
**For Authors**:
|
|
205
|
+
|
|
206
|
+
- Separate tidying commits from behavior changes
|
|
207
|
+
- Write clear commit messages
|
|
208
|
+
- Include context in PR descriptions
|
|
209
|
+
|
|
210
|
+
**For Reviewers**:
|
|
211
|
+
|
|
212
|
+
- Focus on correctness, simplicity, and maintainability
|
|
213
|
+
- Ask questions when unclear
|
|
214
|
+
- Praise good solutions
|
|
215
|
+
|
|
216
|
+
### ♻️ Continuous Improvement
|
|
217
|
+
|
|
218
|
+
- Reflect on decisions and learn from mistakes
|
|
219
|
+
- Share knowledge through documentation and mentoring
|
|
220
|
+
- Regularly refactor and clean up technical debt
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Git Safety Guidelines
|
|
2
|
+
|
|
3
|
+
## Safe Git Operations
|
|
4
|
+
|
|
5
|
+
AI agents should follow these principles when working with git:
|
|
6
|
+
|
|
7
|
+
### ✅ Allowed Operations
|
|
8
|
+
|
|
9
|
+
- **Read operations**: `git status`, `git log`, `git diff`, `git show`
|
|
10
|
+
- **Safe commits**: `git add`, `git commit`
|
|
11
|
+
- **Branch management**: `git branch`, `git checkout -b`, `git switch`
|
|
12
|
+
- **Safe push**: `git push` (standard push without force)
|
|
13
|
+
- **Inspection**: `git blame`, `git ls-files`, `git rev-parse`
|
|
14
|
+
|
|
15
|
+
### ⛔ Operations to Avoid
|
|
16
|
+
|
|
17
|
+
Avoid these dangerous git commands without explicit user approval:
|
|
18
|
+
|
|
19
|
+
- **Add all files**: `git add -A`, `git add --all` (stages all changes including untracked files; prefer `git add <specific-files>` or `git add -p` for interactive staging)
|
|
20
|
+
- **Force push**: `git push --force`, `git push -f` (not recommended; if absolutely required, `--force-with-lease` is safer)
|
|
21
|
+
- **History rewriting**: `git rebase -i`, `git filter-branch`
|
|
22
|
+
- **Amending pushed commits**: `git commit --amend` (only safe for local, unpushed commits)
|
|
23
|
+
- **Reset operations**: `git reset`, `git reset --hard`, `git reset --mixed`, `git reset --soft` (unstages files or moves HEAD; can lose work or change history)
|
|
24
|
+
- **Force operations**: `git checkout --force`, `git clean -f/-d`, `git branch -D`
|
|
25
|
+
- **Stash deletion**: `git stash drop`, `git stash clear`
|
|
26
|
+
- **Reference manipulation**: `git update-ref -d`, `git reflog expire`
|
|
27
|
+
|
|
28
|
+
### Best Practices
|
|
29
|
+
|
|
30
|
+
- Always use `git --no-pager` to prevent interactive pagers in scripts
|
|
31
|
+
- Check repository state with `git status` before operations
|
|
32
|
+
- Stage files intentionally: use `git add <specific-files>` or `git add -p` for interactive staging
|
|
33
|
+
- Use `git diff` to verify changes before committing
|
|
34
|
+
- Prefer `git switch` over `git checkout` for branch switching (Git 2.23+; use `git checkout <branch>` for older versions)
|
|
35
|
+
- Use descriptive commit messages following conventional commits format
|
|
36
|
+
- Create feature branches instead of working directly on main/master
|
|
37
|
+
- Pull before push to avoid conflicts: `git pull origin <branch>` (or `git fetch origin && git merge origin/<branch>` for more control)
|
|
38
|
+
|
|
39
|
+
### Error Handling
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Check if git repository
|
|
43
|
+
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
|
44
|
+
echo "Error: Not a git repository" >&2
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Check for uncommitted changes
|
|
49
|
+
if ! git diff-index --quiet HEAD --; then
|
|
50
|
+
echo "Warning: Uncommitted changes detected" >&2
|
|
51
|
+
fi
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Pre-commit Checklist
|
|
55
|
+
|
|
56
|
+
- [ ] Shell scripts pass `bash -n` syntax check
|
|
57
|
+
- [ ] Tested with `--dry-run`
|
|
58
|
+
- [ ] No absolute paths in configs
|
|
59
|
+
- [ ] Colors and logging functions used consistently
|
|
60
|
+
- [ ] Error handling with `set -e` and guard clauses
|
|
61
|
+
- [ ] Documentation updated if workflow changed
|
|
62
|
+
- [ ] Git operations follow safety guidelines
|
|
63
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "pi-lazy-setup",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Initialize the Pi agent environment from a curated snapshot",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pi-setup": "./bin/pi-setup.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"configs"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node --test",
|
|
16
|
+
"dev": "node bin/pi-setup.mjs"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"cli",
|
|
20
|
+
"pi",
|
|
21
|
+
"agent-setup"
|
|
22
|
+
],
|
|
23
|
+
"author": "kk",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@biomejs/biome": "^2.5.1"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@inquirer/prompts": "^7.10.1",
|
|
30
|
+
"chalk": "^5.3.0",
|
|
31
|
+
"commander": "^12.0.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// pi.mjs — Pi agent descriptor.
|
|
2
|
+
// Pure data; the initializer in src/commands/init.mjs loops over a list of
|
|
3
|
+
// these. Paths with ~ are expanded at runtime by the initializer.
|
|
4
|
+
//
|
|
5
|
+
// The CLI only deploys the config snapshot and the /pi-setup extension
|
|
6
|
+
// (deploy.extensionFiles). Installing the pi binary, packages, skills, and
|
|
7
|
+
// CLI tools is out of scope — `/pi-setup` handles those interactively inside
|
|
8
|
+
// Pi, each item confirmed individually.
|
|
9
|
+
|
|
10
|
+
/** @type {import('./types.mjs').AgentDescriptor} */
|
|
11
|
+
export const pi = {
|
|
12
|
+
id: "pi",
|
|
13
|
+
name: "Pi",
|
|
14
|
+
home: "~/.pi/agent",
|
|
15
|
+
configSource: "configs/pi",
|
|
16
|
+
referenceDest: "~/.pi/agent/pi-setup",
|
|
17
|
+
deploy: {
|
|
18
|
+
overwriteFiles: ["settings.json", "mcp.json", "models.json", "AGENTS.md"],
|
|
19
|
+
replaceDirs: ["prompts"],
|
|
20
|
+
extensionFiles: ["pi-setup.js"],
|
|
21
|
+
overwriteExtra: [{ file: "web-config.json", dest: "~/.config/rpiv-web-tools/config.json" }],
|
|
22
|
+
referenceSource: "configs/reference",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// types.mjs — JSDoc type shapes for agent descriptors.
|
|
2
|
+
// No runtime exports; used only for type hints in editors.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {Object} DeploySpec
|
|
6
|
+
* @property {string[]} overwriteFiles — files copied flat into home
|
|
7
|
+
* @property {string[]} replaceDirs — subdirs rm-rf'd then fully replaced
|
|
8
|
+
* @property {string[]} [extensionFiles] — extension files copied flat into `<home>/extensions/`
|
|
9
|
+
* @property {{ file: string, dest: string }[]} [overwriteExtra] — files copied to a ~-prefixed dest outside home
|
|
10
|
+
* @property {string} referenceSource — package-relative path to reference docs
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} AgentDescriptor
|
|
15
|
+
* @property {string} id
|
|
16
|
+
* @property {string} name
|
|
17
|
+
* @property {string} home — ~-prefixed agent home
|
|
18
|
+
* @property {string} configSource — package-relative path to agent configs
|
|
19
|
+
* @property {string} referenceDest — ~-prefixed destination for reference docs
|
|
20
|
+
* @property {DeploySpec} deploy
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// init.mjs — the initializer.
|
|
2
|
+
// Loops over the agent descriptors and runs detect → (backup)
|
|
3
|
+
// → deploy-configs → deploy-reference.
|
|
4
|
+
// Each step honors { dryRun }.
|
|
5
|
+
//
|
|
6
|
+
// Binaries, packages, skills, and CLI tools are NOT installed here. Instead
|
|
7
|
+
// the /pi-setup extension is deployed (see deploy.extensions) — run `/pi-setup`
|
|
8
|
+
// inside Pi to install those interactively, each item confirmed individually.
|
|
9
|
+
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
import { pi } from "../agents/pi.mjs";
|
|
13
|
+
import { backupToTar, copyDir, copyDirFiles, copyFile, ensureDir } from "../lib/fs.mjs";
|
|
14
|
+
import { log } from "../lib/log.mjs";
|
|
15
|
+
import { expandHome, packageRoot, pkgPath } from "../lib/paths.mjs";
|
|
16
|
+
|
|
17
|
+
const AGENTS = [pi];
|
|
18
|
+
|
|
19
|
+
export { AGENTS };
|
|
20
|
+
|
|
21
|
+
/** Directory for --backup tars: ~/ai-tools-backup-<timestamp>/ */
|
|
22
|
+
function backupDirFor() {
|
|
23
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
24
|
+
return resolve(homedir(), `ai-tools-backup-${ts}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Deploy step: copy the agent snapshot into its home. Flat files are
|
|
29
|
+
* overwritten, subdirs replaced wholesale, extension files deployed so they
|
|
30
|
+
* auto-load on the next Pi start. No binary installs happen here.
|
|
31
|
+
*/
|
|
32
|
+
function deployConfigs(agent, pkgRoot, { dryRun }) {
|
|
33
|
+
const home = expandHome(agent.home);
|
|
34
|
+
const srcDir = pkgPath(pkgRoot, agent.configSource);
|
|
35
|
+
ensureDir(home, { dryRun });
|
|
36
|
+
for (const file of agent.deploy.overwriteFiles) {
|
|
37
|
+
copyFile(resolve(srcDir, file), resolve(home, file), { dryRun });
|
|
38
|
+
}
|
|
39
|
+
for (const dir of agent.deploy.replaceDirs) {
|
|
40
|
+
copyDir(resolve(srcDir, dir), resolve(home, dir), { dryRun });
|
|
41
|
+
}
|
|
42
|
+
for (const extra of agent.deploy.overwriteExtra ?? []) {
|
|
43
|
+
copyFile(resolve(srcDir, extra.file), expandHome(extra.dest), { dryRun });
|
|
44
|
+
}
|
|
45
|
+
// extensions: deploy each file flat into ~/.pi/agent/extensions/ so Pi
|
|
46
|
+
// auto-discovers it on the next start. Existing files with the same name
|
|
47
|
+
// are overwritten (snapshot semantics).
|
|
48
|
+
for (const file of agent.deploy.extensionFiles ?? []) {
|
|
49
|
+
copyFile(resolve(srcDir, "extensions", file), resolve(home, "extensions", file), { dryRun });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function deployReference(agent, pkgRoot, { dryRun }) {
|
|
54
|
+
const dest = expandHome(agent.referenceDest);
|
|
55
|
+
const src = pkgPath(pkgRoot, agent.deploy.referenceSource);
|
|
56
|
+
ensureDir(dest, { dryRun });
|
|
57
|
+
copyDirFiles(src, dest, { dryRun });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Back up the agent's existing home to a tar (no-op if absent).
|
|
62
|
+
*/
|
|
63
|
+
function backupAgentHome(agent, backupDir, { dryRun }) {
|
|
64
|
+
backupToTar(expandHome(agent.home), backupDir, { dryRun });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Initialize the configured agents: (backup) → deploy configs (including the
|
|
69
|
+
* /pi-setup extension) → deploy reference docs. Returns 0 on success.
|
|
70
|
+
*
|
|
71
|
+
* Installing binaries, packages, skills, and CLI tools is deferred to the
|
|
72
|
+
* `/pi-setup` command inside Pi (see configs/pi/extensions/pi-setup.js).
|
|
73
|
+
*
|
|
74
|
+
* @param {{ dryRun?: boolean, backup?: boolean, agents?: Array }} opts
|
|
75
|
+
* @returns {Promise<number>}
|
|
76
|
+
*/
|
|
77
|
+
export async function initialize({ dryRun = false, backup = false, agents = AGENTS } = {}) {
|
|
78
|
+
const pkgRoot = packageRoot(import.meta.url);
|
|
79
|
+
const backupDir = backup ? backupDirFor() : null;
|
|
80
|
+
log.info(dryRun ? "Dry-run mode — no changes will be made" : "Initializing agent environments");
|
|
81
|
+
if (backup) log.info(`Backing up existing agent homes to ${backupDir}`);
|
|
82
|
+
for (const agent of agents) {
|
|
83
|
+
log.step(`Configure ${agent.name}`);
|
|
84
|
+
if (backup) backupAgentHome(agent, backupDir, { dryRun });
|
|
85
|
+
deployConfigs(agent, pkgRoot, { dryRun });
|
|
86
|
+
deployReference(agent, pkgRoot, { dryRun });
|
|
87
|
+
}
|
|
88
|
+
log.success("Setup complete");
|
|
89
|
+
log.info("Run /pi-setup inside Pi to install packages, skills, and CLI tools (each confirmed individually)");
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// select.mjs — resolve which agents to configure.
|
|
2
|
+
//
|
|
3
|
+
// Resolution order:
|
|
4
|
+
// 1. positional args (pi-setup pi) → validate ids, use exactly those
|
|
5
|
+
// 2. non-TTY (CI / piped) → all agents, no prompt
|
|
6
|
+
// 3. TTY → interactive select
|
|
7
|
+
//
|
|
8
|
+
// Exits non-zero on unknown agent ids.
|
|
9
|
+
|
|
10
|
+
import { checkbox } from "@inquirer/prompts";
|
|
11
|
+
import { log } from "../lib/log.mjs";
|
|
12
|
+
import { AGENTS } from "./init.mjs";
|
|
13
|
+
|
|
14
|
+
const VALID_IDS = new Set(AGENTS.map((a) => a.id));
|
|
15
|
+
|
|
16
|
+
export async function selectAgents(positional, { tty = false } = {}) {
|
|
17
|
+
// 1. explicit positional args
|
|
18
|
+
if (positional.length > 0) {
|
|
19
|
+
const invalid = positional.filter((id) => !VALID_IDS.has(id));
|
|
20
|
+
if (invalid.length > 0) {
|
|
21
|
+
log.error(`Unknown agent(s): ${invalid.join(", ")}`);
|
|
22
|
+
log.info(`Valid ids: ${AGENTS.map((a) => a.id).join(", ")}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
return positional;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. non-interactive: all agents
|
|
29
|
+
if (!tty) {
|
|
30
|
+
log.info("Non-interactive mode — configuring all agents");
|
|
31
|
+
return AGENTS.map((a) => a.id);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 3. interactive multi-select
|
|
35
|
+
const selected = await checkbox({
|
|
36
|
+
message: "Select agents to configure:",
|
|
37
|
+
default: true,
|
|
38
|
+
choices: AGENTS.map((a) => ({
|
|
39
|
+
name: a.name,
|
|
40
|
+
value: a.id,
|
|
41
|
+
checked: true,
|
|
42
|
+
})),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (selected.length === 0) {
|
|
46
|
+
log.warning("No agents selected — nothing to do");
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return selected;
|
|
51
|
+
}
|
package/src/lib/fs.mjs
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// fs.mjs — filesystem helpers for deploy.
|
|
2
|
+
// All functions take an optional { dryRun } option: when true, print the
|
|
3
|
+
// planned action with full paths and do NOT touch the filesystem.
|
|
4
|
+
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { basename, dirname, resolve } from "node:path";
|
|
9
|
+
import { log } from "./log.mjs";
|
|
10
|
+
|
|
11
|
+
/** Strip the leading home directory from an absolute path for nicer tar names. */
|
|
12
|
+
function stripHome(p) {
|
|
13
|
+
const h = homedir();
|
|
14
|
+
return p.startsWith(h) ? p.slice(h.length).replace(/^\/+/, "") : p;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Ensure a directory exists (recursive). Prints in dry-run.
|
|
19
|
+
*/
|
|
20
|
+
export function ensureDir(dir, { dryRun = false } = {}) {
|
|
21
|
+
if (dryRun) {
|
|
22
|
+
log.dry(`mkdir -p ${dir}`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!existsSync(dir)) {
|
|
26
|
+
mkdirSync(dir, { recursive: true });
|
|
27
|
+
log.info(`mkdir ${dir}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Remove a directory recursively. No-op if it doesn't exist. Prints in dry-run.
|
|
33
|
+
*/
|
|
34
|
+
export function rmrf(dir, { dryRun = false } = {}) {
|
|
35
|
+
if (dryRun) {
|
|
36
|
+
if (existsSync(dir)) log.dry(`rm -rf ${dir}`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (existsSync(dir)) {
|
|
40
|
+
rmSync(dir, { recursive: true, force: true });
|
|
41
|
+
log.info(`rm -rf ${dir}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Copy a single file, creating parent dirs as needed. Prints in dry-run.
|
|
47
|
+
*/
|
|
48
|
+
export function copyFile(src, dest, { dryRun = false } = {}) {
|
|
49
|
+
if (dryRun) {
|
|
50
|
+
log.dry(`cp ${src} → ${dest}`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
ensureDir(dirname(dest), { dryRun: false });
|
|
54
|
+
cpSync(src, dest, { force: true });
|
|
55
|
+
log.deploy(dest);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Copy a directory recursively, fully replacing the destination.
|
|
60
|
+
* Calls rmrf(dest) then copies src → dest. Prints in dry-run.
|
|
61
|
+
*/
|
|
62
|
+
export function copyDir(src, dest, { dryRun = false } = {}) {
|
|
63
|
+
if (!existsSync(src)) {
|
|
64
|
+
if (dryRun) log.dry(`skip ${dest} (source ${src} does not exist)`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (dryRun) {
|
|
68
|
+
log.dry(`rm -rf ${dest} && cp -r ${src} ${dest}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
rmrf(dest, { dryRun: false });
|
|
72
|
+
ensureDir(dirname(dest), { dryRun: false });
|
|
73
|
+
cpSync(src, dest, { recursive: true, force: true });
|
|
74
|
+
log.deploy(dest);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Copy every file in sourceDir (non-recursively) into destDir.
|
|
79
|
+
* Used to deploy reference docs (flat file set). Prints in dry-run.
|
|
80
|
+
*/
|
|
81
|
+
export function copyDirFiles(sourceDir, destDir, { dryRun = false } = {}) {
|
|
82
|
+
if (!existsSync(sourceDir)) return;
|
|
83
|
+
const entries = readdirSync(sourceDir);
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
const src = resolve(sourceDir, entry);
|
|
86
|
+
const stat = statSync(src);
|
|
87
|
+
if (stat.isFile()) {
|
|
88
|
+
copyFile(src, resolve(destDir, entry), { dryRun });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Back up a directory tree to a tar file under backupDir.
|
|
95
|
+
* Skips silently if the source does not exist. Prints in dry-run.
|
|
96
|
+
* Returns the created tar path (or null if skipped/no-op).
|
|
97
|
+
*/
|
|
98
|
+
export function backupToTar(src, backupDir, { dryRun = false } = {}) {
|
|
99
|
+
if (!existsSync(src)) return null;
|
|
100
|
+
const base = basename(stripHome(src));
|
|
101
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
102
|
+
const tar = resolve(backupDir, `${base}-${ts}.tar.gz`);
|
|
103
|
+
if (dryRun) {
|
|
104
|
+
log.dry(`backup ${src} → ${tar}`);
|
|
105
|
+
return tar;
|
|
106
|
+
}
|
|
107
|
+
ensureDir(backupDir, { dryRun: false });
|
|
108
|
+
execFileSync("tar", ["-czf", tar, "-C", dirname(src), base], { stdio: "ignore" });
|
|
109
|
+
log.info(`backup ${src} → ${tar}`);
|
|
110
|
+
return tar;
|
|
111
|
+
}
|
package/src/lib/log.mjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// log.mjs — structured stdout logger built on chalk.
|
|
2
|
+
// Every action prints a full resolved path so the user can see exactly what
|
|
3
|
+
// the CLI did.
|
|
4
|
+
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
|
|
7
|
+
function fullPath(p) {
|
|
8
|
+
return typeof p === "string" ? p : String(p);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const log = {
|
|
12
|
+
info(msg) {
|
|
13
|
+
console.log(`${chalk.cyan("ℹ")} ${msg}`);
|
|
14
|
+
},
|
|
15
|
+
success(msg) {
|
|
16
|
+
console.log(`${chalk.green("✓")} ${msg}`);
|
|
17
|
+
},
|
|
18
|
+
warning(msg) {
|
|
19
|
+
console.log(`${chalk.yellow("⚠")} ${msg}`);
|
|
20
|
+
},
|
|
21
|
+
error(msg) {
|
|
22
|
+
console.error(`${chalk.red("✗")} ${msg}`);
|
|
23
|
+
},
|
|
24
|
+
dry(msg) {
|
|
25
|
+
console.log(`${chalk.magenta("[dry]")} ${msg}`);
|
|
26
|
+
},
|
|
27
|
+
step(msg) {
|
|
28
|
+
console.log(`${chalk.dim("─")} ${msg}`);
|
|
29
|
+
},
|
|
30
|
+
install(name) {
|
|
31
|
+
console.log(`${chalk.cyan("ℹ")} Installing ${name}...`);
|
|
32
|
+
},
|
|
33
|
+
skip(name) {
|
|
34
|
+
console.log(`${chalk.dim("•")} ${name} already installed, skipping`);
|
|
35
|
+
},
|
|
36
|
+
deploy(dest) {
|
|
37
|
+
console.log(`${chalk.green("✓")} Deployed → ${fullPath(dest)}`);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// paths.mjs — resolve package root and agent homes.
|
|
2
|
+
// Resolves from import.meta.url (NOT process.cwd()) so `npx pi-lazy-setup` works
|
|
3
|
+
// regardless of the user's current directory.
|
|
4
|
+
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the package root (the directory containing bin/, src/, configs/).
|
|
11
|
+
* Pass import.meta.url from the calling module.
|
|
12
|
+
*/
|
|
13
|
+
export function packageRoot(importMetaUrl) {
|
|
14
|
+
const here = dirname(fileURLToPath(importMetaUrl));
|
|
15
|
+
// src/lib/paths.mjs → up two = package root
|
|
16
|
+
return resolve(here, "..", "..");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a path inside the package root. Segments are package-relative
|
|
21
|
+
* (e.g. "configs", "pi", "settings.json"). Descriptors already carry the
|
|
22
|
+
* "configs/<agent>" prefix, so this does NOT add a magic "configs/".
|
|
23
|
+
*/
|
|
24
|
+
export function pkgPath(pkgRoot, ...segments) {
|
|
25
|
+
return resolve(pkgRoot, ...segments);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Expand a leading ~ to the user's home directory.
|
|
30
|
+
*/
|
|
31
|
+
export function expandHome(p) {
|
|
32
|
+
if (!p) return p;
|
|
33
|
+
if (p === "~") return homedir();
|
|
34
|
+
if (p.startsWith("~/")) return resolve(homedir(), p.slice(2));
|
|
35
|
+
return p;
|
|
36
|
+
}
|