pi-var 1.0.1
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/.github/FUNDING.yml +4 -0
- package/.prettierrc +7 -0
- package/README.md +240 -0
- package/commitlint.config.cjs +3 -0
- package/extensions/pi-var/__tests__/integration/smoke.test.ts +308 -0
- package/extensions/pi-var/__tests__/unit/command.test.ts +322 -0
- package/extensions/pi-var/__tests__/unit/cow-detection.test.ts +340 -0
- package/extensions/pi-var/__tests__/unit/cow-git-merge.test.ts +404 -0
- package/extensions/pi-var/__tests__/unit/edr.test.ts +119 -0
- package/extensions/pi-var/__tests__/unit/environment.test.ts +369 -0
- package/extensions/pi-var/__tests__/unit/file-redirect.test.ts +295 -0
- package/extensions/pi-var/__tests__/unit/store.test.ts +489 -0
- package/extensions/pi-var/__tests__/unit/tools.test.ts +133 -0
- package/extensions/pi-var/__tests__/unit/variations.test.ts +352 -0
- package/extensions/pi-var/index.ts +111 -0
- package/extensions/pi-var/src/config.ts +30 -0
- package/extensions/pi-var/src/cow/clone.ts +46 -0
- package/extensions/pi-var/src/cow/detection.ts +215 -0
- package/extensions/pi-var/src/cow/index.ts +6 -0
- package/extensions/pi-var/src/edr/constants.ts +199 -0
- package/extensions/pi-var/src/edr/crowdstrike.ts +62 -0
- package/extensions/pi-var/src/edr/detection.ts +97 -0
- package/extensions/pi-var/src/edr/index.ts +6 -0
- package/extensions/pi-var/src/environment/copy-env.ts +79 -0
- package/extensions/pi-var/src/environment/detect.ts +91 -0
- package/extensions/pi-var/src/environment/index.ts +54 -0
- package/extensions/pi-var/src/environment/marker.ts +42 -0
- package/extensions/pi-var/src/environment/symlink.ts +82 -0
- package/extensions/pi-var/src/gitignore/manager.ts +77 -0
- package/extensions/pi-var/src/state/index.ts +7 -0
- package/extensions/pi-var/src/state/keys.ts +14 -0
- package/extensions/pi-var/src/state/store.ts +110 -0
- package/extensions/pi-var/src/state/types.ts +70 -0
- package/extensions/pi-var/src/tools/command/clean.ts +87 -0
- package/extensions/pi-var/src/tools/command/index.ts +95 -0
- package/extensions/pi-var/src/tools/command/status.ts +58 -0
- package/extensions/pi-var/src/tools/command/stop.ts +37 -0
- package/extensions/pi-var/src/tools/file-redirect.ts +126 -0
- package/extensions/pi-var/src/tools/redirect/bash.ts +75 -0
- package/extensions/pi-var/src/tools/redirect/edit.ts +60 -0
- package/extensions/pi-var/src/tools/redirect/index.ts +64 -0
- package/extensions/pi-var/src/tools/redirect/read.ts +61 -0
- package/extensions/pi-var/src/tools/redirect/write.ts +60 -0
- package/extensions/pi-var/src/tools/variation/create.ts +108 -0
- package/extensions/pi-var/src/tools/variation/merge.ts +80 -0
- package/extensions/pi-var/src/types/index.ts +139 -0
- package/extensions/pi-var/src/utils/names.ts +120 -0
- package/extensions/pi-var/src/variation/create-copy.ts +35 -0
- package/extensions/pi-var/src/variation/create-cow.ts +81 -0
- package/extensions/pi-var/src/variation/create-worktree.ts +31 -0
- package/extensions/pi-var/src/variation/create.ts +127 -0
- package/extensions/pi-var/src/variation/index.ts +9 -0
- package/extensions/pi-var/src/variation/merge/copy.ts +151 -0
- package/extensions/pi-var/src/variation/merge/git.ts +176 -0
- package/extensions/pi-var/src/variation/merge/index.ts +36 -0
- package/extensions/pi-var/src/variation/merge/rsync.ts +74 -0
- package/extensions/pi-var/src/variation/merge/strategy.ts +62 -0
- package/extensions/pi-var/src/variation/paths.ts +26 -0
- package/extensions/pi-var/src/variation/remove.ts +79 -0
- package/npm-shrinkwrap.json +6908 -0
- package/package.json +63 -0
- package/skills/variations/SKILL.md +182 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +19 -0
package/.prettierrc
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🌿 pi-var
|
|
4
|
+
|
|
5
|
+
**AI-driven copy-on-write variations for [pi](https://github.com/earendil-works/pi-coding-agent)**
|
|
6
|
+
|
|
7
|
+
_Work on multiple features simultaneously with isolated workspaces — automatically._
|
|
8
|
+
|
|
9
|
+
[](https://github.com/earendil-works/pi-coding-agent)
|
|
10
|
+
[](./LICENSE)
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Philosophy: The AI Does It
|
|
19
|
+
|
|
20
|
+
Traditional variation management requires you to:
|
|
21
|
+
|
|
22
|
+
- Name the variation
|
|
23
|
+
- Remember to switch contexts
|
|
24
|
+
- Manually merge and clean up
|
|
25
|
+
|
|
26
|
+
**pi-var** removes all of that. The AI detects when isolated work is needed and handles everything:
|
|
27
|
+
|
|
28
|
+
- Creates variations with semantic names
|
|
29
|
+
- Redirects all file operations automatically
|
|
30
|
+
- Suggests merge when work is complete
|
|
31
|
+
|
|
32
|
+
You just say what you want. The AI manages the workspace.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## How It Works
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
User: "Work on the new dashboard feature while I fix this bug in main"
|
|
40
|
+
│
|
|
41
|
+
▼
|
|
42
|
+
┌─────────────┐
|
|
43
|
+
│ pi-var │ ← AI decides isolation is needed
|
|
44
|
+
└──────┬──────┘
|
|
45
|
+
│
|
|
46
|
+
┌─────┴─────┐
|
|
47
|
+
▼ ▼
|
|
48
|
+
┌───────┐ ┌─────────┐
|
|
49
|
+
│ main │ │ 🌿 dash │ ← CoW clone (instant, zero copy)
|
|
50
|
+
│ (src) │ │ (src') │
|
|
51
|
+
└───┬───┘ └────┬────┘
|
|
52
|
+
│ │
|
|
53
|
+
│ bug fix │ dashboard feature
|
|
54
|
+
│ │
|
|
55
|
+
▼ ▼
|
|
56
|
+
merge ◄──────┘
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Auto-detected method** (fastest available):
|
|
60
|
+
|
|
61
|
+
1. **CoW** (APFS clonefile / Linux reflink) — fastest, instant
|
|
62
|
+
2. **Git worktree** — for git projects on non-CoW filesystems
|
|
63
|
+
3. **Full copy** — universal fallback
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pi install https://github.com/monotykamary/pi-var
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Or project-local:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pi install -l https://github.com/monotykamary/pi-var
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Usage
|
|
82
|
+
|
|
83
|
+
### Natural Language
|
|
84
|
+
|
|
85
|
+
Just describe what you need:
|
|
86
|
+
|
|
87
|
+
> "Work on the new dashboard feature while I fix this bug in main"
|
|
88
|
+
|
|
89
|
+
The AI automatically:
|
|
90
|
+
|
|
91
|
+
1. Creates a variation for the dashboard work
|
|
92
|
+
2. Redirects all operations to that variation
|
|
93
|
+
3. When you mention the bug fix, creates another variation
|
|
94
|
+
4. Switches between them transparently
|
|
95
|
+
5. Merges completed work back
|
|
96
|
+
|
|
97
|
+
### Dev Server Isolation
|
|
98
|
+
|
|
99
|
+
> "Run this experiment on a different port so my main dev server keeps running"
|
|
100
|
+
|
|
101
|
+
The AI:
|
|
102
|
+
|
|
103
|
+
1. Creates a variation
|
|
104
|
+
2. Allocates a unique port via `npx portless`
|
|
105
|
+
3. Runs the dev server
|
|
106
|
+
4. All without touching your main server
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## For Users: Manual Commands
|
|
111
|
+
|
|
112
|
+
If you need to intervene or check status:
|
|
113
|
+
|
|
114
|
+
| Command | Purpose |
|
|
115
|
+
| ---------------------- | ----------------------------------- |
|
|
116
|
+
| `/var` | Show status and active variation |
|
|
117
|
+
| `/var list` | List all variations |
|
|
118
|
+
| `/var clean <name>` | Delete a variation |
|
|
119
|
+
| `/var clean --stale 7` | Delete variations older than 7 days |
|
|
120
|
+
| `/var stop` | Return to source directory |
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## For the AI: Tools Reference
|
|
125
|
+
|
|
126
|
+
### create_variation
|
|
127
|
+
|
|
128
|
+
Create an isolated workspace automatically.
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
{
|
|
132
|
+
purpose: string; // "fix auth redirect bug"
|
|
133
|
+
type?: 'cow' | 'worktree' | 'copy'; // Auto-detected if omitted
|
|
134
|
+
createBranch?: boolean; // Create git branch for worktrees
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Auto-generated name:** Creates semantic names from `purpose` (e.g., `fix-auth-redirect-bug`).
|
|
139
|
+
|
|
140
|
+
**Port isolation:** For dev servers, use `npx portless` via bash:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
export PORT=$(npx portless --json | jq -r '.port')
|
|
144
|
+
npm run dev
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### merge_variation
|
|
148
|
+
|
|
149
|
+
Merge current variation back to source.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
{
|
|
153
|
+
dryRun?: boolean; // Preview changes
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Variations persist after merge.** They are never automatically deleted to protect against data loss. Use `/var clean` to remove old variations.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Environment Synchronization
|
|
162
|
+
|
|
163
|
+
**Copied files:** `.env`, `.envrc`, `.npmrc`, `.tool-versions`, `.node-version`
|
|
164
|
+
|
|
165
|
+
**Symlinked directories:** `node_modules`, `.next`, `.nuxt`, `target/`, `.venv/`
|
|
166
|
+
|
|
167
|
+
This saves gigabytes of disk space while ensuring each variation has isolated environment configuration.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Transparent Redirection
|
|
172
|
+
|
|
173
|
+
When a variation is active:
|
|
174
|
+
|
|
175
|
+
- `read` → reads from variation directory
|
|
176
|
+
- `edit` → modifies files in variation
|
|
177
|
+
- `write` → creates files in variation
|
|
178
|
+
- `bash` → executes in variation directory
|
|
179
|
+
- External paths (outside project) → accessed directly
|
|
180
|
+
|
|
181
|
+
The footer shows: `🌿 variation-name`
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Comparison
|
|
186
|
+
|
|
187
|
+
### vs Git Worktrees Alone
|
|
188
|
+
|
|
189
|
+
| Feature | Git Worktrees | pi-var |
|
|
190
|
+
| ---------------- | ------------------- | -------------------------- |
|
|
191
|
+
| Setup | Manual branching | Automatic |
|
|
192
|
+
| File redirection | Manual `cd` | Transparent |
|
|
193
|
+
| Environment sync | Manual copy/symlink | Automatic |
|
|
194
|
+
| Port conflicts | Manual management | AI handles via portless |
|
|
195
|
+
| Best method | Git only | CoW > worktree > copy |
|
|
196
|
+
| Cleanup | Manual git commands | AI-managed or `/var clean` |
|
|
197
|
+
|
|
198
|
+
### vs Docker/Dev Containers
|
|
199
|
+
|
|
200
|
+
- **pi-var:** Native filesystem performance, instant startup, same environment
|
|
201
|
+
- **Docker:** True OS isolation, image builds, slower startup
|
|
202
|
+
|
|
203
|
+
Use pi-var for parallel development work. Use Docker for different OS/toolchain environments.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Configuration
|
|
208
|
+
|
|
209
|
+
Create `.varconfig.yaml` in project root for custom sync rules:
|
|
210
|
+
|
|
211
|
+
```yaml
|
|
212
|
+
# Files to copy
|
|
213
|
+
copy:
|
|
214
|
+
- .env.local
|
|
215
|
+
- secrets.json
|
|
216
|
+
|
|
217
|
+
# Directories to symlink
|
|
218
|
+
symlink:
|
|
219
|
+
- node_modules
|
|
220
|
+
- .turbo
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Development
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# Install dependencies
|
|
229
|
+
npm install
|
|
230
|
+
|
|
231
|
+
# Run tests
|
|
232
|
+
npm test
|
|
233
|
+
|
|
234
|
+
# Type check
|
|
235
|
+
npm run typecheck
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
MIT
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration/smoke tests for pi-var extension
|
|
3
|
+
*
|
|
4
|
+
* These tests verify the end-to-end functionality of the /var command
|
|
5
|
+
* in a real (but isolated) environment.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest';
|
|
9
|
+
import { mkdir, rm, writeFile, readFile } from 'node:fs/promises';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { execSync } from 'node:child_process';
|
|
13
|
+
|
|
14
|
+
describe('pi-var smoke tests', () => {
|
|
15
|
+
const testDir = join(tmpdir(), `pi-var-test-${Date.now()}`);
|
|
16
|
+
let originalCwd: string;
|
|
17
|
+
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
originalCwd = process.cwd();
|
|
20
|
+
|
|
21
|
+
// Create test project structure
|
|
22
|
+
await mkdir(testDir, { recursive: true });
|
|
23
|
+
await mkdir(join(testDir, 'src'), { recursive: true });
|
|
24
|
+
|
|
25
|
+
// Create a simple package.json
|
|
26
|
+
await writeFile(
|
|
27
|
+
join(testDir, 'package.json'),
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
name: 'test-project',
|
|
30
|
+
version: '1.0.0',
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Create a source file
|
|
35
|
+
await writeFile(join(testDir, 'src', 'index.js'), 'console.log("Hello from main");');
|
|
36
|
+
|
|
37
|
+
// Initialize git repo for worktree tests
|
|
38
|
+
try {
|
|
39
|
+
execSync('git init', { cwd: testDir, stdio: 'ignore' });
|
|
40
|
+
execSync('git config user.email "test@test.com"', { cwd: testDir, stdio: 'ignore' });
|
|
41
|
+
execSync('git config user.name "Test User"', { cwd: testDir, stdio: 'ignore' });
|
|
42
|
+
execSync('git add .', { cwd: testDir, stdio: 'ignore' });
|
|
43
|
+
execSync('git commit -m "Initial commit"', { cwd: testDir, stdio: 'ignore' });
|
|
44
|
+
} catch {
|
|
45
|
+
// Git may not be available, skip git-related tests
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
process.chdir(testDir);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterAll(async () => {
|
|
52
|
+
process.chdir(originalCwd);
|
|
53
|
+
await rm(testDir, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('Extension loading', () => {
|
|
57
|
+
it('should have required files', async () => {
|
|
58
|
+
// Verify extension structure exists - use a path that works from test directory
|
|
59
|
+
const fs = await import('node:fs');
|
|
60
|
+
const { dirname } = await import('node:path');
|
|
61
|
+
const { fileURLToPath } = await import('node:url');
|
|
62
|
+
|
|
63
|
+
// Get the extension root from the current test file location
|
|
64
|
+
const testDir = dirname(fileURLToPath(import.meta.url));
|
|
65
|
+
const extensionPath = dirname(dirname(testDir)); // Up two levels from __tests__/integration
|
|
66
|
+
|
|
67
|
+
expect(fs.existsSync(join(extensionPath, 'index.ts'))).toBe(true);
|
|
68
|
+
expect(fs.existsSync(join(extensionPath, 'src', 'types', 'index.ts'))).toBe(true);
|
|
69
|
+
expect(fs.existsSync(join(extensionPath, 'src', 'tools', 'command', 'index.ts'))).toBe(true);
|
|
70
|
+
expect(fs.existsSync(join(extensionPath, 'src', 'state', 'store.ts'))).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('Type definitions', () => {
|
|
75
|
+
it('should be importable', async () => {
|
|
76
|
+
const types = await import('../../src/types/index.js');
|
|
77
|
+
// TypeScript types don't exist at runtime, just verify module loads
|
|
78
|
+
expect(types).toBeDefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('State management', () => {
|
|
83
|
+
it('should create runtime store', async () => {
|
|
84
|
+
const { createRuntimeStore, getSessionKey } = await import('../../src/state/index.js');
|
|
85
|
+
|
|
86
|
+
const store = createRuntimeStore();
|
|
87
|
+
expect(store).toHaveProperty('ensure');
|
|
88
|
+
expect(store).toHaveProperty('get');
|
|
89
|
+
expect(store).toHaveProperty('delete');
|
|
90
|
+
|
|
91
|
+
const mockContext = {
|
|
92
|
+
sessionManager: {
|
|
93
|
+
getSessionId: () => 'test-session-123',
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const key = getSessionKey(mockContext as any);
|
|
98
|
+
expect(key).toBe('test-session-123');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should manage runtime lifecycle', async () => {
|
|
102
|
+
const { createRuntimeStore } = await import('../../src/state/store.js');
|
|
103
|
+
|
|
104
|
+
const store = createRuntimeStore();
|
|
105
|
+
|
|
106
|
+
// Create runtime
|
|
107
|
+
const runtime = store.ensure('session-1');
|
|
108
|
+
expect(runtime.state.sessionId).toBe('session-1');
|
|
109
|
+
expect(runtime.state.activeVariationId).toBeNull();
|
|
110
|
+
expect(runtime.state.variations).toEqual([]);
|
|
111
|
+
|
|
112
|
+
// Modify and verify persistence
|
|
113
|
+
runtime.state.activeVariationId = 'test-variation';
|
|
114
|
+
|
|
115
|
+
const retrieved = store.get('session-1');
|
|
116
|
+
expect(retrieved?.state.activeVariationId).toBe('test-variation');
|
|
117
|
+
|
|
118
|
+
// Delete
|
|
119
|
+
const deleted = store.delete('session-1');
|
|
120
|
+
expect(deleted).toBe(true);
|
|
121
|
+
expect(store.get('session-1')).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('Name generation', () => {
|
|
126
|
+
it('should generate unique IDs', async () => {
|
|
127
|
+
const { generateVariationId } = await import('../../src/utils/names.js');
|
|
128
|
+
|
|
129
|
+
const id1 = generateVariationId();
|
|
130
|
+
const id2 = generateVariationId();
|
|
131
|
+
|
|
132
|
+
expect(id1).toBeDefined();
|
|
133
|
+
expect(id2).toBeDefined();
|
|
134
|
+
expect(id1).not.toBe(id2);
|
|
135
|
+
expect(id1.length).toBeGreaterThan(10);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should generate readable names', async () => {
|
|
139
|
+
const { generateVariationName } = await import('../../src/utils/names.js');
|
|
140
|
+
|
|
141
|
+
const name = generateVariationName();
|
|
142
|
+
|
|
143
|
+
expect(name).toBeDefined();
|
|
144
|
+
expect(name).toMatch(/^[a-z]+-[a-z]+$/);
|
|
145
|
+
expect(name.length).toBeGreaterThan(3);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should generate different names on multiple calls', async () => {
|
|
149
|
+
const { generateVariationName } = await import('../../src/utils/names.js');
|
|
150
|
+
|
|
151
|
+
const names = new Set();
|
|
152
|
+
for (let i = 0; i < 10; i++) {
|
|
153
|
+
names.add(generateVariationName());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// With 30 adjectives and 38 nouns, we should get variety
|
|
157
|
+
expect(names.size).toBeGreaterThan(5);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('Command registration', () => {
|
|
162
|
+
it('should export registerVarCommand', async () => {
|
|
163
|
+
const command = await import('../../src/tools/command/index.js');
|
|
164
|
+
|
|
165
|
+
expect(command).toHaveProperty('registerVarCommand');
|
|
166
|
+
expect(typeof command.registerVarCommand).toBe('function');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should accept ExtensionAPI and dependencies', async () => {
|
|
170
|
+
const { registerVarCommand } = await import('../../src/tools/command/index.js');
|
|
171
|
+
|
|
172
|
+
const mockPi = {
|
|
173
|
+
registerCommand: () => {},
|
|
174
|
+
on: () => {},
|
|
175
|
+
appendEntry: () => {},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const mockRuntime = {
|
|
179
|
+
state: { activeVariationId: null, variations: [], sessionId: 'test' },
|
|
180
|
+
|
|
181
|
+
lastPersisted: Date.now(),
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const mockRuntimeStore = {
|
|
185
|
+
ensure: () => mockRuntime,
|
|
186
|
+
get: () => mockRuntime,
|
|
187
|
+
delete: () => true,
|
|
188
|
+
persistState: () => {},
|
|
189
|
+
restoreState: () => {},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Should not throw
|
|
193
|
+
expect(() => {
|
|
194
|
+
registerVarCommand(mockPi as any, {
|
|
195
|
+
getRuntime: () => mockRuntime,
|
|
196
|
+
pi: mockPi as any,
|
|
197
|
+
runtimeStore: mockRuntimeStore as any,
|
|
198
|
+
});
|
|
199
|
+
}).not.toThrow();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('Extension integration', () => {
|
|
204
|
+
it('should have complete extension structure', async () => {
|
|
205
|
+
const extension = await import('../../index.js');
|
|
206
|
+
|
|
207
|
+
expect(extension).toHaveProperty('default');
|
|
208
|
+
expect(typeof extension.default).toBe('function');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should initialize with ExtensionAPI', async () => {
|
|
212
|
+
const extension = await import('../../index.js');
|
|
213
|
+
|
|
214
|
+
const calls: string[] = [];
|
|
215
|
+
const mockPi = {
|
|
216
|
+
registerCommand: (name: string) => calls.push(`registerCommand:${name}`),
|
|
217
|
+
registerTool: (tool: any) => calls.push(`registerTool:${tool.name || 'unnamed'}`),
|
|
218
|
+
on: (event: string) => calls.push(`on:${event}`),
|
|
219
|
+
exec: () => Promise.resolve({ stdout: '', stderr: '', exitCode: 0 }),
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Should not throw when initializing
|
|
223
|
+
expect(() => {
|
|
224
|
+
extension.default(mockPi as any);
|
|
225
|
+
}).not.toThrow();
|
|
226
|
+
|
|
227
|
+
// Should have registered the /var command
|
|
228
|
+
expect(calls.some((c) => c.includes('var'))).toBe(true);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('File operations integration', () => {
|
|
233
|
+
it('should create and read files in test directory', async () => {
|
|
234
|
+
const testFile = join(testDir, 'test-file.txt');
|
|
235
|
+
const content = 'Hello from test';
|
|
236
|
+
|
|
237
|
+
await writeFile(testFile, content);
|
|
238
|
+
const read = await readFile(testFile, 'utf-8');
|
|
239
|
+
|
|
240
|
+
expect(read).toBe(content);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should handle directory creation', async () => {
|
|
244
|
+
const nestedDir = join(testDir, 'nested', 'deep', 'dir');
|
|
245
|
+
|
|
246
|
+
await mkdir(nestedDir, { recursive: true });
|
|
247
|
+
|
|
248
|
+
const fs = await import('node:fs');
|
|
249
|
+
expect(fs.existsSync(nestedDir)).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('Portless integration (mocked)', () => {
|
|
254
|
+
it('should handle missing portless gracefully', async () => {
|
|
255
|
+
const { registerVarCommand } = await import('../../src/tools/command/index.js');
|
|
256
|
+
|
|
257
|
+
const mockRuntime = {
|
|
258
|
+
state: { activeVariationId: null, variations: [], sessionId: 'test' },
|
|
259
|
+
|
|
260
|
+
lastPersisted: Date.now(),
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
let handler: Function | undefined;
|
|
264
|
+
const mockPi = {
|
|
265
|
+
registerCommand: (name: string, config: any) => {
|
|
266
|
+
handler = config.handler;
|
|
267
|
+
},
|
|
268
|
+
on: () => {},
|
|
269
|
+
appendEntry: () => {},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const notifications: Array<{ msg: string; type: string }> = [];
|
|
273
|
+
const mockCtx = {
|
|
274
|
+
cwd: testDir,
|
|
275
|
+
ui: {
|
|
276
|
+
notify: (msg: string, type: string) => notifications.push({ msg, type }),
|
|
277
|
+
setStatus: () => {},
|
|
278
|
+
confirm: () => Promise.resolve(true),
|
|
279
|
+
select: () => Promise.resolve(null),
|
|
280
|
+
},
|
|
281
|
+
sessionManager: { getSessionId: () => 'test', getSessionFile: () => '' },
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
registerVarCommand(mockPi as any, {
|
|
285
|
+
getRuntime: () => mockRuntime,
|
|
286
|
+
pi: mockPi as any,
|
|
287
|
+
runtimeStore: {
|
|
288
|
+
ensure: () => mockRuntime,
|
|
289
|
+
get: () => mockRuntime,
|
|
290
|
+
delete: () => true,
|
|
291
|
+
persistState: () => {},
|
|
292
|
+
restoreState: () => {},
|
|
293
|
+
} as any,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Attempt to create variation with --isolated (portless won't be available)
|
|
297
|
+
// Should handle gracefully and not crash
|
|
298
|
+
try {
|
|
299
|
+
await handler?.('new test-isolated --isolated', mockCtx as any);
|
|
300
|
+
} catch {
|
|
301
|
+
// Expected to potentially fail without real portless
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Extension should still be functional
|
|
305
|
+
expect(mockRuntime.state.variations.length).toBeGreaterThanOrEqual(0);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|