git-cracked 1.2.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/LICENSE +21 -0
- package/README.md +156 -0
- package/config.example.json +11 -0
- package/package.json +50 -0
- package/scripts/install-linux.js +57 -0
- package/scripts/install-mac.js +63 -0
- package/scripts/install-windows.js +38 -0
- package/src/ai.js +3 -0
- package/src/cli.js +37 -0
- package/src/committer.js +37 -0
- package/src/config.js +36 -0
- package/src/dashboard.js +485 -0
- package/src/index.js +64 -0
- package/src/logger.js +29 -0
- package/src/messages.js +197 -0
- package/src/mutator.js +373 -0
- package/src/paths.js +24 -0
- package/src/setup.js +50 -0
package/src/messages.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
const pool = {
|
|
2
|
+
whitespace: [
|
|
3
|
+
'style: improve vertical spacing for readability',
|
|
4
|
+
'style: add whitespace between logical blocks',
|
|
5
|
+
'style: clean up blank lines for consistency',
|
|
6
|
+
'style: separate concerns with whitespace',
|
|
7
|
+
'style: remove unnecessary blank lines',
|
|
8
|
+
'style: normalize vertical whitespace',
|
|
9
|
+
'style: tighten up spacing in dense block',
|
|
10
|
+
'style: improve code breathing room',
|
|
11
|
+
'refactor: reorganize spacing for clarity',
|
|
12
|
+
'chore: whitespace cleanup',
|
|
13
|
+
'style: consolidate blank lines',
|
|
14
|
+
'style: remove redundant blank line',
|
|
15
|
+
'style: add breathing room between sections',
|
|
16
|
+
],
|
|
17
|
+
|
|
18
|
+
trailing: [
|
|
19
|
+
'style: remove trailing whitespace',
|
|
20
|
+
'chore: strip trailing spaces',
|
|
21
|
+
'style: clean up line endings',
|
|
22
|
+
'style: fix trailing whitespace on modified lines',
|
|
23
|
+
'chore: normalize line endings',
|
|
24
|
+
'style: remove invisible trailing characters',
|
|
25
|
+
'chore: trailing whitespace cleanup',
|
|
26
|
+
],
|
|
27
|
+
|
|
28
|
+
newline: [
|
|
29
|
+
'chore: ensure file ends with newline',
|
|
30
|
+
'style: add missing newline at end of file',
|
|
31
|
+
'chore: fix missing trailing newline',
|
|
32
|
+
'style: normalize end-of-file newline',
|
|
33
|
+
'chore: trim extra trailing newlines',
|
|
34
|
+
'style: conform to POSIX newline convention',
|
|
35
|
+
],
|
|
36
|
+
|
|
37
|
+
quotes: [
|
|
38
|
+
'style: standardise quote style to double quotes',
|
|
39
|
+
'style: normalise string literals to consistent quote style',
|
|
40
|
+
'refactor: align quote usage with project convention',
|
|
41
|
+
'style: use consistent string delimiters',
|
|
42
|
+
'style: enforce quote style across file',
|
|
43
|
+
'chore: quote style normalisation',
|
|
44
|
+
'style: switch to preferred quote style',
|
|
45
|
+
'refactor: unify quote style for linter compliance',
|
|
46
|
+
],
|
|
47
|
+
|
|
48
|
+
const: [
|
|
49
|
+
'refactor: prefer const for non-reassigned bindings',
|
|
50
|
+
'refactor: use const over let where value is not reassigned',
|
|
51
|
+
'style: enforce const correctness',
|
|
52
|
+
'refactor: tighten variable declarations with const',
|
|
53
|
+
'chore: replace let with const for immutable references',
|
|
54
|
+
'refactor: mark non-mutated bindings as const',
|
|
55
|
+
'style: const over let per project convention',
|
|
56
|
+
'refactor: signal immutability with const',
|
|
57
|
+
'fix: prevent accidental reassignment with const',
|
|
58
|
+
],
|
|
59
|
+
|
|
60
|
+
semicolons: [
|
|
61
|
+
'style: add missing semicolons',
|
|
62
|
+
'chore: enforce semicolons per style guide',
|
|
63
|
+
'style: insert missing statement terminators',
|
|
64
|
+
'style: normalise semicolon usage',
|
|
65
|
+
'chore: semicolon consistency pass',
|
|
66
|
+
'style: add semicolons for ASI safety',
|
|
67
|
+
],
|
|
68
|
+
|
|
69
|
+
equality: [
|
|
70
|
+
'fix: use strict equality to avoid type coercion',
|
|
71
|
+
'refactor: prefer === over == for type-safe comparisons',
|
|
72
|
+
'fix: replace loose equality with strict equality check',
|
|
73
|
+
'refactor: enforce strict equality throughout',
|
|
74
|
+
'chore: migrate == to === for correctness',
|
|
75
|
+
'fix: avoid implicit type coercion in equality check',
|
|
76
|
+
'refactor: use identity comparison instead of abstract equality',
|
|
77
|
+
'style: strict equality per lint rules',
|
|
78
|
+
],
|
|
79
|
+
|
|
80
|
+
arrow: [
|
|
81
|
+
'refactor: convert to arrow function syntax',
|
|
82
|
+
'refactor: use arrow function for conciseness',
|
|
83
|
+
'style: prefer arrow functions for anonymous callbacks',
|
|
84
|
+
'refactor: modernise function expression to arrow syntax',
|
|
85
|
+
'chore: align with ES6 arrow function convention',
|
|
86
|
+
'refactor: simplify with arrow function',
|
|
87
|
+
'style: arrow function for lexical this binding',
|
|
88
|
+
],
|
|
89
|
+
|
|
90
|
+
comment_add: [
|
|
91
|
+
'docs: add clarifying inline comment',
|
|
92
|
+
'chore: annotate non-obvious logic',
|
|
93
|
+
'docs: document intent of expression',
|
|
94
|
+
'chore: add note for future maintainers',
|
|
95
|
+
'docs: clarify edge case handling',
|
|
96
|
+
'chore: leave breadcrumb for reviewers',
|
|
97
|
+
'docs: annotate subtle invariant',
|
|
98
|
+
'chore: improve code self-documentation',
|
|
99
|
+
],
|
|
100
|
+
|
|
101
|
+
comment_remove: [
|
|
102
|
+
'chore: remove stale inline comment',
|
|
103
|
+
'docs: drop outdated annotation',
|
|
104
|
+
'chore: clean up obsolete comment',
|
|
105
|
+
'refactor: remove redundant comment noise',
|
|
106
|
+
'docs: prune comment that no longer applies',
|
|
107
|
+
'chore: delete stale TODO resolved elsewhere',
|
|
108
|
+
],
|
|
109
|
+
|
|
110
|
+
imports: [
|
|
111
|
+
'chore: sort import statements alphabetically',
|
|
112
|
+
'style: alphabetise imports for consistency',
|
|
113
|
+
'refactor: reorder imports to match convention',
|
|
114
|
+
'chore: organise import block',
|
|
115
|
+
'style: enforce import sort order',
|
|
116
|
+
'refactor: normalise import ordering',
|
|
117
|
+
'chore: keep imports sorted for diff clarity',
|
|
118
|
+
'style: alphabetical import grouping',
|
|
119
|
+
],
|
|
120
|
+
|
|
121
|
+
numeric: [
|
|
122
|
+
'fix: adjust off-by-one in boundary constant',
|
|
123
|
+
'refactor: update threshold constant',
|
|
124
|
+
'chore: revise magic number',
|
|
125
|
+
'fix: correct numeric boundary value',
|
|
126
|
+
'refactor: adjust timeout constant',
|
|
127
|
+
'chore: tune numeric parameter',
|
|
128
|
+
'fix: tweak limit to match updated spec',
|
|
129
|
+
'refactor: update hardcoded constant',
|
|
130
|
+
'chore: adjust retry count',
|
|
131
|
+
'fix: correct range boundary',
|
|
132
|
+
],
|
|
133
|
+
|
|
134
|
+
python_none: [
|
|
135
|
+
'fix: use identity check for None comparison',
|
|
136
|
+
'refactor: prefer is None over == None per PEP 8',
|
|
137
|
+
'style: replace == None with is None',
|
|
138
|
+
'fix: correct None check to use identity operator',
|
|
139
|
+
'chore: align None comparisons with PEP 8',
|
|
140
|
+
'refactor: use is not None for truthiness check',
|
|
141
|
+
'fix: avoid equality check against None singleton',
|
|
142
|
+
],
|
|
143
|
+
|
|
144
|
+
general: [
|
|
145
|
+
'refactor: minor code quality improvement',
|
|
146
|
+
'chore: routine maintenance pass',
|
|
147
|
+
'style: apply formatting fixes',
|
|
148
|
+
'refactor: small cleanup for readability',
|
|
149
|
+
'chore: tidy up per code review feedback',
|
|
150
|
+
'fix: address minor inconsistency',
|
|
151
|
+
'refactor: simplify expression',
|
|
152
|
+
'chore: lint and style fixes',
|
|
153
|
+
'style: formatting pass',
|
|
154
|
+
'refactor: clean up per style guide',
|
|
155
|
+
'chore: housekeeping',
|
|
156
|
+
'fix: correct minor stylistic issue',
|
|
157
|
+
'refactor: reduce cognitive overhead',
|
|
158
|
+
'chore: address tech debt',
|
|
159
|
+
'style: align with project conventions',
|
|
160
|
+
'refactor: extract repeated pattern',
|
|
161
|
+
'chore: minor polish pass',
|
|
162
|
+
'fix: resolve edge case inconsistency',
|
|
163
|
+
'refactor: improve clarity of intent',
|
|
164
|
+
'chore: routine code hygiene',
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const TYPE_MAP = {
|
|
169
|
+
'improved readability with whitespace': 'whitespace',
|
|
170
|
+
'removed unnecessary blank line': 'whitespace',
|
|
171
|
+
'added whitespace for readability': 'whitespace',
|
|
172
|
+
'removed trailing whitespace': 'trailing',
|
|
173
|
+
'added missing trailing newline': 'newline',
|
|
174
|
+
'trimmed extra trailing newlines': 'newline',
|
|
175
|
+
'ensured trailing newline': 'newline',
|
|
176
|
+
'normalised quote style': 'quotes',
|
|
177
|
+
'standardised string quote style': 'quotes',
|
|
178
|
+
'prefer const over let for immutable binding': 'const',
|
|
179
|
+
'added missing semicolon': 'semicolons',
|
|
180
|
+
'prefer strict equality operator': 'equality',
|
|
181
|
+
'refactored to arrow function syntax': 'arrow',
|
|
182
|
+
'added clarifying inline comment': 'comment_add',
|
|
183
|
+
'removed stale inline comment': 'comment_remove',
|
|
184
|
+
'sorted import statements alphabetically': 'imports',
|
|
185
|
+
'adjusted numeric constant': 'numeric',
|
|
186
|
+
'use identity check for None comparison': 'python_none',
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
function pickRandom(arr) {
|
|
190
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function generateCommitMessage(description) {
|
|
194
|
+
const matched = Object.entries(TYPE_MAP).find(([key]) => description.toLowerCase().includes(key.toLowerCase()));
|
|
195
|
+
const bucket = matched ? pool[matched[1]] : pool.general;
|
|
196
|
+
return pickRandom(bucket ?? pool.general);
|
|
197
|
+
}
|
package/src/mutator.js
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, writeFileSync, statSync } from 'fs';
|
|
2
|
+
import { join, extname, relative } from 'path';
|
|
3
|
+
|
|
4
|
+
const SUPPORTED_EXTENSIONS = new Set([
|
|
5
|
+
'.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs',
|
|
6
|
+
'.py', '.go', '.java', '.rb', '.cs', '.cpp', '.c', '.rs',
|
|
7
|
+
'.php', '.swift', '.kt', '.scala', '.sh',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const IGNORE_DIRS = new Set([
|
|
11
|
+
'node_modules', '.git', 'dist', 'build', '__pycache__',
|
|
12
|
+
'.next', 'vendor', 'target', 'out', 'coverage', '.cache',
|
|
13
|
+
'.turbo', 'tmp', 'temp', '.svelte-kit',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function collectFiles(dir, files = []) {
|
|
17
|
+
let entries;
|
|
18
|
+
try {
|
|
19
|
+
entries = readdirSync(dir);
|
|
20
|
+
} catch {
|
|
21
|
+
return files;
|
|
22
|
+
}
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
if (IGNORE_DIRS.has(entry) || entry.startsWith('.')) continue;
|
|
25
|
+
const full = join(dir, entry);
|
|
26
|
+
try {
|
|
27
|
+
const stat = statSync(full);
|
|
28
|
+
if (stat.isDirectory()) {
|
|
29
|
+
collectFiles(full, files);
|
|
30
|
+
} else if (SUPPORTED_EXTENSIONS.has(extname(entry))) {
|
|
31
|
+
files.push(full);
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// skip unreadable entries
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return files;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function pickRandom(arr) {
|
|
41
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function randomInt(min, max) {
|
|
45
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Each mutation: (lines, ext) => { newContent, description } | null
|
|
49
|
+
const mutations = [
|
|
50
|
+
// ── Whitespace ──────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
// Insert blank line between two non-blank lines
|
|
53
|
+
(lines) => {
|
|
54
|
+
const candidates = [];
|
|
55
|
+
for (let i = 1; i < lines.length - 2; i++) {
|
|
56
|
+
if (lines[i].trim() && lines[i - 1].trim() && lines[i + 1].trim()) {
|
|
57
|
+
candidates.push(i);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!candidates.length) return null;
|
|
61
|
+
const idx = pickRandom(candidates);
|
|
62
|
+
const out = [...lines.slice(0, idx + 1), '', ...lines.slice(idx + 1)];
|
|
63
|
+
return { newContent: out.join('\n'), description: 'improved readability with whitespace' };
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// Remove a redundant blank line
|
|
67
|
+
(lines) => {
|
|
68
|
+
const blanks = lines.reduce((acc, l, i) => (l.trim() === '' ? [...acc, i] : acc), []);
|
|
69
|
+
if (blanks.length < 2) return null;
|
|
70
|
+
const idx = pickRandom(blanks);
|
|
71
|
+
return { newContent: lines.filter((_, i) => i !== idx).join('\n'), description: 'removed unnecessary blank line' };
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Ensure single trailing newline
|
|
75
|
+
(lines) => {
|
|
76
|
+
const content = lines.join('\n');
|
|
77
|
+
if (!content.endsWith('\n')) {
|
|
78
|
+
return { newContent: content + '\n', description: 'added missing trailing newline' };
|
|
79
|
+
}
|
|
80
|
+
if (content.endsWith('\n\n')) {
|
|
81
|
+
return { newContent: content.replace(/\n+$/, '\n'), description: 'trimmed extra trailing newlines' };
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// ── Quote style ─────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
// Normalise single → double quotes on one line (JS/TS/Python)
|
|
89
|
+
(lines, ext) => {
|
|
90
|
+
if (!['.js', '.ts', '.jsx', '.tsx', '.mjs', '.py'].includes(ext)) return null;
|
|
91
|
+
const candidates = lines
|
|
92
|
+
.map((l, i) => ({ l, i }))
|
|
93
|
+
.filter(({ l }) => /'[^']*'/.test(l) && !l.trim().startsWith('//') && !l.trim().startsWith('#'));
|
|
94
|
+
if (!candidates.length) return null;
|
|
95
|
+
const { l, i } = pickRandom(candidates);
|
|
96
|
+
const newLine = l.replace(/'([^']*)'/g, '"$1"');
|
|
97
|
+
if (newLine === l) return null;
|
|
98
|
+
const out = [...lines];
|
|
99
|
+
out[i] = newLine;
|
|
100
|
+
return { newContent: out.join('\n'), description: 'standardised string quote style' };
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// ── const / let ─────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
// Change `let x =` to `const x =` where safe-ish (no reassignment heuristic)
|
|
106
|
+
(lines, ext) => {
|
|
107
|
+
if (!['.js', '.ts', '.jsx', '.tsx', '.mjs'].includes(ext)) return null;
|
|
108
|
+
const candidates = lines
|
|
109
|
+
.map((l, i) => ({ l, i }))
|
|
110
|
+
.filter(({ l }) => /^\s*let\s+\w+\s*=/.test(l));
|
|
111
|
+
if (!candidates.length) return null;
|
|
112
|
+
const { l, i } = pickRandom(candidates);
|
|
113
|
+
const newLine = l.replace(/\blet\b/, 'const');
|
|
114
|
+
const out = [...lines];
|
|
115
|
+
out[i] = newLine;
|
|
116
|
+
return { newContent: out.join('\n'), description: 'prefer const over let for immutable binding' };
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// ── Semicolons ───────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
// Add missing semicolons (JS/TS)
|
|
122
|
+
(lines, ext) => {
|
|
123
|
+
if (!['.js', '.ts', '.jsx', '.tsx', '.mjs'].includes(ext)) return null;
|
|
124
|
+
const candidates = lines
|
|
125
|
+
.map((l, i) => ({ l, i }))
|
|
126
|
+
.filter(({ l }) => {
|
|
127
|
+
const t = l.trimEnd();
|
|
128
|
+
return t.length > 0
|
|
129
|
+
&& !t.endsWith(';')
|
|
130
|
+
&& !t.endsWith('{')
|
|
131
|
+
&& !t.endsWith('}')
|
|
132
|
+
&& !t.endsWith(',')
|
|
133
|
+
&& !t.endsWith('(')
|
|
134
|
+
&& !t.trim().startsWith('//')
|
|
135
|
+
&& /^\s+(return|const|let|var|throw)\s/.test(l);
|
|
136
|
+
});
|
|
137
|
+
if (!candidates.length) return null;
|
|
138
|
+
const { l, i } = pickRandom(candidates);
|
|
139
|
+
const out = [...lines];
|
|
140
|
+
out[i] = l.trimEnd() + ';';
|
|
141
|
+
return { newContent: out.join('\n'), description: 'added missing semicolon' };
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// ── Numeric constants ────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
// Nudge a numeric literal ±1
|
|
147
|
+
(lines) => {
|
|
148
|
+
const candidates = lines
|
|
149
|
+
.map((l, i) => ({ l, i }))
|
|
150
|
+
.filter(({ l }) => /\b([1-9]\d{0,3})\b/.test(l) && !l.trim().startsWith('//') && !l.trim().startsWith('#'));
|
|
151
|
+
if (!candidates.length) return null;
|
|
152
|
+
const { l, i } = pickRandom(candidates);
|
|
153
|
+
const newLine = l.replace(/\b([1-9]\d{0,3})\b/, (m) => {
|
|
154
|
+
const n = parseInt(m, 10);
|
|
155
|
+
return String(Math.random() < 0.5 ? n + 1 : Math.max(1, n - 1));
|
|
156
|
+
});
|
|
157
|
+
if (newLine === l) return null;
|
|
158
|
+
const out = [...lines];
|
|
159
|
+
out[i] = newLine;
|
|
160
|
+
return { newContent: out.join('\n'), description: 'adjusted numeric constant' };
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
// ── Comments ─────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
// Add a short inline comment to a plain assignment line
|
|
166
|
+
(lines, ext) => {
|
|
167
|
+
const commentChar = ['.py', '.rb', '.sh'].includes(ext) ? '#' : '//';
|
|
168
|
+
const comments = [
|
|
169
|
+
'TODO: revisit this',
|
|
170
|
+
'edge case handled',
|
|
171
|
+
'intentional',
|
|
172
|
+
'perf: hot path',
|
|
173
|
+
'keep in sync with schema',
|
|
174
|
+
'zero-indexed',
|
|
175
|
+
'inclusive range',
|
|
176
|
+
];
|
|
177
|
+
const candidates = lines
|
|
178
|
+
.map((l, i) => ({ l, i }))
|
|
179
|
+
.filter(({ l }) => {
|
|
180
|
+
const t = l.trim();
|
|
181
|
+
return t.length > 4
|
|
182
|
+
&& !t.startsWith('//')
|
|
183
|
+
&& !t.startsWith('#')
|
|
184
|
+
&& !t.startsWith('*')
|
|
185
|
+
&& !t.startsWith('/*')
|
|
186
|
+
&& !t.includes('//')
|
|
187
|
+
&& !t.includes('#')
|
|
188
|
+
&& (t.includes('=') || t.includes('return'));
|
|
189
|
+
});
|
|
190
|
+
if (!candidates.length) return null;
|
|
191
|
+
const { l, i } = pickRandom(candidates);
|
|
192
|
+
const out = [...lines];
|
|
193
|
+
out[i] = l.trimEnd() + ` ${commentChar} ${pickRandom(comments)}`;
|
|
194
|
+
return { newContent: out.join('\n'), description: 'added clarifying inline comment' };
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// Remove an existing inline comment
|
|
198
|
+
(lines, ext) => {
|
|
199
|
+
const commentChar = ['.py', '.rb', '.sh'].includes(ext) ? '#' : '//';
|
|
200
|
+
const pattern = new RegExp(`\\s+${commentChar.replace('/', '\\/')}.*$`);
|
|
201
|
+
const candidates = lines
|
|
202
|
+
.map((l, i) => ({ l, i }))
|
|
203
|
+
.filter(({ l }) => {
|
|
204
|
+
const t = l.trim();
|
|
205
|
+
return !t.startsWith(commentChar) && pattern.test(l);
|
|
206
|
+
});
|
|
207
|
+
if (!candidates.length) return null;
|
|
208
|
+
const { l, i } = pickRandom(candidates);
|
|
209
|
+
const out = [...lines];
|
|
210
|
+
out[i] = l.replace(pattern, '');
|
|
211
|
+
return { newContent: out.join('\n'), description: 'removed stale inline comment' };
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
// ── Spacing around operators ─────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
// Fix missing space after comma on a random line
|
|
217
|
+
(lines) => {
|
|
218
|
+
const candidates = lines
|
|
219
|
+
.map((l, i) => ({ l, i }))
|
|
220
|
+
.filter(({ l }) => /,\S/.test(l) && !l.trim().startsWith('//') && !l.trim().startsWith('#'));
|
|
221
|
+
if (!candidates.length) return null;
|
|
222
|
+
const { l, i } = pickRandom(candidates);
|
|
223
|
+
const newLine = l.replace(/,(\S)/g, ', $1');
|
|
224
|
+
if (newLine === l) return null;
|
|
225
|
+
const out = [...lines];
|
|
226
|
+
out[i] = newLine;
|
|
227
|
+
return { newContent: out.join('\n'), description: 'fixed spacing after comma' };
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
// ── Import / require reordering ──────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
// Sort a block of consecutive import lines alphabetically
|
|
233
|
+
(lines, ext) => {
|
|
234
|
+
if (!['.js', '.ts', '.jsx', '.tsx', '.mjs', '.py'].includes(ext)) return null;
|
|
235
|
+
const importPattern = ext === '.py' ? /^\s*(import |from )/ : /^\s*import /;
|
|
236
|
+
|
|
237
|
+
// Find a contiguous block of imports
|
|
238
|
+
let blockStart = -1;
|
|
239
|
+
let blockEnd = -1;
|
|
240
|
+
let inBlock = false;
|
|
241
|
+
for (let i = 0; i < lines.length; i++) {
|
|
242
|
+
if (importPattern.test(lines[i])) {
|
|
243
|
+
if (!inBlock) { blockStart = i; inBlock = true; }
|
|
244
|
+
blockEnd = i;
|
|
245
|
+
} else if (inBlock && lines[i].trim() === '') {
|
|
246
|
+
inBlock = false;
|
|
247
|
+
} else if (inBlock) {
|
|
248
|
+
inBlock = false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (blockStart === -1 || blockEnd === blockStart) return null;
|
|
252
|
+
const block = lines.slice(blockStart, blockEnd + 1);
|
|
253
|
+
const sorted = [...block].sort((a, b) => a.localeCompare(b));
|
|
254
|
+
if (sorted.join('\n') === block.join('\n')) return null;
|
|
255
|
+
const out = [...lines.slice(0, blockStart), ...sorted, ...lines.slice(blockEnd + 1)];
|
|
256
|
+
return { newContent: out.join('\n'), description: 'sorted import statements alphabetically' };
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
// ── Trailing whitespace ──────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
// Strip trailing spaces from lines that have them
|
|
262
|
+
(lines) => {
|
|
263
|
+
const hasTrailing = lines.some(l => / +$/.test(l));
|
|
264
|
+
if (!hasTrailing) return null;
|
|
265
|
+
const out = lines.map(l => l.replace(/ +$/, ''));
|
|
266
|
+
return { newContent: out.join('\n'), description: 'removed trailing whitespace' };
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
// ── Boolean / strict equality ────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
// Replace == with === (JS/TS)
|
|
272
|
+
(lines, ext) => {
|
|
273
|
+
if (!['.js', '.ts', '.jsx', '.tsx', '.mjs'].includes(ext)) return null;
|
|
274
|
+
const candidates = lines
|
|
275
|
+
.map((l, i) => ({ l, i }))
|
|
276
|
+
.filter(({ l }) => /[^=!<>]={2}[^=]/.test(l) && !l.trim().startsWith('//'));
|
|
277
|
+
if (!candidates.length) return null;
|
|
278
|
+
const { l, i } = pickRandom(candidates);
|
|
279
|
+
const newLine = l.replace(/([^=!<>])==([^=])/g, '$1===$2');
|
|
280
|
+
if (newLine === l) return null;
|
|
281
|
+
const out = [...lines];
|
|
282
|
+
out[i] = newLine;
|
|
283
|
+
return { newContent: out.join('\n'), description: 'prefer strict equality operator' };
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
// ── Arrow functions ──────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
// Convert `function foo()` to `const foo = ()` style (JS/TS, single occurrence)
|
|
289
|
+
(lines, ext) => {
|
|
290
|
+
if (!['.js', '.ts', '.mjs'].includes(ext)) return null;
|
|
291
|
+
const candidates = lines
|
|
292
|
+
.map((l, i) => ({ l, i }))
|
|
293
|
+
.filter(({ l }) => /^\s*function\s+\w+\s*\(/.test(l));
|
|
294
|
+
if (!candidates.length) return null;
|
|
295
|
+
const { l, i } = pickRandom(candidates);
|
|
296
|
+
const newLine = l.replace(
|
|
297
|
+
/^(\s*)function\s+(\w+)\s*(\([^)]*\))\s*\{/,
|
|
298
|
+
'$1const $2 = $3 => {'
|
|
299
|
+
);
|
|
300
|
+
if (newLine === l) return null;
|
|
301
|
+
const out = [...lines];
|
|
302
|
+
out[i] = newLine;
|
|
303
|
+
return { newContent: out.join('\n'), description: 'refactored to arrow function syntax' };
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
// ── Python-specific ──────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
// Replace `== None` with `is None`
|
|
309
|
+
(lines, ext) => {
|
|
310
|
+
if (ext !== '.py') return null;
|
|
311
|
+
const candidates = lines
|
|
312
|
+
.map((l, i) => ({ l, i }))
|
|
313
|
+
.filter(({ l }) => /==\s*None/.test(l));
|
|
314
|
+
if (!candidates.length) return null;
|
|
315
|
+
const { l, i } = pickRandom(candidates);
|
|
316
|
+
const out = [...lines];
|
|
317
|
+
out[i] = l.replace(/==\s*None/g, 'is None');
|
|
318
|
+
return { newContent: out.join('\n'), description: 'use identity check for None comparison' };
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
// Replace `!= None` with `is not None`
|
|
322
|
+
(lines, ext) => {
|
|
323
|
+
if (ext !== '.py') return null;
|
|
324
|
+
const candidates = lines
|
|
325
|
+
.map((l, i) => ({ l, i }))
|
|
326
|
+
.filter(({ l }) => /!=\s*None/.test(l));
|
|
327
|
+
if (!candidates.length) return null;
|
|
328
|
+
const { l, i } = pickRandom(candidates);
|
|
329
|
+
const out = [...lines];
|
|
330
|
+
out[i] = l.replace(/!=\s*None/g, 'is not None');
|
|
331
|
+
return { newContent: out.join('\n'), description: 'use identity check for None comparison' };
|
|
332
|
+
},
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
export async function mutateRepo(repoPath) {
|
|
336
|
+
const allFiles = collectFiles(repoPath);
|
|
337
|
+
if (!allFiles.length) return { changedFiles: [], description: '' };
|
|
338
|
+
|
|
339
|
+
const shuffled = [...allFiles].sort(() => Math.random() - 0.5);
|
|
340
|
+
|
|
341
|
+
for (const file of shuffled) {
|
|
342
|
+
let content;
|
|
343
|
+
try {
|
|
344
|
+
content = readFileSync(file, 'utf8');
|
|
345
|
+
} catch {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Skip minified files
|
|
350
|
+
const lines = content.split('\n');
|
|
351
|
+
if (lines.length < 5) continue;
|
|
352
|
+
if (lines.some(l => l.length > 500)) continue;
|
|
353
|
+
|
|
354
|
+
const ext = extname(file);
|
|
355
|
+
const shuffledMutations = [...mutations].sort(() => Math.random() - 0.5);
|
|
356
|
+
|
|
357
|
+
for (const mutate of shuffledMutations) {
|
|
358
|
+
let result;
|
|
359
|
+
try {
|
|
360
|
+
result = mutate(lines, ext);
|
|
361
|
+
} catch {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (result && result.newContent !== content) {
|
|
365
|
+
writeFileSync(file, result.newContent, 'utf8');
|
|
366
|
+
const relPath = relative(repoPath, file);
|
|
367
|
+
return { changedFiles: [file], description: `${result.description} in ${relPath}`, relPath };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return { changedFiles: [], description: '' };
|
|
373
|
+
}
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { mkdirSync, existsSync, renameSync, readFileSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
// All user data lives in ~/.git-cracked so it survives npx cache cleanup
|
|
7
|
+
// and package updates.
|
|
8
|
+
export const DATA_DIR = join(homedir(), '.git-cracked');
|
|
9
|
+
export const CONFIG_PATH = join(DATA_DIR, 'config.json');
|
|
10
|
+
export const ACTIVITY_PATH = join(DATA_DIR, 'activity.json');
|
|
11
|
+
|
|
12
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
13
|
+
|
|
14
|
+
// Migrate config/activity from the old in-package location (pre-1.2.0)
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const oldConfig = join(__dirname, '..', 'config.json');
|
|
17
|
+
const oldActivity = join(__dirname, '..', 'activity.json');
|
|
18
|
+
|
|
19
|
+
if (!existsSync(CONFIG_PATH) && existsSync(oldConfig)) {
|
|
20
|
+
try { renameSync(oldConfig, CONFIG_PATH); } catch {}
|
|
21
|
+
}
|
|
22
|
+
if (!existsSync(ACTIVITY_PATH) && existsSync(oldActivity)) {
|
|
23
|
+
try { renameSync(oldActivity, ACTIVITY_PATH); } catch {}
|
|
24
|
+
}
|
package/src/setup.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createInterface } from 'readline';
|
|
2
|
+
import { writeFileSync, existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { CONFIG_PATH as configPath } from './paths.js';
|
|
4
|
+
|
|
5
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
6
|
+
const ask = (q) => new Promise((res) => rl.question(q, res));
|
|
7
|
+
|
|
8
|
+
const platform = process.platform;
|
|
9
|
+
const installCmd =
|
|
10
|
+
platform === 'win32' ? 'npm run install-windows'
|
|
11
|
+
: platform === 'darwin' ? 'npm run install-mac'
|
|
12
|
+
: 'npm run install-linux';
|
|
13
|
+
|
|
14
|
+
console.log('\n=== Git Cracked Setup ===\n');
|
|
15
|
+
|
|
16
|
+
const existing = existsSync(configPath)
|
|
17
|
+
? JSON.parse(readFileSync(configPath, 'utf8'))
|
|
18
|
+
: {};
|
|
19
|
+
|
|
20
|
+
const repoPath = (await ask(`Absolute path to your private repo [${existing.repoPath ?? ''}]: `)).trim()
|
|
21
|
+
|| existing.repoPath;
|
|
22
|
+
|
|
23
|
+
if (!repoPath) {
|
|
24
|
+
console.error('Repo path is required.');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const branch = (await ask(`Branch [${existing.branch ?? 'main'}]: `)).trim()
|
|
29
|
+
|| existing.branch
|
|
30
|
+
|| 'main';
|
|
31
|
+
|
|
32
|
+
const pushInput = (await ask(`Push to remote after each commit? (y/n) [${existing.pushAfterCommit !== false ? 'y' : 'n'}]: `)).trim().toLowerCase();
|
|
33
|
+
const pushAfterCommit = pushInput ? pushInput === 'y' : existing.pushAfterCommit !== false;
|
|
34
|
+
|
|
35
|
+
const config = {
|
|
36
|
+
repoPath,
|
|
37
|
+
branch,
|
|
38
|
+
remoteName: existing.remoteName ?? 'origin',
|
|
39
|
+
pushAfterCommit,
|
|
40
|
+
schedule: existing.schedule ?? ['0 9 * * 1-5', '0 13 * * 1-5', '0 16 * * 1-5'],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
44
|
+
|
|
45
|
+
console.log(`\nConfig saved to ${configPath}`);
|
|
46
|
+
console.log('\nNext steps:');
|
|
47
|
+
console.log(' Test a commit right now: npm run commit-now');
|
|
48
|
+
console.log(' Start the scheduler: npm start');
|
|
49
|
+
console.log(` Auto-start on boot: ${installCmd}`);
|
|
50
|
+
rl.close();
|