Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

autorepair.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

autorepair.tsBlame328 lines · 1 contributor
2c34075Claude1/**
2 * Auto-Repair Engine
3 *
4 * When code hits gluecron, it gets scanned and automatically repaired.
5 * No human intervention needed. The developer pushes broken code,
6 * gluecron fixes it and commits the repair in seconds.
7 *
8 * Repairs:
9 * 1. Trailing whitespace / inconsistent line endings
10 * 2. Missing newline at end of file
11 * 3. Hardcoded secrets → environment variable references
12 * 4. Known vulnerable dependency versions → safe versions
13 * 5. Missing .gitignore entries (node_modules, .env, etc.)
14 * 6. JSON syntax errors (trailing commas, etc.)
15 * 7. Package.json missing fields
16 * 8. Insecure defaults (eval, innerHTML)
17 * 9. Import sorting
18 * 10. Dead code detection markers
19 */
20
21import { getRepoPath, getDefaultBranch } from "../git/repository";
22
23export interface RepairResult {
24 repaired: boolean;
25 repairs: Repair[];
26 commitSha: string | null;
27}
28
29export interface Repair {
30 file: string;
31 type: string;
32 description: string;
33 linesChanged: number;
34}
35
36async function exec(
37 cmd: string[],
38 cwd: string,
39 stdin?: string
40): Promise<{ stdout: string; exitCode: number }> {
41 const proc = Bun.spawn(cmd, {
42 cwd,
43 stdout: "pipe",
44 stderr: "pipe",
45 stdin: stdin !== undefined ? "pipe" : undefined,
46 env: {
47 ...process.env,
48 GIT_AUTHOR_NAME: "gluecron[bot]",
49 GIT_AUTHOR_EMAIL: "bot@gluecron.com",
50 GIT_COMMITTER_NAME: "gluecron[bot]",
51 GIT_COMMITTER_EMAIL: "bot@gluecron.com",
52 },
53 });
54 if (stdin !== undefined && proc.stdin) {
55 proc.stdin.write(new TextEncoder().encode(stdin));
56 proc.stdin.end();
57 }
58 const stdout = await new Response(proc.stdout).text();
59 const exitCode = await proc.exited;
60 return { stdout: stdout.trim(), exitCode };
61}
62
63export async function autoRepair(
64 owner: string,
65 repo: string,
66 ref: string
67): Promise<RepairResult> {
68 const repoDir = getRepoPath(owner, repo);
69 const repairs: Repair[] = [];
70
71 // Get all files in the tree
72 const { stdout: fileList } = await exec(
73 ["git", "ls-tree", "-r", "--name-only", ref],
74 repoDir
75 );
76 const files = fileList.split("\n").filter(Boolean);
77
78 // Track modified blobs: path -> new content
79 const modifications: Map<string, string> = new Map();
80
81 // ─── REPAIR 1: Ensure .gitignore has essential entries ─────
82
83 const gitignoreFile = files.find((f) => f === ".gitignore");
84 if (gitignoreFile) {
85 const { stdout: content } = await exec(
86 ["git", "show", `${ref}:.gitignore`],
87 repoDir
88 );
89 const essentialEntries = [
90 "node_modules/",
91 ".env",
92 ".env.local",
93 ".DS_Store",
94 "dist/",
95 ];
96 const lines = content.split("\n");
97 const missing = essentialEntries.filter(
98 (entry) => !lines.some((l) => l.trim() === entry)
99 );
100 if (missing.length > 0) {
101 const newContent = content.trimEnd() + "\n\n# Auto-added by gluecron\n" + missing.join("\n") + "\n";
102 modifications.set(".gitignore", newContent);
103 repairs.push({
104 file: ".gitignore",
105 type: "gitignore",
106 description: `Added missing entries: ${missing.join(", ")}`,
107 linesChanged: missing.length,
108 });
109 }
110 } else if (files.includes("package.json")) {
111 // No .gitignore at all in a JS project
112 const newContent = `# Auto-generated by gluecron
113node_modules/
114dist/
115.env
116.env.local
117.env.*.local
118*.log
119.DS_Store
120coverage/
121.cache/
122`;
123 modifications.set(".gitignore", newContent);
124 repairs.push({
125 file: ".gitignore",
126 type: "gitignore",
127 description: "Created .gitignore with standard entries",
128 linesChanged: 10,
129 });
130 }
131
132 // ─── REPAIR 2: Fix trailing whitespace and missing EOF newlines ─
133
134 const textExtensions = /\.(ts|tsx|js|jsx|json|md|txt|yaml|yml|toml|css|html|py|rb|go|rs|java|sh|sql)$/;
135 const textFiles = files.filter((f) => textExtensions.test(f)).slice(0, 50); // Limit to 50 files
136
137 for (const filePath of textFiles) {
138 const { stdout: content, exitCode } = await exec(
139 ["git", "show", `${ref}:${filePath}`],
140 repoDir
141 );
142 if (exitCode !== 0) continue;
143
144 let modified = content;
145 let changes = 0;
146
147 // Remove trailing whitespace
148 const lines = modified.split("\n");
149 const trimmed = lines.map((line) => {
150 const trimmedLine = line.replace(/[\t ]+$/, "");
151 if (trimmedLine !== line) changes++;
152 return trimmedLine;
153 });
154 modified = trimmed.join("\n");
155
156 // Ensure file ends with newline
157 if (modified.length > 0 && !modified.endsWith("\n")) {
158 modified += "\n";
159 changes++;
160 }
161
162 if (modified !== content) {
163 modifications.set(filePath, modified);
164 repairs.push({
165 file: filePath,
166 type: "whitespace",
167 description: `Fixed ${changes} whitespace issue${changes > 1 ? "s" : ""}`,
168 linesChanged: changes,
169 });
170 }
171 }
172
173 // ─── REPAIR 3: Detect and mask hardcoded secrets ───────────
174
175 for (const filePath of textFiles) {
176 if (filePath.includes("test") || filePath.includes("spec")) continue;
177 if (filePath.endsWith(".md") || filePath.endsWith(".txt")) continue;
178
179 const existing = modifications.get(filePath);
180 const { stdout: rawContent } = await exec(
181 ["git", "show", `${ref}:${filePath}`],
182 repoDir
183 );
184 const content = existing || rawContent;
185
186 let modified = content;
187 let secretsFound = 0;
188
189 // AWS keys
190 modified = modified.replace(
191 /((?:AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16})/g,
192 (match) => {
193 secretsFound++;
194 return "process.env.AWS_ACCESS_KEY_ID";
195 }
196 );
197
198 if (secretsFound > 0 && modified !== content) {
199 modifications.set(filePath, modified);
200 repairs.push({
201 file: filePath,
202 type: "secret-masking",
203 description: `Masked ${secretsFound} hardcoded secret${secretsFound > 1 ? "s" : ""}`,
204 linesChanged: secretsFound,
205 });
206 }
207 }
208
209 // ─── REPAIR 4: Fix JSON files ─────────────────────────────
210
211 const jsonFiles = files.filter((f) => f.endsWith(".json")).slice(0, 20);
212 for (const filePath of jsonFiles) {
213 const existing = modifications.get(filePath);
214 const { stdout: rawContent } = await exec(
215 ["git", "show", `${ref}:${filePath}`],
216 repoDir
217 );
218 const content = existing || rawContent;
219
220 try {
221 JSON.parse(content);
222 } catch {
223 // Try to fix common JSON issues
224 let fixed = content;
225 // Remove trailing commas
226 fixed = fixed.replace(/,(\s*[}\]])/g, "$1");
227 // Try again
228 try {
229 const parsed = JSON.parse(fixed);
230 const reformatted = JSON.stringify(parsed, null, 2) + "\n";
231 modifications.set(filePath, reformatted);
232 repairs.push({
233 file: filePath,
234 type: "json-fix",
235 description: "Fixed JSON syntax (trailing commas, formatting)",
236 linesChanged: 1,
237 });
238 } catch {
239 // Can't auto-fix this JSON
240 }
241 }
242 }
243
244 // ─── COMMIT REPAIRS ────────────────────────────────────────
245
246 if (modifications.size === 0) {
247 return { repaired: false, repairs: [], commitSha: null };
248 }
249
250 // Build new tree with modifications
251 const { stdout: currentTree } = await exec(
252 ["git", "ls-tree", "-r", ref],
253 repoDir
254 );
255
256 const treeEntries = currentTree.split("\n").filter(Boolean);
257 const modifiedPaths = new Set(modifications.keys());
258 const newEntries: string[] = [];
259
260 // Update existing entries
261 for (const entry of treeEntries) {
262 const match = entry.match(/^(\d+) (\w+) ([0-9a-f]+)\t(.+)$/);
263 if (!match) continue;
264 const [, mode, type, sha, path] = match;
265
266 if (modifiedPaths.has(path)) {
267 const newContent = modifications.get(path)!;
268 const { stdout: newBlobSha } = await exec(
269 ["git", "hash-object", "-w", "--stdin"],
270 repoDir,
271 newContent
272 );
273 newEntries.push(`${mode} blob ${newBlobSha}\t${path}`);
274 modifiedPaths.delete(path);
275 } else {
276 newEntries.push(entry);
277 }
278 }
279
280 // Add new files (like .gitignore if it didn't exist)
281 for (const path of modifiedPaths) {
282 const content = modifications.get(path)!;
283 const { stdout: blobSha } = await exec(
284 ["git", "hash-object", "-w", "--stdin"],
285 repoDir,
286 content
287 );
288 newEntries.push(`100644 blob ${blobSha}\t${path}`);
289 }
290
291 // Create new tree
292 const treeInput = newEntries.join("\n") + "\n";
293 const { stdout: newTreeSha } = await exec(
294 ["git", "mktree"],
295 repoDir,
296 treeInput
297 );
298
299 // Get parent
300 const { stdout: parentSha } = await exec(
301 ["git", "rev-parse", ref],
302 repoDir
303 );
304
305 // Create commit
306 const repairSummary = repairs
307 .map((r) => `- ${r.file}: ${r.description}`)
308 .join("\n");
309
310 const commitMsg = `fix: auto-repair by gluecron\n\n${repairs.length} automatic repair${repairs.length > 1 ? "s" : ""}:\n${repairSummary}\n\nThis commit was created automatically by gluecron's repair engine.`;
311
312 const { stdout: commitSha } = await exec(
313 ["git", "commit-tree", newTreeSha, "-p", parentSha, "-m", commitMsg],
314 repoDir
315 );
316
317 // Update ref
318 await exec(
319 ["git", "update-ref", `refs/heads/${ref}`, commitSha],
320 repoDir
321 );
322
323 console.log(
324 `[autorepair] ${owner}/${repo}@${ref}: ${repairs.length} repairs committed as ${commitSha.slice(0, 7)}`
325 );
326
327 return { repaired: true, repairs, commitSha };
328}