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
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* create_variation tool registration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
6
|
+
import { Type } from '@sinclair/typebox';
|
|
7
|
+
import type { VarRuntime } from '../../types/index';
|
|
8
|
+
import { createVariation } from '../../variation/create';
|
|
9
|
+
import { setupVariationEnvironment, DEFAULT_CONFIG } from '../../environment/index';
|
|
10
|
+
import { generateVariationName } from '../../utils/names';
|
|
11
|
+
|
|
12
|
+
export function registerCreateVariationTool(
|
|
13
|
+
pi: ExtensionAPI,
|
|
14
|
+
getRuntime: (ctx: ExtensionContext) => VarRuntime
|
|
15
|
+
): void {
|
|
16
|
+
pi.registerTool({
|
|
17
|
+
name: 'create_variation',
|
|
18
|
+
label: 'Create Variation',
|
|
19
|
+
description:
|
|
20
|
+
'Create a copy-on-write variation for isolated development work. ' +
|
|
21
|
+
'Auto-generates a semantic name from the purpose. ' +
|
|
22
|
+
'Auto-detects best method: CoW (APFS clonefile/Linux reflink) > Git worktree > Full copy. ' +
|
|
23
|
+
'All file operations automatically redirect to the variation. ' +
|
|
24
|
+
'For port isolation (dev servers), run `npx portless --json` via bash and set PORT from result.',
|
|
25
|
+
parameters: Type.Object({
|
|
26
|
+
purpose: Type.String({
|
|
27
|
+
description:
|
|
28
|
+
'Brief description of what this variation is for (e.g., "fix auth bug", "experiment with new UI"). ' +
|
|
29
|
+
'Used to auto-generate a semantic name.',
|
|
30
|
+
}),
|
|
31
|
+
type: Type.Optional(
|
|
32
|
+
Type.Union([Type.Literal('cow'), Type.Literal('worktree'), Type.Literal('copy')], {
|
|
33
|
+
description:
|
|
34
|
+
'Optional: Force specific creation method. If omitted, auto-detects best available.',
|
|
35
|
+
})
|
|
36
|
+
),
|
|
37
|
+
createBranch: Type.Optional(
|
|
38
|
+
Type.Boolean({
|
|
39
|
+
default: false,
|
|
40
|
+
description: 'For worktrees: create a git branch (var/<name>) for this variation.',
|
|
41
|
+
})
|
|
42
|
+
),
|
|
43
|
+
}),
|
|
44
|
+
execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => {
|
|
45
|
+
const text = (msg: string) => ({
|
|
46
|
+
content: [{ type: 'text' as const, text: msg }],
|
|
47
|
+
details: undefined,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const runtime = getRuntime(ctx);
|
|
51
|
+
|
|
52
|
+
// Auto-generate name from purpose
|
|
53
|
+
const semanticName = params.purpose
|
|
54
|
+
.toLowerCase()
|
|
55
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
56
|
+
.replace(/^-|-$/g, '')
|
|
57
|
+
.slice(0, 30);
|
|
58
|
+
const variationName = semanticName || generateVariationName();
|
|
59
|
+
|
|
60
|
+
// Check for existing
|
|
61
|
+
const existing = runtime.state.variations.find((v) => v.name === variationName);
|
|
62
|
+
if (existing) {
|
|
63
|
+
return text(
|
|
64
|
+
`Variation "${variationName}" already exists. ` +
|
|
65
|
+
`Switch to it with the variation context, or create with a different purpose.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// Create variation
|
|
71
|
+
const variation = await createVariation(ctx.cwd, {
|
|
72
|
+
name: variationName,
|
|
73
|
+
type: params.type,
|
|
74
|
+
createBranch: params.createBranch,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Setup environment
|
|
78
|
+
await setupVariationEnvironment(ctx.cwd, variation.path, DEFAULT_CONFIG);
|
|
79
|
+
|
|
80
|
+
// Add to runtime and activate
|
|
81
|
+
runtime.state.variations.push(variation);
|
|
82
|
+
runtime.state.activeVariationId = variation.id;
|
|
83
|
+
|
|
84
|
+
const typeLabel =
|
|
85
|
+
variation.type === 'cow'
|
|
86
|
+
? 'CoW clone'
|
|
87
|
+
: variation.type === 'worktree'
|
|
88
|
+
? 'Git worktree'
|
|
89
|
+
: 'Full copy';
|
|
90
|
+
|
|
91
|
+
const branchInfo = variation.branchName
|
|
92
|
+
? `\nBranch: ${variation.branchName}` +
|
|
93
|
+
(variation.mergeBaseCommit ? ` (base: ${variation.mergeBaseCommit.slice(0, 8)})` : '')
|
|
94
|
+
: '';
|
|
95
|
+
|
|
96
|
+
return text(
|
|
97
|
+
`Created ${typeLabel} variation "${variation.name}" for: ${params.purpose}\n` +
|
|
98
|
+
`Path: ${variation.path}\n\n` +
|
|
99
|
+
`All file operations now redirect to this variation.` +
|
|
100
|
+
branchInfo
|
|
101
|
+
);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
104
|
+
return text(`Failed to create variation: ${message}`);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* merge_variation tool registration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
6
|
+
import { Type } from '@sinclair/typebox';
|
|
7
|
+
import type { VarRuntime } from '../../types/index';
|
|
8
|
+
import { mergeVariation } from '../../variation/merge/index.js';
|
|
9
|
+
|
|
10
|
+
export function registerMergeVariationTool(
|
|
11
|
+
pi: ExtensionAPI,
|
|
12
|
+
getRuntime: (ctx: ExtensionContext) => VarRuntime,
|
|
13
|
+
persistState: (ctx: ExtensionContext) => void
|
|
14
|
+
): void {
|
|
15
|
+
pi.registerTool({
|
|
16
|
+
name: 'merge_variation',
|
|
17
|
+
label: 'Merge Variation',
|
|
18
|
+
description:
|
|
19
|
+
'Merge changes from the current variation back to the source directory. ' +
|
|
20
|
+
'Auto-selects merge strategy: git (for worktrees) > rsync > copy. ' +
|
|
21
|
+
'Variations are never deleted automatically. Use /var clean to remove old variations.',
|
|
22
|
+
parameters: Type.Object({
|
|
23
|
+
dryRun: Type.Optional(
|
|
24
|
+
Type.Boolean({
|
|
25
|
+
default: false,
|
|
26
|
+
description: 'Preview what would be merged without applying changes.',
|
|
27
|
+
})
|
|
28
|
+
),
|
|
29
|
+
}),
|
|
30
|
+
execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => {
|
|
31
|
+
const text = (msg: string) => ({
|
|
32
|
+
content: [{ type: 'text' as const, text: msg }],
|
|
33
|
+
details: undefined,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const runtime = getRuntime(ctx);
|
|
37
|
+
const activeId = runtime.state.activeVariationId;
|
|
38
|
+
|
|
39
|
+
if (!activeId) {
|
|
40
|
+
return text('No active variation to merge. Create one with create_variation first.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const variation = runtime.state.variations.find((v) => v.id === activeId);
|
|
44
|
+
if (!variation) {
|
|
45
|
+
return text('Active variation not found in state. This is a bug.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const dryRunOutput = await mergeVariation(variation, ctx.cwd, {
|
|
50
|
+
dryRun: params.dryRun,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Deactivate variation after successful merge (not dry-run)
|
|
54
|
+
if (!params.dryRun) {
|
|
55
|
+
runtime.state.activeVariationId = null;
|
|
56
|
+
ctx.ui.setStatus('pi-var', '');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Persist state to session
|
|
60
|
+
persistState(ctx);
|
|
61
|
+
|
|
62
|
+
const action = params.dryRun ? 'Would merge' : 'Merged';
|
|
63
|
+
const strategyUsed =
|
|
64
|
+
variation.type === 'cow' && variation.branchName
|
|
65
|
+
? ' (via git three-way merge)'
|
|
66
|
+
: variation.type === 'worktree'
|
|
67
|
+
? ' (via git merge)'
|
|
68
|
+
: '';
|
|
69
|
+
const output = dryRunOutput ? `\n\n${dryRunOutput}` : '';
|
|
70
|
+
const deactivated = !params.dryRun ? ' Now back in source directory.' : '';
|
|
71
|
+
return text(
|
|
72
|
+
`${action} variation "${variation.name}" to source${strategyUsed}.${deactivated}${output}`
|
|
73
|
+
);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
76
|
+
return text(`Failed to merge variation: ${message}`);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core type definitions for pi-var extension
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* EDR detection result
|
|
7
|
+
*/
|
|
8
|
+
export interface EDRDetectionResult {
|
|
9
|
+
/** Whether any EDR was detected */
|
|
10
|
+
detected: boolean;
|
|
11
|
+
/** List of detected EDR process names */
|
|
12
|
+
found: string[];
|
|
13
|
+
/** Detailed info about each detected EDR */
|
|
14
|
+
details: EDRDetails[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detailed information about a detected EDR
|
|
19
|
+
*/
|
|
20
|
+
export interface EDRDetails {
|
|
21
|
+
/** Process name */
|
|
22
|
+
process: string;
|
|
23
|
+
/** EDR vendor/product name */
|
|
24
|
+
product: string;
|
|
25
|
+
/** Whether this EDR is known to significantly impact CoW operations */
|
|
26
|
+
knownSlowCoW: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* CoW detection result with EDR awareness
|
|
31
|
+
*/
|
|
32
|
+
export interface CoWDetectionResult {
|
|
33
|
+
/** Whether CoW is supported on this platform/filesystem */
|
|
34
|
+
supported: boolean;
|
|
35
|
+
/** CoW method if supported */
|
|
36
|
+
method?: 'clonefile' | 'reflink';
|
|
37
|
+
/** EDR detection results */
|
|
38
|
+
edr?: {
|
|
39
|
+
/** Whether any EDR was detected */
|
|
40
|
+
detected: boolean;
|
|
41
|
+
/** List of detected EDR products */
|
|
42
|
+
products: string[];
|
|
43
|
+
/** Whether any detected EDR is known to slow CoW operations */
|
|
44
|
+
hasSlowCoWEDR: boolean;
|
|
45
|
+
};
|
|
46
|
+
/** Performance test results */
|
|
47
|
+
performance?: {
|
|
48
|
+
/** Whether CoW performed fast enough to be usable */
|
|
49
|
+
fast: boolean;
|
|
50
|
+
/** Average time in ms for CoW operations */
|
|
51
|
+
durationMs: number;
|
|
52
|
+
/** Number of timing samples collected */
|
|
53
|
+
samples?: number;
|
|
54
|
+
/** Maximum duration observed (helps detect spikes) */
|
|
55
|
+
maxDurationMs?: number;
|
|
56
|
+
/** Confidence level in the timing measurement */
|
|
57
|
+
confidence?: 'high' | 'medium' | 'low';
|
|
58
|
+
};
|
|
59
|
+
/** Recommended variation type based on CoW support and EDR presence */
|
|
60
|
+
recommendedType: 'cow' | 'worktree' | 'copy';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type VariationType = 'cow' | 'worktree' | 'copy';
|
|
64
|
+
|
|
65
|
+
export interface Variation {
|
|
66
|
+
/** Unique identifier */
|
|
67
|
+
id: string;
|
|
68
|
+
/** Human-readable name */
|
|
69
|
+
name: string;
|
|
70
|
+
/** Absolute path to variation directory */
|
|
71
|
+
path: string;
|
|
72
|
+
/** Absolute path to source directory */
|
|
73
|
+
sourcePath: string;
|
|
74
|
+
/** Creation method used */
|
|
75
|
+
type: VariationType;
|
|
76
|
+
/** ISO timestamp when created */
|
|
77
|
+
createdAt: string;
|
|
78
|
+
/** Last accessed timestamp */
|
|
79
|
+
lastAccessed: string;
|
|
80
|
+
/** Git branch name (for worktrees and git-backed CoW variations) */
|
|
81
|
+
branchName?: string;
|
|
82
|
+
/** The commit SHA the variation was branched from, used as merge base for three-way merge */
|
|
83
|
+
mergeBaseCommit?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface VarState {
|
|
87
|
+
/** Currently active variation ID, null if in source */
|
|
88
|
+
activeVariationId: string | null;
|
|
89
|
+
/** All variations for this session */
|
|
90
|
+
variations: Variation[];
|
|
91
|
+
/** Session ID for isolation */
|
|
92
|
+
sessionId: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface VarConfig {
|
|
96
|
+
/** Files to copy from source to variation */
|
|
97
|
+
copy: string[];
|
|
98
|
+
/** Directories to symlink (saves space) */
|
|
99
|
+
symlink: string[];
|
|
100
|
+
/** Commands to run after variation creation */
|
|
101
|
+
postCreate: string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface VarRuntime {
|
|
105
|
+
/** In-memory state (per-session) */
|
|
106
|
+
state: VarState;
|
|
107
|
+
/** Last time the state was persisted to JSONL */
|
|
108
|
+
lastPersisted: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface CreateVariationOptions {
|
|
112
|
+
/** Variation name (auto-generated if not provided) */
|
|
113
|
+
name?: string;
|
|
114
|
+
/** Force specific type */
|
|
115
|
+
type?: VariationType;
|
|
116
|
+
/** Create git branch */
|
|
117
|
+
createBranch?: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface MergeOptions {
|
|
121
|
+
/** Dry run - show what would change */
|
|
122
|
+
dryRun?: boolean;
|
|
123
|
+
/** Merge strategy: 'auto' | 'git' | 'rsync' | 'copy' */
|
|
124
|
+
strategy?: 'auto' | 'git' | 'rsync' | 'copy';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Context detection result */
|
|
128
|
+
export interface VariationContext {
|
|
129
|
+
/** True if currently inside a variation directory */
|
|
130
|
+
inVariation: boolean;
|
|
131
|
+
/** Variation ID (if in variation) */
|
|
132
|
+
variationId: string | null;
|
|
133
|
+
/** Variation name (if in variation) */
|
|
134
|
+
variationName: string | null;
|
|
135
|
+
/** Current variation path (if in variation) */
|
|
136
|
+
variationPath: string | null;
|
|
137
|
+
/** Source directory path */
|
|
138
|
+
sourcePath: string;
|
|
139
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Name generation utilities for variations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* List of adjectives for variation name generation
|
|
7
|
+
*/
|
|
8
|
+
const ADJECTIVES: readonly string[] = [
|
|
9
|
+
'swift',
|
|
10
|
+
'bright',
|
|
11
|
+
'calm',
|
|
12
|
+
'fierce',
|
|
13
|
+
'gentle',
|
|
14
|
+
'bold',
|
|
15
|
+
'quiet',
|
|
16
|
+
'wild',
|
|
17
|
+
'purple',
|
|
18
|
+
'crimson',
|
|
19
|
+
'azure',
|
|
20
|
+
'golden',
|
|
21
|
+
'silver',
|
|
22
|
+
'amber',
|
|
23
|
+
'emerald',
|
|
24
|
+
'brave',
|
|
25
|
+
'clever',
|
|
26
|
+
'happy',
|
|
27
|
+
'noble',
|
|
28
|
+
'proud',
|
|
29
|
+
'sunny',
|
|
30
|
+
'vivid',
|
|
31
|
+
'warm',
|
|
32
|
+
'cool',
|
|
33
|
+
'deep',
|
|
34
|
+
'high',
|
|
35
|
+
'light',
|
|
36
|
+
'rapid',
|
|
37
|
+
'smooth',
|
|
38
|
+
'strong',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* List of nouns for variation name generation
|
|
43
|
+
*/
|
|
44
|
+
const NOUNS: readonly string[] = [
|
|
45
|
+
'river',
|
|
46
|
+
'mountain',
|
|
47
|
+
'forest',
|
|
48
|
+
'ocean',
|
|
49
|
+
'meadow',
|
|
50
|
+
'valley',
|
|
51
|
+
'canyon',
|
|
52
|
+
'thunder',
|
|
53
|
+
'lightning',
|
|
54
|
+
'sunrise',
|
|
55
|
+
'sunset',
|
|
56
|
+
'storm',
|
|
57
|
+
'breeze',
|
|
58
|
+
'flame',
|
|
59
|
+
'octopus',
|
|
60
|
+
'falcon',
|
|
61
|
+
'tiger',
|
|
62
|
+
'wolf',
|
|
63
|
+
'eagle',
|
|
64
|
+
'dolphin',
|
|
65
|
+
'whale',
|
|
66
|
+
'bear',
|
|
67
|
+
'comet',
|
|
68
|
+
'galaxy',
|
|
69
|
+
'nebula',
|
|
70
|
+
'planet',
|
|
71
|
+
'star',
|
|
72
|
+
'moon',
|
|
73
|
+
'nova',
|
|
74
|
+
'quasar',
|
|
75
|
+
'shadow',
|
|
76
|
+
'echo',
|
|
77
|
+
'dream',
|
|
78
|
+
'spirit',
|
|
79
|
+
'phantom',
|
|
80
|
+
'mirage',
|
|
81
|
+
'legend',
|
|
82
|
+
'myth',
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate a unique variation ID
|
|
87
|
+
* Uses crypto.randomUUID() when available, falls back to timestamp + random
|
|
88
|
+
* @returns Unique identifier string
|
|
89
|
+
*/
|
|
90
|
+
export function generateVariationId(): string {
|
|
91
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
92
|
+
return crypto.randomUUID();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Fallback: timestamp + random hex string
|
|
96
|
+
const timestamp = Date.now().toString(36);
|
|
97
|
+
const random = Math.random().toString(36).substring(2, 10);
|
|
98
|
+
return `${timestamp}-${random}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Generate a random integer between min and max (inclusive)
|
|
103
|
+
* @param min - Minimum value
|
|
104
|
+
* @param max - Maximum value
|
|
105
|
+
* @returns Random integer in range [min, max]
|
|
106
|
+
*/
|
|
107
|
+
function randomInt(min: number, max: number): number {
|
|
108
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Generate a human-readable variation name
|
|
113
|
+
* Format: adjective-noun (e.g., "purple-octopus", "swift-river")
|
|
114
|
+
* @returns Generated variation name
|
|
115
|
+
*/
|
|
116
|
+
export function generateVariationName(): string {
|
|
117
|
+
const adjective = ADJECTIVES[randomInt(0, ADJECTIVES.length - 1)];
|
|
118
|
+
const noun = NOUNS[randomInt(0, NOUNS.length - 1)];
|
|
119
|
+
return `${adjective}-${noun}`;
|
|
120
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full copy variation creation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { promises as fs } from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a full copy variation
|
|
10
|
+
*/
|
|
11
|
+
export async function createCopyVariation(sourcePath: string, destPath: string): Promise<void> {
|
|
12
|
+
// Create destination directory first
|
|
13
|
+
await fs.mkdir(destPath, { recursive: true });
|
|
14
|
+
|
|
15
|
+
// Get all entries from source
|
|
16
|
+
const entries = await fs.readdir(sourcePath, { withFileTypes: true });
|
|
17
|
+
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const srcPath = path.join(sourcePath, entry.name);
|
|
20
|
+
const destFilePath = path.join(destPath, entry.name);
|
|
21
|
+
|
|
22
|
+
// Skip the .pi directory to avoid recursion
|
|
23
|
+
if (entry.name === '.pi') {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (entry.isDirectory()) {
|
|
28
|
+
// Recursively copy subdirectory
|
|
29
|
+
await createCopyVariation(srcPath, destFilePath);
|
|
30
|
+
} else {
|
|
31
|
+
// Copy file
|
|
32
|
+
await fs.copyFile(srcPath, destFilePath);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CoW variation creation with git branch initialization
|
|
3
|
+
*
|
|
4
|
+
* After the CoW clone (delegated to cow/clone.ts), if the source is a git repo,
|
|
5
|
+
* we create a branch inside the variation and commit any uncommitted changes
|
|
6
|
+
* from the source. This gives CoW variations a proper merge base for three-way
|
|
7
|
+
* git merge, solving the "overwrite previous merges" and "dirty base" problems.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { exec } from 'child_process';
|
|
12
|
+
import { promisify } from 'util';
|
|
13
|
+
import { promises as fs } from 'fs';
|
|
14
|
+
import { createCoWClone } from '../cow/clone';
|
|
15
|
+
|
|
16
|
+
const execAsync = promisify(exec);
|
|
17
|
+
|
|
18
|
+
/** Result of CoW variation creation with optional git metadata */
|
|
19
|
+
interface CoWCreationResult {
|
|
20
|
+
/** Git branch name, if variation was initialized in a git repo */
|
|
21
|
+
branchName?: string;
|
|
22
|
+
/** The commit SHA the branch was created from (merge base) */
|
|
23
|
+
mergeBaseCommit?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a CoW variation: clone the source, then initialize a git branch.
|
|
28
|
+
*
|
|
29
|
+
* The CoW clone is delegated to cow/clone.ts (platform-specific cp -c / --reflink).
|
|
30
|
+
* Then, if .git exists in the clone, we create a var/<name> branch and commit
|
|
31
|
+
* any uncommitted changes from the source as an initial commit.
|
|
32
|
+
*/
|
|
33
|
+
export async function createCoWVariation(
|
|
34
|
+
sourcePath: string,
|
|
35
|
+
destPath: string,
|
|
36
|
+
variationName: string
|
|
37
|
+
): Promise<CoWCreationResult> {
|
|
38
|
+
// 1. CoW clone (platform-specific, includes .git if present)
|
|
39
|
+
await createCoWClone(sourcePath, destPath);
|
|
40
|
+
|
|
41
|
+
// 2. Initialize git branch in the variation (if .git exists)
|
|
42
|
+
// This gives us proper merge semantics — the branch creates a named ref
|
|
43
|
+
// and the initial commit captures the source's working tree state at
|
|
44
|
+
// creation time (including any uncommitted changes).
|
|
45
|
+
const result: CoWCreationResult = {};
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const gitDir = path.join(destPath, '.git');
|
|
49
|
+
await fs.access(gitDir);
|
|
50
|
+
|
|
51
|
+
// Record the current HEAD commit as the merge base
|
|
52
|
+
// This is the commit BEFORE any variation-specific changes,
|
|
53
|
+
// so it serves as the common ancestor for three-way merge.
|
|
54
|
+
const { stdout: headCommit } = await execAsync(`git -C "${destPath}" rev-parse HEAD`);
|
|
55
|
+
result.mergeBaseCommit = headCommit.trim();
|
|
56
|
+
|
|
57
|
+
// Create a named branch in the variation's git repo
|
|
58
|
+
const branchName = `var/${variationName}`;
|
|
59
|
+
await execAsync(`git -C "${destPath}" checkout -b "${branchName}"`);
|
|
60
|
+
result.branchName = branchName;
|
|
61
|
+
|
|
62
|
+
// Capture any uncommitted changes from the source as an initial commit.
|
|
63
|
+
// This records "what the source looked like" at creation time —
|
|
64
|
+
// including dirty working tree files — so that the merge base is
|
|
65
|
+
// accurate even if the source had uncommitted changes.
|
|
66
|
+
try {
|
|
67
|
+
await execAsync(`git -C "${destPath}" add -A`);
|
|
68
|
+
await execAsync(
|
|
69
|
+
`git -C "${destPath}" commit -m "variation base: ${variationName}" --allow-empty`
|
|
70
|
+
);
|
|
71
|
+
} catch {
|
|
72
|
+
// Nothing to commit — source was clean. The branch already points
|
|
73
|
+
// to the right commit, so this is fine.
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// No .git directory — source is not a git repo.
|
|
77
|
+
// Skip git branch setup; merge will fall back to rsync/copy.
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git worktree variation creation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a git worktree variation
|
|
12
|
+
*/
|
|
13
|
+
export async function createWorktreeVariation(
|
|
14
|
+
sourcePath: string,
|
|
15
|
+
destPath: string,
|
|
16
|
+
name: string,
|
|
17
|
+
createBranch?: boolean
|
|
18
|
+
): Promise<string | undefined> {
|
|
19
|
+
const branchName = createBranch ? `var/${name}` : undefined;
|
|
20
|
+
|
|
21
|
+
if (branchName) {
|
|
22
|
+
// Create new branch and worktree
|
|
23
|
+
await execAsync(`git -C "${sourcePath}" branch "${branchName}"`);
|
|
24
|
+
await execAsync(`git -C "${sourcePath}" worktree add "${destPath}" "${branchName}"`);
|
|
25
|
+
} else {
|
|
26
|
+
// Add worktree from current HEAD (detached)
|
|
27
|
+
await execAsync(`git -C "${sourcePath}" worktree add --detach "${destPath}"`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return branchName;
|
|
31
|
+
}
|