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.
Files changed (64) hide show
  1. package/.github/FUNDING.yml +4 -0
  2. package/.prettierrc +7 -0
  3. package/README.md +240 -0
  4. package/commitlint.config.cjs +3 -0
  5. package/extensions/pi-var/__tests__/integration/smoke.test.ts +308 -0
  6. package/extensions/pi-var/__tests__/unit/command.test.ts +322 -0
  7. package/extensions/pi-var/__tests__/unit/cow-detection.test.ts +340 -0
  8. package/extensions/pi-var/__tests__/unit/cow-git-merge.test.ts +404 -0
  9. package/extensions/pi-var/__tests__/unit/edr.test.ts +119 -0
  10. package/extensions/pi-var/__tests__/unit/environment.test.ts +369 -0
  11. package/extensions/pi-var/__tests__/unit/file-redirect.test.ts +295 -0
  12. package/extensions/pi-var/__tests__/unit/store.test.ts +489 -0
  13. package/extensions/pi-var/__tests__/unit/tools.test.ts +133 -0
  14. package/extensions/pi-var/__tests__/unit/variations.test.ts +352 -0
  15. package/extensions/pi-var/index.ts +111 -0
  16. package/extensions/pi-var/src/config.ts +30 -0
  17. package/extensions/pi-var/src/cow/clone.ts +46 -0
  18. package/extensions/pi-var/src/cow/detection.ts +215 -0
  19. package/extensions/pi-var/src/cow/index.ts +6 -0
  20. package/extensions/pi-var/src/edr/constants.ts +199 -0
  21. package/extensions/pi-var/src/edr/crowdstrike.ts +62 -0
  22. package/extensions/pi-var/src/edr/detection.ts +97 -0
  23. package/extensions/pi-var/src/edr/index.ts +6 -0
  24. package/extensions/pi-var/src/environment/copy-env.ts +79 -0
  25. package/extensions/pi-var/src/environment/detect.ts +91 -0
  26. package/extensions/pi-var/src/environment/index.ts +54 -0
  27. package/extensions/pi-var/src/environment/marker.ts +42 -0
  28. package/extensions/pi-var/src/environment/symlink.ts +82 -0
  29. package/extensions/pi-var/src/gitignore/manager.ts +77 -0
  30. package/extensions/pi-var/src/state/index.ts +7 -0
  31. package/extensions/pi-var/src/state/keys.ts +14 -0
  32. package/extensions/pi-var/src/state/store.ts +110 -0
  33. package/extensions/pi-var/src/state/types.ts +70 -0
  34. package/extensions/pi-var/src/tools/command/clean.ts +87 -0
  35. package/extensions/pi-var/src/tools/command/index.ts +95 -0
  36. package/extensions/pi-var/src/tools/command/status.ts +58 -0
  37. package/extensions/pi-var/src/tools/command/stop.ts +37 -0
  38. package/extensions/pi-var/src/tools/file-redirect.ts +126 -0
  39. package/extensions/pi-var/src/tools/redirect/bash.ts +75 -0
  40. package/extensions/pi-var/src/tools/redirect/edit.ts +60 -0
  41. package/extensions/pi-var/src/tools/redirect/index.ts +64 -0
  42. package/extensions/pi-var/src/tools/redirect/read.ts +61 -0
  43. package/extensions/pi-var/src/tools/redirect/write.ts +60 -0
  44. package/extensions/pi-var/src/tools/variation/create.ts +108 -0
  45. package/extensions/pi-var/src/tools/variation/merge.ts +80 -0
  46. package/extensions/pi-var/src/types/index.ts +139 -0
  47. package/extensions/pi-var/src/utils/names.ts +120 -0
  48. package/extensions/pi-var/src/variation/create-copy.ts +35 -0
  49. package/extensions/pi-var/src/variation/create-cow.ts +81 -0
  50. package/extensions/pi-var/src/variation/create-worktree.ts +31 -0
  51. package/extensions/pi-var/src/variation/create.ts +127 -0
  52. package/extensions/pi-var/src/variation/index.ts +9 -0
  53. package/extensions/pi-var/src/variation/merge/copy.ts +151 -0
  54. package/extensions/pi-var/src/variation/merge/git.ts +176 -0
  55. package/extensions/pi-var/src/variation/merge/index.ts +36 -0
  56. package/extensions/pi-var/src/variation/merge/rsync.ts +74 -0
  57. package/extensions/pi-var/src/variation/merge/strategy.ts +62 -0
  58. package/extensions/pi-var/src/variation/paths.ts +26 -0
  59. package/extensions/pi-var/src/variation/remove.ts +79 -0
  60. package/npm-shrinkwrap.json +6908 -0
  61. package/package.json +63 -0
  62. package/skills/variations/SKILL.md +182 -0
  63. package/tsconfig.json +25 -0
  64. package/vitest.config.ts +19 -0
@@ -0,0 +1,352 @@
1
+ /**
2
+ * Unit tests for variation management utilities
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
6
+ import { promises as fs } from 'fs';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import crypto from 'crypto';
10
+ import {
11
+ detectCoWSupport,
12
+ hasGitRepo,
13
+ createVariation,
14
+ removeVariation,
15
+ mergeVariation,
16
+ } from '../../src/variation/index';
17
+ import type { Variation } from '../../src/types/index';
18
+
19
+ // Helper to create a temporary test directory
20
+ async function createTempDir(prefix: string): Promise<string> {
21
+ const tmpDir = os.tmpdir();
22
+ const randomId = crypto.randomBytes(4).toString('hex');
23
+ const dir = path.join(tmpDir, `${prefix}-${randomId}`);
24
+ await fs.mkdir(dir, { recursive: true });
25
+ return dir;
26
+ }
27
+
28
+ // Helper to cleanup a directory
29
+ async function cleanupDir(dir: string): Promise<void> {
30
+ try {
31
+ await fs.rm(dir, { recursive: true, force: true });
32
+ } catch {
33
+ // Ignore cleanup errors
34
+ }
35
+ }
36
+
37
+ describe('detectCoWSupport', () => {
38
+ let testDir: string;
39
+
40
+ beforeEach(async () => {
41
+ testDir = await createTempDir('cow-test');
42
+ });
43
+
44
+ afterEach(async () => {
45
+ await cleanupDir(testDir);
46
+ });
47
+
48
+ it('should return a result object with supported boolean', async () => {
49
+ const result = await detectCoWSupport(testDir);
50
+
51
+ expect(result).toHaveProperty('supported');
52
+ expect(typeof result.supported).toBe('boolean');
53
+ });
54
+
55
+ it('should return method when CoW is supported', async () => {
56
+ const result = await detectCoWSupport(testDir);
57
+
58
+ if (result.supported) {
59
+ expect(result.method).toBeDefined();
60
+ expect(['clonefile', 'reflink']).toContain(result.method);
61
+ }
62
+ });
63
+
64
+ it('should not leave test files behind', async () => {
65
+ const filesBefore = await fs.readdir(testDir).catch(() => []);
66
+
67
+ await detectCoWSupport(testDir);
68
+
69
+ const filesAfter = await fs.readdir(testDir).catch(() => []);
70
+
71
+ // Should only have files we created, no cow-test files
72
+ const cowTestFiles = filesAfter.filter(
73
+ (f) => f.includes('cow-test') || f.includes('cow-clone')
74
+ );
75
+ expect(cowTestFiles).toHaveLength(0);
76
+ });
77
+ });
78
+
79
+ describe('hasGitRepo', () => {
80
+ let testDir: string;
81
+
82
+ beforeEach(async () => {
83
+ testDir = await createTempDir('git-test');
84
+ });
85
+
86
+ afterEach(async () => {
87
+ await cleanupDir(testDir);
88
+ });
89
+
90
+ it('should return false for non-git directory', async () => {
91
+ const result = await hasGitRepo(testDir);
92
+ expect(result).toBe(false);
93
+ });
94
+
95
+ it('should return true for git directory', async () => {
96
+ // Initialize git repo
97
+ await fs.mkdir(path.join(testDir, '.git'), { recursive: true });
98
+ await fs.writeFile(path.join(testDir, '.git', 'HEAD'), 'ref: refs/heads/main\n');
99
+
100
+ const result = await hasGitRepo(testDir);
101
+ expect(result).toBe(true);
102
+ });
103
+
104
+ it('should return false for non-existent path', async () => {
105
+ const nonExistent = path.join(testDir, 'does-not-exist');
106
+ const result = await hasGitRepo(nonExistent);
107
+ expect(result).toBe(false);
108
+ });
109
+ });
110
+
111
+ describe('createVariation', () => {
112
+ let sourceDir: string;
113
+ let variationsBase: string;
114
+
115
+ beforeEach(async () => {
116
+ sourceDir = await createTempDir('source');
117
+
118
+ // Create some test files
119
+ await fs.writeFile(path.join(sourceDir, 'package.json'), JSON.stringify({ name: 'test' }));
120
+ await fs.mkdir(path.join(sourceDir, 'src'), { recursive: true });
121
+ await fs.writeFile(path.join(sourceDir, 'src', 'index'), 'console.log("hello");');
122
+
123
+ // Create .pi directory structure
124
+ variationsBase = path.join(sourceDir, '.pi', 'variations');
125
+ });
126
+
127
+ afterEach(async () => {
128
+ await cleanupDir(sourceDir);
129
+ });
130
+
131
+ it('should create a variation with auto-generated name', async () => {
132
+ const variation = await createVariation(sourceDir);
133
+
134
+ expect(variation).toHaveProperty('id');
135
+ expect(variation).toHaveProperty('name');
136
+ expect(variation).toHaveProperty('path');
137
+ expect(variation).toHaveProperty('sourcePath', sourceDir);
138
+ expect(variation).toHaveProperty('type');
139
+ expect(variation).toHaveProperty('createdAt');
140
+ expect(variation).toHaveProperty('lastAccessed');
141
+
142
+ // Verify the directory was created
143
+ const stats = await fs.stat(variation.path);
144
+ expect(stats.isDirectory()).toBe(true);
145
+ });
146
+
147
+ it('should create a variation with specified name', async () => {
148
+ const variation = await createVariation(sourceDir, { name: 'my-variation' });
149
+
150
+ expect(variation.name).toBe('my-variation');
151
+ expect(variation.path).toContain('my-variation');
152
+ });
153
+
154
+ it('should throw error if variation already exists', async () => {
155
+ await createVariation(sourceDir, { name: 'duplicate' });
156
+
157
+ await expect(createVariation(sourceDir, { name: 'duplicate' })).rejects.toThrow(
158
+ /already exists/
159
+ );
160
+ });
161
+
162
+ it('should create copy variation when type is explicitly set', async () => {
163
+ const variation = await createVariation(sourceDir, {
164
+ name: 'copy-test',
165
+ type: 'copy',
166
+ });
167
+
168
+ expect(variation.type).toBe('copy');
169
+
170
+ // Verify files were copied
171
+ const pkgJson = await fs.readFile(path.join(variation.path, 'package.json'), 'utf-8');
172
+ expect(JSON.parse(pkgJson).name).toBe('test');
173
+ });
174
+
175
+ it('should not copy nested .pi/variations directory', async () => {
176
+ // Create a pre-existing variation
177
+ await createVariation(sourceDir, { name: 'existing' });
178
+
179
+ // Create new variation - should not include the existing variation's .pi folder
180
+ const newVariation = await createVariation(sourceDir, {
181
+ name: 'new-var',
182
+ type: 'copy',
183
+ });
184
+
185
+ // The new variation should not have the existing variation inside it
186
+ const nestedVarPath = path.join(newVariation.path, '.pi', 'variations', 'existing');
187
+ await expect(fs.access(nestedVarPath)).rejects.toThrow();
188
+ });
189
+ });
190
+
191
+ describe('removeVariation', () => {
192
+ let sourceDir: string;
193
+
194
+ beforeEach(async () => {
195
+ sourceDir = await createTempDir('source');
196
+ await fs.writeFile(path.join(sourceDir, 'file.txt'), 'content');
197
+ });
198
+
199
+ afterEach(async () => {
200
+ await cleanupDir(sourceDir);
201
+ });
202
+
203
+ it('should remove a copy variation', async () => {
204
+ const variation = await createVariation(sourceDir, {
205
+ name: 'to-remove',
206
+ type: 'copy',
207
+ });
208
+
209
+ // Verify it exists
210
+ await fs.access(variation.path);
211
+
212
+ // Remove it
213
+ await removeVariation(variation);
214
+
215
+ // Verify it's gone
216
+ await expect(fs.access(variation.path)).rejects.toThrow();
217
+ });
218
+
219
+ it('should clean up empty parent directories', async () => {
220
+ const variation = await createVariation(sourceDir, {
221
+ name: 'cleanup-test',
222
+ type: 'copy',
223
+ });
224
+
225
+ const variationsDir = path.dirname(variation.path);
226
+ const piDir = path.dirname(variationsDir);
227
+
228
+ await removeVariation(variation);
229
+
230
+ // If this was the only variation, .pi/variations should be cleaned up
231
+ try {
232
+ await fs.access(variationsDir);
233
+ // If it still exists, that's fine - other variations may exist
234
+ } catch {
235
+ // Was cleaned up as expected
236
+ }
237
+ });
238
+ });
239
+
240
+ describe('mergeVariation', () => {
241
+ let sourceDir: string;
242
+ let variation: Variation;
243
+
244
+ beforeEach(async () => {
245
+ sourceDir = await createTempDir('source');
246
+
247
+ // Create source files
248
+ await fs.writeFile(path.join(sourceDir, 'file.txt'), 'original');
249
+ await fs.mkdir(path.join(sourceDir, 'src'), { recursive: true });
250
+ await fs.writeFile(path.join(sourceDir, 'src', 'app'), '// original');
251
+
252
+ // Create variation with changes
253
+ variation = await createVariation(sourceDir, {
254
+ name: 'to-merge',
255
+ type: 'copy',
256
+ });
257
+
258
+ // Modify files in variation
259
+ await fs.writeFile(path.join(variation.path, 'new-file.txt'), 'new content');
260
+ await fs.writeFile(path.join(variation.path, 'src', 'app'), '// modified');
261
+ });
262
+
263
+ afterEach(async () => {
264
+ await cleanupDir(sourceDir);
265
+ });
266
+
267
+ it('should merge new files to source', async () => {
268
+ // New file should not exist in source yet
269
+ await expect(fs.access(path.join(sourceDir, 'new-file.txt'))).rejects.toThrow();
270
+
271
+ await mergeVariation(variation, sourceDir);
272
+
273
+ // New file should now exist
274
+ const content = await fs.readFile(path.join(sourceDir, 'new-file.txt'), 'utf-8');
275
+ expect(content).toBe('new content');
276
+ });
277
+
278
+ it('should merge modified files to source', async () => {
279
+ const original = await fs.readFile(path.join(sourceDir, 'src', 'app'), 'utf-8');
280
+ expect(original).toBe('// original');
281
+
282
+ // Force copy strategy to avoid rsync issues in test environment
283
+ await mergeVariation(variation, sourceDir, { strategy: 'copy' });
284
+
285
+ const merged = await fs.readFile(path.join(sourceDir, 'src', 'app'), 'utf-8');
286
+ expect(merged).toBe('// modified');
287
+ });
288
+
289
+ it('should keep variation after merge', async () => {
290
+ const variationPath = variation.path;
291
+
292
+ await mergeVariation(variation, sourceDir);
293
+
294
+ // Variation should still exist
295
+ const stats = await fs.stat(variationPath);
296
+ expect(stats.isDirectory()).toBe(true);
297
+ });
298
+
299
+ it('should support dry run mode', async () => {
300
+ const variationPath = variation.path;
301
+
302
+ await mergeVariation(variation, sourceDir, { dryRun: true });
303
+
304
+ // Variation should still exist
305
+ const stats = await fs.stat(variationPath);
306
+ expect(stats.isDirectory()).toBe(true);
307
+
308
+ // But files should not have been merged
309
+ await expect(fs.access(path.join(sourceDir, 'new-file.txt'))).rejects.toThrow();
310
+ });
311
+ });
312
+
313
+ describe('variation type selection', () => {
314
+ let sourceDir: string;
315
+
316
+ beforeEach(async () => {
317
+ sourceDir = await createTempDir('source');
318
+ await fs.writeFile(path.join(sourceDir, 'file.txt'), 'content');
319
+ });
320
+
321
+ afterEach(async () => {
322
+ await cleanupDir(sourceDir);
323
+ });
324
+
325
+ it('should auto-select appropriate type', async () => {
326
+ const variation = await createVariation(sourceDir);
327
+
328
+ // Type should be one of the valid types
329
+ expect(['cow', 'worktree', 'copy']).toContain(variation.type);
330
+ });
331
+
332
+ it('should allow forcing specific type', async () => {
333
+ const copyVar = await createVariation(sourceDir, {
334
+ name: 'forced-copy',
335
+ type: 'copy',
336
+ });
337
+ expect(copyVar.type).toBe('copy');
338
+
339
+ // Create another source dir for next test
340
+ const anotherSource = await createTempDir('another-source');
341
+ await fs.writeFile(path.join(anotherSource, 'file.txt'), 'content');
342
+
343
+ const cowVar = await createVariation(anotherSource, {
344
+ name: 'forced-cow',
345
+ type: 'cow',
346
+ });
347
+ // May fall back to copy if CoW not supported
348
+ expect(['cow', 'copy']).toContain(cowVar.type);
349
+
350
+ await cleanupDir(anotherSource);
351
+ });
352
+ });
@@ -0,0 +1,111 @@
1
+ /**
2
+ * pi-var — Copy-on-write variations extension (autoregressive)
3
+ *
4
+ * AI-driven variation management:
5
+ * - create_variation tool — AI creates variations automatically
6
+ * - /var command — argument-free status and manual override
7
+ * - File redirection — transparent read/edit/write to active variation
8
+ * - Environment sync — copy .env, symlink node_modules
9
+ *
10
+ * Port isolation is handled by the AI via bash (portless), not extension code.
11
+ */
12
+
13
+ import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
14
+ import type { VarRuntime } from './src/types/index';
15
+ import { generateVariationName } from './src/utils/names';
16
+ import { getSessionKey, createRuntimeStore } from './src/state/index';
17
+ import { registerVarCommand } from './src/tools/command/index';
18
+ import { registerRedirectedTools } from './src/tools/redirect/index';
19
+ import { registerCreateVariationTool } from './src/tools/variation/create';
20
+ import { registerMergeVariationTool } from './src/tools/variation/merge';
21
+ import { detectVariationContext } from './src/environment/index';
22
+ import { DEFAULT_CONFIG } from './src/config';
23
+
24
+ export default function piVarExtension(pi: ExtensionAPI) {
25
+ // Session-scoped runtime store
26
+ const runtimeStore = createRuntimeStore();
27
+ const getRuntime = (ctx: ExtensionContext): VarRuntime => runtimeStore.ensure(getSessionKey(ctx));
28
+ const persistState = (ctx: ExtensionContext): void =>
29
+ runtimeStore.persistState(getSessionKey(ctx), pi);
30
+
31
+ // Register /var command (argument-free, status/overview)
32
+ registerVarCommand(pi, { getRuntime, pi, runtimeStore });
33
+
34
+ // Register redirected tools (read, edit, write, bash) - overrides built-ins when variation is active
35
+ registerRedirectedTools(pi, getRuntime);
36
+
37
+ // Register variation management tools
38
+ registerCreateVariationTool(pi, getRuntime);
39
+ registerMergeVariationTool(pi, getRuntime, persistState);
40
+
41
+ // Session lifecycle
42
+ pi.on('session_start', async (_event, ctx) => {
43
+ const sessionKey = getSessionKey(ctx);
44
+
45
+ // First ensure runtime exists, then restore from session
46
+ runtimeStore.ensure(sessionKey);
47
+ runtimeStore.restoreState(sessionKey, ctx.sessionManager);
48
+
49
+ const runtime = getRuntime(ctx);
50
+
51
+ // Detect if we're inside a variation directory
52
+ const context = detectVariationContext(ctx.cwd);
53
+ if (context.inVariation && context.variationId) {
54
+ // Reconnect to existing variation
55
+ const existing = runtime.state.variations.find((v) => v.id === context.variationId);
56
+ if (existing) {
57
+ runtime.state.activeVariationId = existing.id;
58
+ ctx.ui.notify(`Reconnected to variation: ${existing.name}`, 'info');
59
+ // Persist the reconnection
60
+ runtimeStore.persistState(sessionKey, pi);
61
+ }
62
+ }
63
+ });
64
+
65
+ // System prompt extension when in variation
66
+ pi.on('before_agent_start', async (event, ctx) => {
67
+ const runtime = getRuntime(ctx);
68
+ if (!runtime.state.activeVariationId) return;
69
+
70
+ const variation = runtime.state.variations.find(
71
+ (v) => v.id === runtime.state.activeVariationId
72
+ );
73
+ if (!variation) return;
74
+
75
+ const typeLabel =
76
+ variation.type === 'cow'
77
+ ? 'Copy-on-Write (APFS clonefile)'
78
+ : variation.type === 'worktree'
79
+ ? 'Git worktree'
80
+ : 'Full copy';
81
+
82
+ const mergeInfo = variation.branchName
83
+ ? '\n- Merge: Use merge_variation for git three-way merge (conflict detection enabled)'
84
+ : '';
85
+
86
+ return {
87
+ systemPrompt:
88
+ event.systemPrompt +
89
+ `
90
+
91
+ ## Active Variation: "${variation.name}"
92
+ You are working in an isolated variation. All operations automatically redirect:
93
+
94
+ **File Operations:** read, edit, write → variation directory (${variation.path})
95
+ **Shell Commands:** bash → executes in variation directory with cwd=${variation.path}
96
+ **Environment:** PI_VARIATION=${variation.name}, PI_VARIATION_PATH, PI_SOURCE_PATH set
97
+
98
+ - Source: ${variation.sourcePath}
99
+ - Variation: ${variation.path}
100
+ - Type: ${typeLabel}${mergeInfo}
101
+
102
+ Use tools normally - the extension handles all redirection. When finished, use merge_variation to merge back to source.
103
+ `,
104
+ };
105
+ });
106
+
107
+ // Session end - persist final state
108
+ pi.on('session_shutdown', async (_event, ctx) => {
109
+ runtimeStore.persistState(getSessionKey(ctx), pi);
110
+ });
111
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Extension configuration
3
+ */
4
+
5
+ export const DEFAULT_CONFIG = {
6
+ copy: [
7
+ '.env',
8
+ '.env.*',
9
+ '.envrc',
10
+ '.npmrc',
11
+ '.yarnrc',
12
+ '.yarnrc.yml',
13
+ '.tool-versions',
14
+ '.node-version',
15
+ '.python-version',
16
+ 'docker-compose.override.yml',
17
+ ],
18
+ symlink: [
19
+ 'node_modules',
20
+ '.next',
21
+ '.nuxt',
22
+ '.angular',
23
+ '.turbo',
24
+ 'target',
25
+ '.venv',
26
+ 'venv',
27
+ 'vendor',
28
+ ],
29
+ postCreate: [],
30
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Low-level CoW clone using platform-specific methods.
3
+ * This is the filesystem-level clone only — no git branch setup.
4
+ * For the full variation creation with git branch, use variation/create-cow.ts.
5
+ */
6
+
7
+ import * as os from 'os';
8
+ import * as path from 'path';
9
+ import { exec } from 'child_process';
10
+ import { promisify } from 'util';
11
+ import { promises as fs } from 'fs';
12
+ import { detectCoWSupport } from './detection';
13
+
14
+ const execAsync = promisify(exec);
15
+
16
+ /**
17
+ * Create a CoW clone using platform-specific methods.
18
+ * Copies the entire source directory (including .git if present)
19
+ * using copy-on-write for zero-additional-space clones.
20
+ */
21
+ export async function createCoWClone(sourcePath: string, destPath: string): Promise<void> {
22
+ const platform = os.platform();
23
+ const cowSupport = await detectCoWSupport(path.dirname(destPath));
24
+
25
+ if (!cowSupport.supported) {
26
+ throw new Error('CoW not supported on this platform/filesystem');
27
+ }
28
+
29
+ if (platform === 'darwin' && cowSupport.method === 'clonefile') {
30
+ // macOS: Use cp -c for clonefile
31
+ await execAsync(`cp -c -R "${sourcePath}" "${destPath}"`);
32
+ } else if (platform === 'linux' && cowSupport.method === 'reflink') {
33
+ // Linux: Use cp --reflink=auto
34
+ await execAsync(`cp -a --reflink=auto "${sourcePath}" "${destPath}"`);
35
+ } else {
36
+ throw new Error(`Unsupported CoW method: ${cowSupport.method}`);
37
+ }
38
+
39
+ // Remove the .pi/variations directory from the clone to avoid recursion
40
+ const nestedVariations = path.join(destPath, '.pi', 'variations');
41
+ try {
42
+ await fs.rm(nestedVariations, { recursive: true, force: true });
43
+ } catch {
44
+ // Ignore if doesn't exist
45
+ }
46
+ }