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 +21 -0
- package/README.md +56 -2
- package/bin/pi-setup.mjs +36 -0
- package/configs/pi/AGENTS.md +7 -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/packages.mjs +28 -0
- package/src/agents/pi.mjs +27 -0
- package/src/agents/skills.mjs +19 -0
- package/src/agents/types.mjs +54 -0
- package/src/commands/init.mjs +189 -0
- package/src/commands/select.mjs +51 -0
- package/src/lib/exec.mjs +40 -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,57 @@
|
|
|
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. **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
|
package/bin/pi-setup.mjs
ADDED
|
@@ -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,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": "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
|
+
}
|
package/src/lib/exec.mjs
ADDED
|
@@ -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
|
+
}
|
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
|
+
}
|