@codeledger/harness 0.1.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.
@@ -0,0 +1,171 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, mkdirSync, readdirSync, rmSync, symlinkSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { validateGitRef } from '@codeledger/instrument';
6
+ const SAFE_PREFIX = /^[a-zA-Z0-9_-]+$/;
7
+ export function createWorktree(spec) {
8
+ // Validate inputs — no shell interpolation needed with execFileSync
9
+ validateGitRef(spec.commit);
10
+ if (spec.prefix && !SAFE_PREFIX.test(spec.prefix)) {
11
+ throw new Error(`Invalid worktree prefix: "${spec.prefix}" contains disallowed characters`);
12
+ }
13
+ const id = randomUUID().slice(0, 8);
14
+ const branchName = `codeledger-sandbox-${spec.prefix ?? 'run'}-${id}`;
15
+ const worktreePath = join(spec.worktreeRoot, branchName);
16
+ if (!existsSync(spec.worktreeRoot)) {
17
+ mkdirSync(spec.worktreeRoot, { recursive: true });
18
+ }
19
+ // Create detached worktree using argument-array form (no shell)
20
+ execFileSync('git', ['worktree', 'add', '--detach', worktreePath, '--', spec.commit], {
21
+ cwd: spec.repoRoot,
22
+ stdio: ['pipe', 'pipe', 'pipe'],
23
+ });
24
+ // Provision dependencies based on configured mode
25
+ const depMode = spec.dependencyMode ?? 'symlink';
26
+ provisionDependencies(depMode, spec.repoRoot, worktreePath, spec.installCmd);
27
+ const cleanup = async () => {
28
+ try {
29
+ execFileSync('git', ['worktree', 'remove', '--force', worktreePath], {
30
+ cwd: spec.repoRoot,
31
+ stdio: ['pipe', 'pipe', 'pipe'],
32
+ });
33
+ }
34
+ catch {
35
+ // Best effort cleanup
36
+ }
37
+ };
38
+ return {
39
+ path: worktreePath,
40
+ branch: branchName,
41
+ commit: spec.commit,
42
+ cleanup,
43
+ };
44
+ }
45
+ function provisionDependencies(mode, repoRoot, worktreePath, installCmd) {
46
+ if (mode === 'none')
47
+ return;
48
+ if (mode === 'install') {
49
+ const cmd = installCmd ?? (existsSync(join(repoRoot, 'pnpm-lock.yaml')) ? 'pnpm install --frozen-lockfile' : 'npm ci');
50
+ const [bin, ...args] = cmd.split(' ');
51
+ execFileSync(bin, args, {
52
+ cwd: worktreePath,
53
+ stdio: ['pipe', 'pipe', 'pipe'],
54
+ });
55
+ return;
56
+ }
57
+ // Default: symlink
58
+ mirrorDependencyInstall(repoRoot, worktreePath);
59
+ }
60
+ function mirrorDependencyInstall(repoRoot, worktreePath) {
61
+ linkDependencyDir(join(repoRoot, 'node_modules'), join(worktreePath, 'node_modules'));
62
+ const repoPackagesRoot = join(repoRoot, 'packages');
63
+ const worktreePackagesRoot = join(worktreePath, 'packages');
64
+ if (!existsSync(repoPackagesRoot))
65
+ return;
66
+ for (const entry of readdirSync(repoPackagesRoot, { withFileTypes: true })) {
67
+ if (!entry.isDirectory())
68
+ continue;
69
+ const name = entry.name;
70
+ linkDependencyDir(join(repoPackagesRoot, name, 'node_modules'), join(worktreePackagesRoot, name, 'node_modules'));
71
+ }
72
+ }
73
+ function linkDependencyDir(from, to) {
74
+ if (!existsSync(from) || existsSync(to))
75
+ return;
76
+ try {
77
+ symlinkSync(from, to, 'junction');
78
+ }
79
+ catch {
80
+ // Non-fatal: acceptance commands may still run if they don't need dependencies.
81
+ }
82
+ }
83
+ /**
84
+ * Remove all orphaned codeledger sandbox worktrees.
85
+ *
86
+ * 1. Runs `git worktree prune` to clean stale bookkeeping entries.
87
+ * 2. Lists remaining worktrees via `git worktree list --porcelain`.
88
+ * 3. Removes any whose path contains `codeledger-sandbox-`.
89
+ * 4. Deletes leftover sandbox directories under `worktreeRoot` that git
90
+ * no longer tracks (e.g. from a hard kill where the entry was never
91
+ * registered or was already pruned but the directory survived).
92
+ *
93
+ * Returns the number of worktrees removed.
94
+ */
95
+ export function cleanupAllWorktrees(repoRoot, worktreeRoot) {
96
+ let removed = 0;
97
+ // Step 1: prune stale bookkeeping
98
+ try {
99
+ execFileSync('git', ['worktree', 'prune'], {
100
+ cwd: repoRoot,
101
+ stdio: ['pipe', 'pipe', 'pipe'],
102
+ });
103
+ }
104
+ catch {
105
+ // git worktree prune failure is non-fatal
106
+ }
107
+ // Step 2: list registered worktrees and remove codeledger sandboxes
108
+ const registeredPaths = new Set();
109
+ try {
110
+ const listOutput = execFileSync('git', ['worktree', 'list', '--porcelain'], { cwd: repoRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
111
+ for (const line of listOutput.split('\n')) {
112
+ if (!line.startsWith('worktree '))
113
+ continue;
114
+ const wtPath = resolve(line.slice('worktree '.length).trim());
115
+ registeredPaths.add(wtPath);
116
+ if (!wtPath.includes('codeledger-sandbox-'))
117
+ continue;
118
+ try {
119
+ execFileSync('git', ['worktree', 'remove', '--force', wtPath], {
120
+ cwd: repoRoot,
121
+ stdio: ['pipe', 'pipe', 'pipe'],
122
+ });
123
+ removed++;
124
+ }
125
+ catch {
126
+ // If git remove fails, try manual directory cleanup + re-prune
127
+ try {
128
+ rmSync(wtPath, { recursive: true, force: true });
129
+ execFileSync('git', ['worktree', 'prune'], {
130
+ cwd: repoRoot,
131
+ stdio: ['pipe', 'pipe', 'pipe'],
132
+ });
133
+ removed++;
134
+ }
135
+ catch {
136
+ // Best effort
137
+ }
138
+ }
139
+ }
140
+ }
141
+ catch {
142
+ // git worktree list failure — fall through to directory scan
143
+ }
144
+ // Step 3: remove leftover sandbox directories that git no longer tracks
145
+ const root = worktreeRoot ?? join(repoRoot, '.codeledger', 'worktrees');
146
+ if (existsSync(root)) {
147
+ try {
148
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
149
+ if (!entry.isDirectory())
150
+ continue;
151
+ if (!entry.name.startsWith('codeledger-sandbox-'))
152
+ continue;
153
+ const dirPath = resolve(root, entry.name);
154
+ if (registeredPaths.has(dirPath))
155
+ continue; // already handled above
156
+ try {
157
+ rmSync(dirPath, { recursive: true, force: true });
158
+ removed++;
159
+ }
160
+ catch {
161
+ // Best effort
162
+ }
163
+ }
164
+ }
165
+ catch {
166
+ // readdir failure is non-fatal
167
+ }
168
+ }
169
+ return removed;
170
+ }
171
+ //# sourceMappingURL=worktree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree.js","sourceRoot":"","sources":["../src/worktree.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@codeledger/harness",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "description": "Benchmark execution and A/B comparison for CodeLedger",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/codeledgerECF/codeledger-blackbox.git",
10
+ "directory": "packages/harness"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ }
25
+ },
26
+ "dependencies": {
27
+ "@codeledger/types": "0.1.1",
28
+ "@codeledger/core": "0.1.1",
29
+ "@codeledger/core-engine": "0.1.1",
30
+ "@codeledger/instrument": "0.1.1"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^5.4.0"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "typecheck": "tsc --noEmit",
38
+ "clean": "rm -rf dist"
39
+ }
40
+ }