pi-lazy-setup 0.0.0-stage → 2.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 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,57 @@
1
- # Temporary Holding Version
1
+ # pi-setup
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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. **Installs the binary** — `pi` via npm. Skips if already on PATH.
12
+ 2. **Deploys configs** — overwrites `~/.pi/agent` from bundled `configs/pi/`.
13
+ 3. **Deploys reference docs** — copies into `~/.pi/agent/pi-setup/`.
14
+ 4. **Installs default skills** — via `npx skills add` (https://skills.sh/).
15
+
16
+ ## Flags
17
+
18
+ | Flag | Description |
19
+ | --- | --- |
20
+ | `[agents...]` | `pi`. Omit to run directly. |
21
+ | `--dry-run` | Preview planned actions, touch nothing. |
22
+ | `--backup` | Tar the existing Pi home before overwrite. |
23
+
24
+ ## Snapshot Philosophy
25
+
26
+ This is a **snapshot**, not a generic installer. Configs ship exactly as the author uses them — personal proxies, extension lists, etc. No templating.
27
+
28
+ ## Scope
29
+
30
+ **Pi** (<https://github.com/mariozechner/pi-coding-agent>). MCP server binaries are not installed — only the declaration file (`mcp.json`) and config files (`settings.json`, `models.json`, `AGENTS.md`, `prompts/`) are deployed.
31
+
32
+ For skill management, see [@kkmila/isc](https://github.com/machuqiji/isc).
33
+
34
+ ## Development
35
+
36
+ ```bash
37
+ npm install # required for local development
38
+ node bin/pi-setup.mjs --dry-run # preview
39
+ node bin/pi-setup.mjs # real run
40
+ npm test # process-seam tests
41
+ ```
42
+
43
+ ## Releasing
44
+
45
+ Create a GitHub Release to trigger npm publish:
46
+
47
+ ```bash
48
+ npm version <major|minor|patch>
49
+ git push
50
+ gh release create v1.2.0 --generate-notes
51
+ ```
52
+
53
+ The workflow runs tests (`continue-on-error: true`) then `npm publish`. Requires `NPM_TOKEN` secret.
54
+
55
+ ## License
56
+
57
+ MIT
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+
3
+ // pi-setup — initialize the Pi agent environment from a snapshot.
4
+ //
5
+ // Usage:
6
+ // pi-setup run directly (all = pi)
7
+ // pi-setup pi explicit
8
+ // Flags: --dry-run, --backup, --help, --version.
9
+
10
+ import { createRequire } from "node:module";
11
+ import { program } from "commander";
12
+ import { AGENTS, initialize } from "../src/commands/init.mjs";
13
+ import { selectAgents } from "../src/commands/select.mjs";
14
+
15
+ const { version } = createRequire(import.meta.url)("../package.json");
16
+
17
+ program
18
+ .name("pi-setup")
19
+ .description("Initialize the Pi agent environment from a curated snapshot")
20
+ .version(version)
21
+ .argument("[agents...]", "agent id to configure (pi); omit to run directly")
22
+ .option("--dry-run", "print planned actions, execute nothing")
23
+ .option("--backup", "back up existing agent home before overwriting")
24
+ .action(async (positional, opts) => {
25
+ const ids = await selectAgents(positional, { tty: process.stdin.isTTY });
26
+ const chosen = AGENTS.filter((a) => ids.includes(a.id));
27
+ const code = await initialize({
28
+ dryRun: !!opts.dryRun,
29
+ backup: !!opts.backup,
30
+ tty: !!process.stdin.isTTY,
31
+ agents: chosen,
32
+ });
33
+ process.exit(code);
34
+ });
35
+
36
+ program.parse();
@@ -0,0 +1,7 @@
1
+ ## Language
2
+
3
+ - 用中文回复。代码、命令、文件名、报错原文保持原文。
4
+
5
+ ## network
6
+
7
+ - 网络失败先按 127.0.0.1:7890 → 127.0.0.1:7891 → 192.168.1.254:7893 顺序换代理重试。
@@ -0,0 +1,5 @@
1
+ {
2
+ "mcpServers": {
3
+
4
+ }
5
+ }
@@ -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
- "name": "pi-lazy-setup",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "pi-lazy-setup",
3
+ "version": "2.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,28 @@
1
+ // packages.mjs — Pi extension packages offered for install via `pi install`.
2
+ // The settings.json snapshot ships WITHOUT a "packages" key; the initializer
3
+ // prompts the user to pick which ones to install (interactive TTY only;
4
+ // non-interactive runs skip package install entirely — nothing is
5
+ // auto-installed).
6
+
7
+ /**
8
+ * Packages offered for installation. `source` is passed verbatim to
9
+ * `pi install`, which writes the declaration into ~/.pi/agent/settings.json.
10
+ */
11
+ export const DEFAULT_PACKAGES = [
12
+ {
13
+ source: "npm:pi-footer",
14
+ description: "Footer status line (token usage, model, cost).",
15
+ },
16
+ {
17
+ source: "npm:pi-tps-meter",
18
+ description: "Tokens-per-second meter for streaming responses.",
19
+ },
20
+ {
21
+ source: "npm:@juicesharp/rpiv-pi",
22
+ description: "RPIV pipeline skills (code review, commit, handoff, diagrams).",
23
+ },
24
+ {
25
+ source: "https://github.com/cathrynlavery/diagram-design",
26
+ description: "Branded diagram/chart generation skills.",
27
+ },
28
+ ];
@@ -0,0 +1,27 @@
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
+ import { DEFAULT_PACKAGES } from "./packages.mjs";
5
+ import { DEFAULT_SKILLS } from "./skills.mjs";
6
+
7
+ /** @type {import('./types.mjs').AgentDescriptor} */
8
+ export const pi = {
9
+ id: "pi",
10
+ name: "Pi",
11
+ home: "~/.pi/agent",
12
+ configSource: "configs/pi",
13
+ referenceDest: "~/.pi/agent/pi-setup",
14
+ install: {
15
+ kind: "npm",
16
+ pkg: "@mariozechner/pi-coding-agent",
17
+ bin: "pi",
18
+ },
19
+ deploy: {
20
+ overwriteFiles: ["settings.json", "mcp.json", "models.json", "AGENTS.md"],
21
+ replaceDirs: ["prompts"],
22
+ overwriteExtra: [{ file: "web-config.json", dest: "~/.config/rpiv-web-tools/config.json" }],
23
+ referenceSource: "configs/reference",
24
+ },
25
+ skills: { specs: DEFAULT_SKILLS, cliAgent: "pi" },
26
+ packages: { specs: DEFAULT_PACKAGES },
27
+ };
@@ -0,0 +1,19 @@
1
+ // skills.mjs — skill specs available to install from remote via `npx skills add`.
2
+ // Each spec is "owner/repo@skill" (see https://skills.sh/).
3
+ // The initializer prompts the user to pick which ones to install
4
+ // (interactive TTY only; non-interactive runs skip skill install).
5
+
6
+ /**
7
+ * Skills offered for installation. Sourced from remote via
8
+ * `npx skills add` (https://skills.sh/).
9
+ */
10
+ export const DEFAULT_SKILLS = [
11
+ {
12
+ spec: "mattpocock/skills@grill-me",
13
+ description: "Matt Pocock — a relentless interview to sharpen a plan or design.",
14
+ },
15
+ {
16
+ spec: "microsoft/playwright-cli@playwright-cli",
17
+ description: "Microsoft — browser automation and Playwright test driving.",
18
+ },
19
+ ];
@@ -0,0 +1,54 @@
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} InstallSpec
6
+ * @property {"npm"} kind
7
+ * @property {string} [pkg] — npm package name
8
+ * @property {string} bin — binary name to detect on PATH
9
+ */
10
+
11
+ /**
12
+ * @typedef {Object} DeploySpec
13
+ * @property {string[]} overwriteFiles — files copied flat into home
14
+ * @property {string[]} replaceDirs — subdirs rm-rf'd then fully replaced
15
+ * @property {{ file: string, dest: string }[]} [overwriteExtra] — files copied to a ~-prefixed dest outside home
16
+ * @property {string} referenceSource — package-relative path to reference docs
17
+ * @property {SkillsSpec} [skills] — default skills to install from remote
18
+ * @property {PackagesSpec} [packages] — extension packages offered via `pi install`
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} SkillSpec
23
+ * @property {string} spec — "owner/repo@skill" identifier
24
+ * @property {string} [description] — shown in the interactive skill picker
25
+ */
26
+
27
+ /**
28
+ * @typedef {Object} SkillsSpec
29
+ * @property {SkillSpec[]} specs — skills offered for install (user selects)
30
+ * @property {string} [cliAgent] — lowercase skills-CLI agent name; install via `npx skills add`
31
+ */
32
+ /**
33
+ * @typedef {Object} PackageSpec
34
+ * @property {string} source — package source passed to `pi install`
35
+ * @property {string} [description] — shown in the interactive package picker
36
+ */
37
+
38
+ /**
39
+ * @typedef {Object} PackagesSpec
40
+ * @property {PackageSpec[]} specs — packages offered for install (user selects)
41
+ */
42
+
43
+ /**
44
+ * @typedef {Object} AgentDescriptor
45
+ * @property {string} id
46
+ * @property {string} name
47
+ * @property {string} home — ~-prefixed agent home
48
+ * @property {string} configSource — package-relative path to agent configs
49
+ * @property {string} referenceDest — ~-prefixed destination for reference docs
50
+ * @property {InstallSpec} install
51
+ * @property {DeploySpec} deploy
52
+ */
53
+
54
+ export {};
@@ -0,0 +1,189 @@
1
+ // init.mjs — the initializer.
2
+ // Loops over the agent descriptors and runs detect → install → (backup)
3
+ // → deploy-configs → deploy-reference → select-packages → install-packages
4
+ // → select-skills → install-skills.
5
+ // Each step honors { dryRun }.
6
+
7
+ import { homedir } from "node:os";
8
+ import { resolve } from "node:path";
9
+ import { checkbox } from "@inquirer/prompts";
10
+ import { pi } from "../agents/pi.mjs";
11
+ import { detectBinary, runCommand } from "../lib/exec.mjs";
12
+ import { backupToTar, copyDir, copyDirFiles, copyFile, ensureDir } from "../lib/fs.mjs";
13
+ import { log } from "../lib/log.mjs";
14
+ import { expandHome, packageRoot, pkgPath } from "../lib/paths.mjs";
15
+
16
+ const AGENTS = [pi];
17
+
18
+ export { AGENTS };
19
+
20
+ /** Directory for --backup tars: ~/ai-tools-backup-<timestamp>/ */
21
+ function backupDirFor() {
22
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
23
+ return resolve(homedir(), `ai-tools-backup-${ts}`);
24
+ }
25
+
26
+ /**
27
+ * Install step: detect the agent's binary on PATH; if missing, run the
28
+ * install command. No-op (print only) in dry-run.
29
+ */
30
+ async function installStep(agent, { dryRun }) {
31
+ const present = dryRun ? false : detectBinary(agent.install.bin);
32
+ if (present) {
33
+ if (!dryRun) log.skip(agent.name);
34
+ else log.dry(`detect ${agent.install.bin} (assume present → skip)`);
35
+ return 0;
36
+ }
37
+ // install
38
+ const cmdline = installCmdline(agent.install);
39
+ if (dryRun) {
40
+ log.dry(`detect ${agent.install.bin} → missing → install`);
41
+ }
42
+ log.install(agent.name);
43
+ return runCommand(cmdline, { dryRun });
44
+ }
45
+
46
+ function installCmdline(spec) {
47
+ if (spec.kind === "npm") return `npm install -g ${spec.pkg}`;
48
+ throw new Error(`unknown install kind: ${spec.kind}`);
49
+ }
50
+
51
+ /**
52
+ * Deploy agent configs: overwrite flat files, replace subdirs wholesale.
53
+ */
54
+ function deployConfigs(agent, pkgRoot, { dryRun }) {
55
+ const home = expandHome(agent.home);
56
+ const srcDir = pkgPath(pkgRoot, agent.configSource);
57
+ ensureDir(home, { dryRun });
58
+ for (const file of agent.deploy.overwriteFiles) {
59
+ copyFile(resolve(srcDir, file), resolve(home, file), { dryRun });
60
+ }
61
+ for (const dir of agent.deploy.replaceDirs) {
62
+ copyDir(resolve(srcDir, dir), resolve(home, dir), { dryRun });
63
+ }
64
+ for (const extra of agent.deploy.overwriteExtra ?? []) {
65
+ copyFile(resolve(srcDir, extra.file), expandHome(extra.dest), { dryRun });
66
+ }
67
+ }
68
+
69
+ function deployReference(agent, pkgRoot, { dryRun }) {
70
+ const dest = expandHome(agent.referenceDest);
71
+ const src = pkgPath(pkgRoot, agent.deploy.referenceSource);
72
+ ensureDir(dest, { dryRun });
73
+ copyDirFiles(src, dest, { dryRun });
74
+ }
75
+
76
+ /**
77
+ * Skills step: let the user pick which skills to install (interactive TTY),
78
+ * then install each selected one from remote via `npx skills add`
79
+ * (https://skills.sh/). Non-interactive runs skip skill install entirely —
80
+ * nothing is auto-installed.
81
+ */
82
+ async function installSkillsStep(agent, { dryRun, tty }) {
83
+ const spec = agent.skills;
84
+ if (!spec) return 0;
85
+ let selected = [];
86
+ if (tty) {
87
+ selected = await checkbox({
88
+ message: "Select skills to install:",
89
+ choices: spec.specs.map((s) => ({
90
+ name: s.spec,
91
+ value: s.spec,
92
+ description: s.description,
93
+ checked: true,
94
+ })),
95
+ });
96
+ if (selected.length === 0) {
97
+ log.info("No skills selected — skipping skill install");
98
+ return 0;
99
+ }
100
+ } else {
101
+ log.info(`Non-interactive mode — skipping skill install (available: ${spec.specs.map((s) => s.spec).join(", ")})`);
102
+ return 0;
103
+ }
104
+ for (const skill of selected) {
105
+ const { repo, name } = parseSkillSpec(skill);
106
+ const cmdline = `npx --yes skills add ${repo} --skill ${name} -g --agent ${spec.cliAgent} -y`;
107
+ log.install(`${skill} → ${spec.cliAgent}`);
108
+ const code = await runCommand(cmdline, { dryRun });
109
+ if (code !== 0) return code;
110
+ }
111
+ return 0;
112
+ }
113
+
114
+ /**
115
+ * Packages step: let the user pick which Pi extension packages to install
116
+ * (interactive TTY), then install each via `pi install <source>`, which
117
+ * writes the declaration into ~/.pi/agent/settings.json. Non-interactive
118
+ * runs skip package install entirely — nothing is auto-installed.
119
+ */
120
+ async function installPackagesStep(agent, { dryRun, tty }) {
121
+ const spec = agent.packages;
122
+ if (!spec) return 0;
123
+ let selected = [];
124
+ if (tty) {
125
+ selected = await checkbox({
126
+ message: "Select Pi extension packages to install:",
127
+ choices: spec.specs.map((p) => ({
128
+ name: p.source,
129
+ value: p.source,
130
+ description: p.description,
131
+ checked: false,
132
+ })),
133
+ });
134
+ if (selected.length === 0) {
135
+ log.info("No packages selected — skipping package install");
136
+ return 0;
137
+ }
138
+ } else {
139
+ const available = spec.specs.map((p) => p.source).join(", ");
140
+ log.info(`Non-interactive mode — skipping package install (available: ${available})`);
141
+ return 0;
142
+ }
143
+ for (const source of selected) {
144
+ const cmdline = `pi install ${source}`;
145
+ log.install(`${source} → ${agent.name}`);
146
+ const code = await runCommand(cmdline, { dryRun });
147
+ if (code !== 0) return code;
148
+ }
149
+ return 0;
150
+ }
151
+
152
+ /** Parse "owner/repo@skill" into { repo, name }. */
153
+ function parseSkillSpec(spec) {
154
+ const at = spec.lastIndexOf("@");
155
+ if (at < 0) throw new Error(`skill spec missing @skill: ${spec}`);
156
+ return { repo: spec.slice(0, at), name: spec.slice(at + 1) };
157
+ }
158
+
159
+ /**
160
+ * Back up the agent's existing home to a tar (no-op if absent).
161
+ */
162
+ function backupAgentHome(agent, backupDir, { dryRun }) {
163
+ backupToTar(expandHome(agent.home), backupDir, { dryRun });
164
+ }
165
+
166
+ /**
167
+ * Initialize the configured agents. Returns 0 on success, non-zero on failure.
168
+ *
169
+ * @param {{ dryRun?: boolean, backup?: boolean, tty?: boolean, agents?: Array }} opts
170
+ * @returns {Promise<number>}
171
+ */
172
+ export async function initialize({ dryRun = false, backup = false, tty = false, agents = AGENTS } = {}) {
173
+ const pkgRoot = packageRoot(import.meta.url);
174
+ const backupDir = backup ? backupDirFor() : null;
175
+ log.info(dryRun ? "Dry-run mode — no changes will be made" : "Initializing agent environments");
176
+ if (backup) log.info(`Backing up existing agent homes to ${backupDir}`);
177
+ let rc = 0;
178
+ for (const agent of agents) {
179
+ log.step(`Configure ${agent.name}`);
180
+ rc ||= await installStep(agent, { dryRun });
181
+ if (backup) backupAgentHome(agent, backupDir, { dryRun });
182
+ deployConfigs(agent, pkgRoot, { dryRun });
183
+ deployReference(agent, pkgRoot, { dryRun });
184
+ rc ||= await installPackagesStep(agent, { dryRun, tty });
185
+ rc ||= await installSkillsStep(agent, { dryRun, tty });
186
+ }
187
+ if (rc === 0) log.success("Setup complete");
188
+ return rc;
189
+ }
@@ -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
+ }
@@ -0,0 +1,40 @@
1
+ // exec.mjs — exec + detect helpers that honor --dry-run.
2
+ //
3
+ // runCommand takes a single shell command string. In dry-run mode it prints
4
+ // "[dry] <cmd>" instead of spawning. In real mode it spawns with shell:true
5
+ // and inherited stdio, returning the exit code.
6
+
7
+ import { spawn, spawnSync } from "node:child_process";
8
+
9
+ /**
10
+ * Run a shell command. If dryRun is true, only print the command.
11
+ * Returns 0 on success, non-zero on failure. Resolves a Promise.
12
+ */
13
+ export function runCommand(cmdline, { dryRun = false, cwd } = {}) {
14
+ if (dryRun) {
15
+ console.log(`[dry] ${cmdline}`);
16
+ return Promise.resolve(0);
17
+ }
18
+ return new Promise((resolve) => {
19
+ const child = spawn(cmdline, { cwd, stdio: "inherit", shell: true });
20
+ child.on("close", (code) => resolve(code ?? 1));
21
+ child.on("error", () => resolve(1));
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Detect whether a binary is on PATH. Synchronous — used by the detect step
27
+ * to decide install-vs-skip. Returns boolean.
28
+ *
29
+ * The command is passed as a single string, NOT as an args array: args +
30
+ * shell:true triggers Node's DEP0190 DeprecationWarning, which is printed
31
+ * asynchronously — it lands inside the first interactive prompt that follows
32
+ * and shifts the cursor, breaking @inquirer's screen clearing (stale frames).
33
+ */
34
+ export function detectBinary(name) {
35
+ const result = spawnSync(`command -v ${name}`, {
36
+ shell: true,
37
+ stdio: "ignore",
38
+ });
39
+ return result.status === 0;
40
+ }
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
+ }
@@ -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
+ }