guardrail-core 1.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/dist/__tests__/autopilot.test.d.ts +7 -0
- package/dist/__tests__/autopilot.test.d.ts.map +1 -0
- package/dist/__tests__/autopilot.test.js +156 -0
- package/dist/__tests__/tier-config.test.d.ts +9 -0
- package/dist/__tests__/tier-config.test.d.ts.map +1 -0
- package/dist/__tests__/tier-config.test.js +230 -0
- package/dist/__tests__/utils/hash-inline.test.d.ts +2 -0
- package/dist/__tests__/utils/hash-inline.test.d.ts.map +1 -0
- package/dist/__tests__/utils/hash-inline.test.js +62 -0
- package/dist/__tests__/utils/hash.test.d.ts +3 -0
- package/dist/__tests__/utils/hash.test.d.ts.map +1 -0
- package/dist/__tests__/utils/hash.test.js +95 -0
- package/dist/__tests__/utils/simple.test.d.ts +1 -0
- package/dist/__tests__/utils/simple.test.d.ts.map +1 -0
- package/dist/__tests__/utils/simple.test.js +10 -0
- package/dist/__tests__/utils/utils-simple.test.d.ts +1 -0
- package/dist/__tests__/utils/utils-simple.test.d.ts.map +1 -0
- package/dist/__tests__/utils/utils-simple.test.js +6 -0
- package/dist/__tests__/utils/utils.test.d.ts +15 -0
- package/dist/__tests__/utils/utils.test.d.ts.map +1 -0
- package/dist/__tests__/utils/utils.test.js +172 -0
- package/dist/autopilot/autopilot-runner.d.ts +33 -0
- package/dist/autopilot/autopilot-runner.d.ts.map +1 -0
- package/dist/autopilot/autopilot-runner.js +479 -0
- package/dist/autopilot/index.d.ts +6 -0
- package/dist/autopilot/index.d.ts.map +1 -0
- package/dist/autopilot/index.js +25 -0
- package/dist/autopilot/types.d.ts +102 -0
- package/dist/autopilot/types.d.ts.map +1 -0
- package/dist/autopilot/types.js +18 -0
- package/dist/cache/index.d.ts +7 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +22 -0
- package/dist/cache/redis-cache.d.ts +145 -0
- package/dist/cache/redis-cache.d.ts.map +1 -0
- package/dist/cache/redis-cache.js +459 -0
- package/dist/ci/github-actions.d.ts +77 -0
- package/dist/ci/github-actions.d.ts.map +1 -0
- package/dist/ci/github-actions.js +277 -0
- package/dist/ci/index.d.ts +12 -0
- package/dist/ci/index.d.ts.map +1 -0
- package/dist/ci/index.js +27 -0
- package/dist/ci/pre-commit.d.ts +65 -0
- package/dist/ci/pre-commit.d.ts.map +1 -0
- package/dist/ci/pre-commit.js +286 -0
- package/dist/entitlements.d.ts +149 -0
- package/dist/entitlements.d.ts.map +1 -0
- package/dist/entitlements.js +464 -0
- package/dist/env.d.ts +113 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +204 -0
- package/dist/fix-packs/__tests__/generate-fix-packs.test.d.ts +7 -0
- package/dist/fix-packs/__tests__/generate-fix-packs.test.d.ts.map +1 -0
- package/dist/fix-packs/__tests__/generate-fix-packs.test.js +250 -0
- package/dist/fix-packs/generate-fix-packs.d.ts +15 -0
- package/dist/fix-packs/generate-fix-packs.d.ts.map +1 -0
- package/dist/fix-packs/generate-fix-packs.js +505 -0
- package/dist/fix-packs/index.d.ts +8 -0
- package/dist/fix-packs/index.d.ts.map +1 -0
- package/dist/fix-packs/index.js +23 -0
- package/dist/fix-packs/types.d.ts +113 -0
- package/dist/fix-packs/types.d.ts.map +1 -0
- package/dist/fix-packs/types.js +71 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/metrics/prometheus.d.ts +99 -0
- package/dist/metrics/prometheus.d.ts.map +1 -0
- package/dist/metrics/prometheus.js +306 -0
- package/dist/quota-ledger.d.ts +119 -0
- package/dist/quota-ledger.d.ts.map +1 -0
- package/dist/quota-ledger.js +462 -0
- package/dist/rbac/__tests__/permissions.test.d.ts +8 -0
- package/dist/rbac/__tests__/permissions.test.d.ts.map +1 -0
- package/dist/rbac/__tests__/permissions.test.js +350 -0
- package/dist/rbac/index.d.ts +9 -0
- package/dist/rbac/index.d.ts.map +1 -0
- package/dist/rbac/index.js +32 -0
- package/dist/rbac/permissions.d.ts +71 -0
- package/dist/rbac/permissions.d.ts.map +1 -0
- package/dist/rbac/permissions.js +247 -0
- package/dist/rbac/types.d.ts +69 -0
- package/dist/rbac/types.d.ts.map +1 -0
- package/dist/rbac/types.js +213 -0
- package/dist/tier-config.d.ts +203 -0
- package/dist/tier-config.d.ts.map +1 -0
- package/dist/tier-config.js +675 -0
- package/dist/types.d.ts +365 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/utils.d.ts +36 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +127 -0
- package/dist/verified-autofix/__tests__/format-validator.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/format-validator.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/format-validator.test.js +285 -0
- package/dist/verified-autofix/__tests__/pipeline.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/pipeline.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/pipeline.test.js +389 -0
- package/dist/verified-autofix/__tests__/repo-fingerprint.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/repo-fingerprint.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/repo-fingerprint.test.js +236 -0
- package/dist/verified-autofix/__tests__/workspace.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/workspace.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/workspace.test.js +314 -0
- package/dist/verified-autofix/format-validator.d.ts +101 -0
- package/dist/verified-autofix/format-validator.d.ts.map +1 -0
- package/dist/verified-autofix/format-validator.js +446 -0
- package/dist/verified-autofix/index.d.ts +14 -0
- package/dist/verified-autofix/index.d.ts.map +1 -0
- package/dist/verified-autofix/index.js +39 -0
- package/dist/verified-autofix/pipeline.d.ts +68 -0
- package/dist/verified-autofix/pipeline.d.ts.map +1 -0
- package/dist/verified-autofix/pipeline.js +330 -0
- package/dist/verified-autofix/repo-fingerprint.d.ts +56 -0
- package/dist/verified-autofix/repo-fingerprint.d.ts.map +1 -0
- package/dist/verified-autofix/repo-fingerprint.js +396 -0
- package/dist/verified-autofix/workspace.d.ts +83 -0
- package/dist/verified-autofix/workspace.d.ts.map +1 -0
- package/dist/verified-autofix/workspace.js +454 -0
- package/dist/verified-autofix.d.ts +182 -0
- package/dist/verified-autofix.d.ts.map +1 -0
- package/dist/verified-autofix.js +1021 -0
- package/dist/visualization/dependency-graph.d.ts +79 -0
- package/dist/visualization/dependency-graph.d.ts.map +1 -0
- package/dist/visualization/dependency-graph.js +399 -0
- package/dist/visualization/index.d.ts +5 -0
- package/dist/visualization/index.d.ts.map +1 -0
- package/dist/visualization/index.js +20 -0
- package/package.json +29 -0
- package/src/__tests__/autopilot.test.ts +196 -0
- package/src/__tests__/tier-config.test.ts +289 -0
- package/src/__tests__/utils/hash-inline.test.ts +76 -0
- package/src/__tests__/utils/hash.test.ts +119 -0
- package/src/__tests__/utils/simple.test.ts +10 -0
- package/src/__tests__/utils/utils-simple.test.ts +5 -0
- package/src/__tests__/utils/utils.test.ts +203 -0
- package/src/autopilot/autopilot-runner.ts +503 -0
- package/src/autopilot/index.ts +6 -0
- package/src/autopilot/types.ts +119 -0
- package/src/cache/index.ts +7 -0
- package/src/cache/redis-cache.d.ts +155 -0
- package/src/cache/redis-cache.d.ts.map +1 -0
- package/src/cache/redis-cache.ts +517 -0
- package/src/ci/github-actions.ts +335 -0
- package/src/ci/index.ts +12 -0
- package/src/ci/pre-commit.ts +338 -0
- package/src/db/usage-schema.prisma +114 -0
- package/src/entitlements.ts +570 -0
- package/src/env.d.ts +68 -0
- package/src/env.d.ts.map +1 -0
- package/src/env.ts +247 -0
- package/src/fix-packs/__tests__/generate-fix-packs.test.ts +317 -0
- package/src/fix-packs/generate-fix-packs.ts +577 -0
- package/src/fix-packs/index.ts +8 -0
- package/src/fix-packs/types.ts +206 -0
- package/src/index.d.ts +7 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.ts +12 -0
- package/src/metrics/prometheus.d.ts +104 -0
- package/src/metrics/prometheus.d.ts.map +1 -0
- package/src/metrics/prometheus.ts +446 -0
- package/src/quota-ledger.ts +548 -0
- package/src/rbac/__tests__/permissions.test.ts +446 -0
- package/src/rbac/index.ts +46 -0
- package/src/rbac/permissions.ts +301 -0
- package/src/rbac/types.ts +298 -0
- package/src/tier-config.json +157 -0
- package/src/tier-config.ts +815 -0
- package/src/types.d.ts +365 -0
- package/src/types.d.ts.map +1 -0
- package/src/types.ts +441 -0
- package/src/utils.d.ts +36 -0
- package/src/utils.d.ts.map +1 -0
- package/src/utils.ts +140 -0
- package/src/verified-autofix/__tests__/format-validator.test.ts +335 -0
- package/src/verified-autofix/__tests__/pipeline.test.ts +419 -0
- package/src/verified-autofix/__tests__/repo-fingerprint.test.ts +241 -0
- package/src/verified-autofix/__tests__/workspace.test.ts +373 -0
- package/src/verified-autofix/format-validator.ts +517 -0
- package/src/verified-autofix/index.ts +63 -0
- package/src/verified-autofix/pipeline.ts +403 -0
- package/src/verified-autofix/repo-fingerprint.ts +459 -0
- package/src/verified-autofix/workspace.ts +531 -0
- package/src/verified-autofix.ts +1187 -0
- package/src/visualization/dependency-graph.d.ts +85 -0
- package/src/visualization/dependency-graph.d.ts.map +1 -0
- package/src/visualization/dependency-graph.ts +495 -0
- package/src/visualization/index.ts +5 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temp Workspace Manager - Isolated Verification Environment
|
|
3
|
+
*
|
|
4
|
+
* Creates isolated workspaces for testing patches:
|
|
5
|
+
* 1. Prefers git worktree when available
|
|
6
|
+
* 2. Falls back to directory copy
|
|
7
|
+
* 3. Applies diffs with git apply --check validation
|
|
8
|
+
* 4. Runs verification commands (typecheck, build, tests)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as crypto from 'crypto';
|
|
14
|
+
import { execSync, ExecSyncOptionsWithBufferEncoding } from 'child_process';
|
|
15
|
+
import type { ParsedHunk } from './format-validator';
|
|
16
|
+
import type { RepoFingerprint } from './repo-fingerprint';
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// TYPES
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
export interface WorkspaceOptions {
|
|
23
|
+
projectPath: string;
|
|
24
|
+
useWorktree?: boolean;
|
|
25
|
+
installDeps?: boolean;
|
|
26
|
+
timeout?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface WorkspaceInfo {
|
|
30
|
+
id: string;
|
|
31
|
+
path: string;
|
|
32
|
+
type: 'worktree' | 'copy';
|
|
33
|
+
projectPath: string;
|
|
34
|
+
createdAt: Date;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ApplyResult {
|
|
38
|
+
success: boolean;
|
|
39
|
+
applied: number;
|
|
40
|
+
failed: number;
|
|
41
|
+
errors: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface VerifyResult {
|
|
45
|
+
passed: boolean;
|
|
46
|
+
checks: CheckResult[];
|
|
47
|
+
duration: number;
|
|
48
|
+
failureContext: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CheckResult {
|
|
52
|
+
name: string;
|
|
53
|
+
command: string;
|
|
54
|
+
passed: boolean;
|
|
55
|
+
output: string;
|
|
56
|
+
duration: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// CONSTANTS
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
const WORKSPACE_BASE_DIR = path.join(require('os').tmpdir(), 'guardrail-verified-autofix');
|
|
64
|
+
const DEFAULT_TIMEOUT = 120000; // 2 minutes per command
|
|
65
|
+
const MAX_OUTPUT_LINES = 100;
|
|
66
|
+
|
|
67
|
+
const EXCLUDE_PATTERNS = [
|
|
68
|
+
'node_modules',
|
|
69
|
+
'.git',
|
|
70
|
+
'dist',
|
|
71
|
+
'build',
|
|
72
|
+
'.next',
|
|
73
|
+
'.nuxt',
|
|
74
|
+
'.output',
|
|
75
|
+
'__pycache__',
|
|
76
|
+
'.cache',
|
|
77
|
+
'coverage',
|
|
78
|
+
'.turbo',
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// WORKSPACE MANAGER CLASS
|
|
83
|
+
// ============================================================================
|
|
84
|
+
|
|
85
|
+
export class TempWorkspace {
|
|
86
|
+
private workspaces: Map<string, WorkspaceInfo> = new Map();
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create an isolated workspace for verification
|
|
90
|
+
*/
|
|
91
|
+
async create(options: WorkspaceOptions): Promise<WorkspaceInfo> {
|
|
92
|
+
const id = crypto.randomBytes(8).toString('hex');
|
|
93
|
+
const workspacePath = path.join(WORKSPACE_BASE_DIR, id);
|
|
94
|
+
|
|
95
|
+
await fs.promises.mkdir(workspacePath, { recursive: true });
|
|
96
|
+
|
|
97
|
+
let type: 'worktree' | 'copy' = 'copy';
|
|
98
|
+
|
|
99
|
+
// Try git worktree first (much faster, shares objects)
|
|
100
|
+
if (options.useWorktree !== false) {
|
|
101
|
+
const worktreeCreated = await this.tryCreateWorktree(
|
|
102
|
+
options.projectPath,
|
|
103
|
+
workspacePath
|
|
104
|
+
);
|
|
105
|
+
if (worktreeCreated) {
|
|
106
|
+
type = 'worktree';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Fall back to copy
|
|
111
|
+
if (type === 'copy') {
|
|
112
|
+
await this.copyProject(options.projectPath, workspacePath);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Install dependencies if requested
|
|
116
|
+
if (options.installDeps) {
|
|
117
|
+
await this.installDependencies(workspacePath, options.timeout);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const info: WorkspaceInfo = {
|
|
121
|
+
id,
|
|
122
|
+
path: workspacePath,
|
|
123
|
+
type,
|
|
124
|
+
projectPath: options.projectPath,
|
|
125
|
+
createdAt: new Date(),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
this.workspaces.set(id, info);
|
|
129
|
+
return info;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Apply a unified diff to the workspace
|
|
134
|
+
*/
|
|
135
|
+
async applyDiff(
|
|
136
|
+
workspacePath: string,
|
|
137
|
+
diff: string,
|
|
138
|
+
hunks: ParsedHunk[]
|
|
139
|
+
): Promise<ApplyResult> {
|
|
140
|
+
const errors: string[] = [];
|
|
141
|
+
let applied = 0;
|
|
142
|
+
let failed = 0;
|
|
143
|
+
|
|
144
|
+
// First, try git apply --check to validate
|
|
145
|
+
const gitCheckResult = await this.tryGitApply(workspacePath, diff, true);
|
|
146
|
+
|
|
147
|
+
if (gitCheckResult.success) {
|
|
148
|
+
// Git apply --check passed, do the real apply
|
|
149
|
+
const gitApplyResult = await this.tryGitApply(workspacePath, diff, false);
|
|
150
|
+
if (gitApplyResult.success) {
|
|
151
|
+
return { success: true, applied: hunks.length, failed: 0, errors: [] };
|
|
152
|
+
}
|
|
153
|
+
errors.push(`git apply failed: ${gitApplyResult.error}`);
|
|
154
|
+
} else {
|
|
155
|
+
errors.push(`git apply --check failed: ${gitCheckResult.error}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Fall back to manual hunk application
|
|
159
|
+
for (const hunk of hunks) {
|
|
160
|
+
try {
|
|
161
|
+
await this.applyHunk(workspacePath, hunk);
|
|
162
|
+
applied++;
|
|
163
|
+
} catch (e) {
|
|
164
|
+
failed++;
|
|
165
|
+
errors.push(`Failed to apply hunk to ${hunk.file}: ${(e as Error).message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
success: failed === 0,
|
|
171
|
+
applied,
|
|
172
|
+
failed,
|
|
173
|
+
errors,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Run verification checks in the workspace
|
|
179
|
+
*/
|
|
180
|
+
async verify(
|
|
181
|
+
workspacePath: string,
|
|
182
|
+
fingerprint: RepoFingerprint,
|
|
183
|
+
options?: { skipTests?: boolean; timeout?: number }
|
|
184
|
+
): Promise<VerifyResult> {
|
|
185
|
+
const startTime = Date.now();
|
|
186
|
+
const checks: CheckResult[] = [];
|
|
187
|
+
const timeout = options?.timeout || DEFAULT_TIMEOUT;
|
|
188
|
+
|
|
189
|
+
// Build verification command list based on fingerprint
|
|
190
|
+
const commands = this.getVerificationCommands(fingerprint, options?.skipTests);
|
|
191
|
+
|
|
192
|
+
for (const { name, command } of commands) {
|
|
193
|
+
const checkStart = Date.now();
|
|
194
|
+
let passed = false;
|
|
195
|
+
let output = '';
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const result = execSync(command, {
|
|
199
|
+
cwd: workspacePath,
|
|
200
|
+
encoding: 'utf8',
|
|
201
|
+
timeout,
|
|
202
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
203
|
+
env: {
|
|
204
|
+
...process.env,
|
|
205
|
+
CI: 'true',
|
|
206
|
+
NODE_ENV: 'test',
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
passed = true;
|
|
210
|
+
output = this.truncateOutput(result);
|
|
211
|
+
} catch (e) {
|
|
212
|
+
const err = e as { stdout?: string; stderr?: string; message: string };
|
|
213
|
+
output = this.truncateOutput(
|
|
214
|
+
(err.stderr || '') + '\n' + (err.stdout || '') || err.message
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
checks.push({
|
|
219
|
+
name,
|
|
220
|
+
command,
|
|
221
|
+
passed,
|
|
222
|
+
output,
|
|
223
|
+
duration: Date.now() - checkStart,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Stop on first failure for faster feedback
|
|
227
|
+
if (!passed) break;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Extract top 3 failure contexts
|
|
231
|
+
const failureContext = this.extractFailureContext(checks);
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
passed: checks.every(c => c.passed),
|
|
235
|
+
checks,
|
|
236
|
+
duration: Date.now() - startTime,
|
|
237
|
+
failureContext,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Copy changes back to the original project
|
|
243
|
+
*/
|
|
244
|
+
async copyBack(workspacePath: string, projectPath: string, files: string[]): Promise<void> {
|
|
245
|
+
for (const file of files) {
|
|
246
|
+
const src = path.join(workspacePath, file);
|
|
247
|
+
const dest = path.join(projectPath, file);
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
// Create backup
|
|
251
|
+
if (fs.existsSync(dest)) {
|
|
252
|
+
await fs.promises.copyFile(dest, `${dest}.guardrail-backup`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Copy new version
|
|
256
|
+
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
|
|
257
|
+
await fs.promises.copyFile(src, dest);
|
|
258
|
+
} catch (e) {
|
|
259
|
+
throw new Error(`Failed to copy ${file}: ${(e as Error).message}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Cleanup a workspace
|
|
266
|
+
*/
|
|
267
|
+
async cleanup(workspaceId: string): Promise<void> {
|
|
268
|
+
const info = this.workspaces.get(workspaceId);
|
|
269
|
+
if (!info) return;
|
|
270
|
+
|
|
271
|
+
// Remove git worktree if applicable
|
|
272
|
+
if (info.type === 'worktree') {
|
|
273
|
+
try {
|
|
274
|
+
execSync(`git worktree remove "${info.path}" --force`, {
|
|
275
|
+
cwd: info.projectPath,
|
|
276
|
+
stdio: 'pipe',
|
|
277
|
+
});
|
|
278
|
+
} catch {
|
|
279
|
+
// Fall through to rm
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Remove directory
|
|
284
|
+
try {
|
|
285
|
+
await fs.promises.rm(info.path, { recursive: true, force: true });
|
|
286
|
+
} catch {
|
|
287
|
+
// Ignore errors
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
this.workspaces.delete(workspaceId);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Cleanup all workspaces
|
|
295
|
+
*/
|
|
296
|
+
async cleanupAll(): Promise<void> {
|
|
297
|
+
for (const id of this.workspaces.keys()) {
|
|
298
|
+
await this.cleanup(id);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ==========================================================================
|
|
303
|
+
// PRIVATE METHODS
|
|
304
|
+
// ==========================================================================
|
|
305
|
+
|
|
306
|
+
private async tryCreateWorktree(
|
|
307
|
+
projectPath: string,
|
|
308
|
+
workspacePath: string
|
|
309
|
+
): Promise<boolean> {
|
|
310
|
+
try {
|
|
311
|
+
const gitDir = path.join(projectPath, '.git');
|
|
312
|
+
if (!fs.existsSync(gitDir)) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
execSync(`git worktree add "${workspacePath}" HEAD --detach`, {
|
|
317
|
+
cwd: projectPath,
|
|
318
|
+
stdio: 'pipe',
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return true;
|
|
322
|
+
} catch {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private async copyProject(src: string, dest: string): Promise<void> {
|
|
328
|
+
const entries = await fs.promises.readdir(src, { withFileTypes: true });
|
|
329
|
+
|
|
330
|
+
for (const entry of entries) {
|
|
331
|
+
if (EXCLUDE_PATTERNS.includes(entry.name)) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const srcPath = path.join(src, entry.name);
|
|
336
|
+
const destPath = path.join(dest, entry.name);
|
|
337
|
+
|
|
338
|
+
if (entry.isDirectory()) {
|
|
339
|
+
await fs.promises.mkdir(destPath, { recursive: true });
|
|
340
|
+
await this.copyProject(srcPath, destPath);
|
|
341
|
+
} else if (entry.isFile()) {
|
|
342
|
+
await fs.promises.copyFile(srcPath, destPath);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private async installDependencies(workspacePath: string, timeout?: number): Promise<void> {
|
|
348
|
+
const execOpts: ExecSyncOptionsWithBufferEncoding = {
|
|
349
|
+
cwd: workspacePath,
|
|
350
|
+
stdio: 'pipe',
|
|
351
|
+
timeout: timeout || DEFAULT_TIMEOUT,
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Detect package manager
|
|
355
|
+
if (fs.existsSync(path.join(workspacePath, 'pnpm-lock.yaml'))) {
|
|
356
|
+
execSync('pnpm install --frozen-lockfile', execOpts);
|
|
357
|
+
} else if (fs.existsSync(path.join(workspacePath, 'yarn.lock'))) {
|
|
358
|
+
execSync('yarn install --frozen-lockfile', execOpts);
|
|
359
|
+
} else if (fs.existsSync(path.join(workspacePath, 'package-lock.json'))) {
|
|
360
|
+
execSync('npm ci', execOpts);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private async tryGitApply(
|
|
365
|
+
workspacePath: string,
|
|
366
|
+
diff: string,
|
|
367
|
+
checkOnly: boolean
|
|
368
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
369
|
+
const tempFile = path.join(workspacePath, '.guardrail-patch.diff');
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
await fs.promises.writeFile(tempFile, diff);
|
|
373
|
+
|
|
374
|
+
const cmd = checkOnly
|
|
375
|
+
? `git apply --check "${tempFile}"`
|
|
376
|
+
: `git apply "${tempFile}"`;
|
|
377
|
+
|
|
378
|
+
execSync(cmd, {
|
|
379
|
+
cwd: workspacePath,
|
|
380
|
+
stdio: 'pipe',
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
return { success: true };
|
|
384
|
+
} catch (e) {
|
|
385
|
+
const err = e as { stderr?: Buffer; message: string };
|
|
386
|
+
return {
|
|
387
|
+
success: false,
|
|
388
|
+
error: err.stderr?.toString() || err.message,
|
|
389
|
+
};
|
|
390
|
+
} finally {
|
|
391
|
+
try {
|
|
392
|
+
await fs.promises.unlink(tempFile);
|
|
393
|
+
} catch {
|
|
394
|
+
// Ignore
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private async applyHunk(workspacePath: string, hunk: ParsedHunk): Promise<void> {
|
|
400
|
+
const filePath = path.join(workspacePath, hunk.file);
|
|
401
|
+
|
|
402
|
+
// Ensure directory exists
|
|
403
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
404
|
+
|
|
405
|
+
// Read existing content or empty for new files
|
|
406
|
+
let content = '';
|
|
407
|
+
try {
|
|
408
|
+
content = await fs.promises.readFile(filePath, 'utf8');
|
|
409
|
+
} catch {
|
|
410
|
+
// New file
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const lines = content.split('\n');
|
|
414
|
+
const hunkLines = hunk.content.split('\n').filter(l => !l.startsWith('@@'));
|
|
415
|
+
|
|
416
|
+
const result: string[] = [];
|
|
417
|
+
let srcIdx = 0;
|
|
418
|
+
|
|
419
|
+
// Copy lines before hunk
|
|
420
|
+
while (srcIdx < hunk.oldStart - 1 && srcIdx < lines.length) {
|
|
421
|
+
result.push(lines[srcIdx] || '');
|
|
422
|
+
srcIdx++;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Process hunk
|
|
426
|
+
for (const line of hunkLines) {
|
|
427
|
+
if (line.startsWith('-')) {
|
|
428
|
+
srcIdx++; // Skip deleted line
|
|
429
|
+
} else if (line.startsWith('+')) {
|
|
430
|
+
result.push(line.slice(1)); // Add new line
|
|
431
|
+
} else if (line.startsWith(' ') || line === '') {
|
|
432
|
+
if (srcIdx < lines.length) {
|
|
433
|
+
result.push(lines[srcIdx] || '');
|
|
434
|
+
srcIdx++;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Copy remaining lines
|
|
440
|
+
while (srcIdx < lines.length) {
|
|
441
|
+
result.push(lines[srcIdx] || '');
|
|
442
|
+
srcIdx++;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
await fs.promises.writeFile(filePath, result.join('\n'));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private getVerificationCommands(
|
|
449
|
+
fingerprint: RepoFingerprint,
|
|
450
|
+
skipTests?: boolean
|
|
451
|
+
): Array<{ name: string; command: string }> {
|
|
452
|
+
const commands: Array<{ name: string; command: string }> = [];
|
|
453
|
+
|
|
454
|
+
// TypeScript check
|
|
455
|
+
if (fingerprint.hasTypeScript) {
|
|
456
|
+
commands.push({
|
|
457
|
+
name: 'TypeScript',
|
|
458
|
+
command: 'npx tsc --noEmit',
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Build check based on framework
|
|
463
|
+
if (fingerprint.buildTool === 'turbo') {
|
|
464
|
+
commands.push({ name: 'Build (Turbo)', command: 'npx turbo run build' });
|
|
465
|
+
} else if (fingerprint.buildTool === 'nx') {
|
|
466
|
+
commands.push({ name: 'Build (Nx)', command: 'npx nx run-many --target=build' });
|
|
467
|
+
} else if (fingerprint.framework === 'next') {
|
|
468
|
+
commands.push({ name: 'Build (Next.js)', command: 'npm run build' });
|
|
469
|
+
} else if (fingerprint.framework === 'vite') {
|
|
470
|
+
commands.push({ name: 'Build (Vite)', command: 'npm run build' });
|
|
471
|
+
} else if (fingerprint.hasBuildScript) {
|
|
472
|
+
commands.push({ name: 'Build', command: 'npm run build' });
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Tests (optional)
|
|
476
|
+
if (!skipTests && fingerprint.testRunner) {
|
|
477
|
+
const testCmd = fingerprint.testRunner === 'vitest'
|
|
478
|
+
? 'npx vitest run'
|
|
479
|
+
: fingerprint.testRunner === 'jest'
|
|
480
|
+
? 'npx jest --passWithNoTests'
|
|
481
|
+
: 'npm test';
|
|
482
|
+
commands.push({ name: `Tests (${fingerprint.testRunner})`, command: testCmd });
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return commands;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private truncateOutput(output: string): string {
|
|
489
|
+
const lines = output.split('\n');
|
|
490
|
+
if (lines.length <= MAX_OUTPUT_LINES) {
|
|
491
|
+
return output;
|
|
492
|
+
}
|
|
493
|
+
return lines.slice(0, MAX_OUTPUT_LINES).join('\n') + `\n... (${lines.length - MAX_OUTPUT_LINES} more lines)`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private extractFailureContext(checks: CheckResult[]): string[] {
|
|
497
|
+
const failures: string[] = [];
|
|
498
|
+
|
|
499
|
+
for (const check of checks) {
|
|
500
|
+
if (check.passed) continue;
|
|
501
|
+
|
|
502
|
+
const lines = check.output.split('\n');
|
|
503
|
+
const errorLines: string[] = [];
|
|
504
|
+
|
|
505
|
+
for (const line of lines) {
|
|
506
|
+
// TypeScript errors
|
|
507
|
+
if (line.includes('error TS') || line.includes(': error')) {
|
|
508
|
+
errorLines.push(line.trim());
|
|
509
|
+
}
|
|
510
|
+
// Build errors
|
|
511
|
+
else if (line.includes('Error:') || line.includes('error:')) {
|
|
512
|
+
errorLines.push(line.trim());
|
|
513
|
+
}
|
|
514
|
+
// Test failures
|
|
515
|
+
else if (line.includes('FAIL') || line.includes('✗') || line.includes('×')) {
|
|
516
|
+
errorLines.push(line.trim());
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (errorLines.length >= 3) break;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
failures.push(...errorLines);
|
|
523
|
+
if (failures.length >= 3) break;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return failures.slice(0, 3);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Export singleton
|
|
531
|
+
export const tempWorkspace = new TempWorkspace();
|