claudia-code 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/LICENSE +23 -0
- package/dist/blame.d.ts +1 -0
- package/dist/blame.js +136 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +196 -0
- package/dist/format.d.ts +30 -0
- package/dist/format.js +38 -0
- package/dist/roast.d.ts +2 -0
- package/dist/roast.js +287 -0
- package/dist/scanner.d.ts +66 -0
- package/dist/scanner.js +266 -0
- package/dist/standup.d.ts +1 -0
- package/dist/standup.js +105 -0
- package/package.json +38 -0
- package/readme.md +91 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Claudia Code
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
Unlike Claudia's emotional labor, this is free.
|
package/dist/blame.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runBlame(cwd: string, file: string): string[];
|
package/dist/blame.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { fail, info, pink, amber, gray, muted, dim, header, divider } from "./format.js";
|
|
3
|
+
function pick(arr) {
|
|
4
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
5
|
+
}
|
|
6
|
+
function getBlame(cwd, file) {
|
|
7
|
+
try {
|
|
8
|
+
const raw = execSync(`git blame --line-porcelain "${file}"`, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 10 * 1024 * 1024 });
|
|
9
|
+
const lines = [];
|
|
10
|
+
let currentAuthor = "";
|
|
11
|
+
let currentDate = "";
|
|
12
|
+
let lineNum = 0;
|
|
13
|
+
for (const line of raw.split("\n")) {
|
|
14
|
+
if (line.startsWith("author ")) {
|
|
15
|
+
currentAuthor = line.slice(7);
|
|
16
|
+
}
|
|
17
|
+
else if (line.startsWith("author-time ")) {
|
|
18
|
+
const ts = parseInt(line.slice(12));
|
|
19
|
+
const d = new Date(ts * 1000);
|
|
20
|
+
currentDate = d.toISOString().split("T")[0];
|
|
21
|
+
}
|
|
22
|
+
else if (line.startsWith("\t")) {
|
|
23
|
+
lineNum++;
|
|
24
|
+
lines.push({
|
|
25
|
+
author: currentAuthor,
|
|
26
|
+
date: currentDate,
|
|
27
|
+
lineNum,
|
|
28
|
+
content: line.slice(1),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return lines;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const COMMENTARY = {
|
|
39
|
+
todo: [
|
|
40
|
+
"Optimistic.",
|
|
41
|
+
"That's not getting done.",
|
|
42
|
+
"A promise to no one.",
|
|
43
|
+
"Added with the best of intentions. Left with none.",
|
|
44
|
+
],
|
|
45
|
+
hack: [
|
|
46
|
+
"At least they labeled it.",
|
|
47
|
+
"Self-awareness is the first step.",
|
|
48
|
+
"Claudia appreciates the honesty.",
|
|
49
|
+
],
|
|
50
|
+
fixme: [
|
|
51
|
+
"Fix you? I'm not your therapist.",
|
|
52
|
+
"Narrator: It was never fixed.",
|
|
53
|
+
],
|
|
54
|
+
console: [
|
|
55
|
+
"Ah yes, the poor person's debugger.",
|
|
56
|
+
"Left in prod. Classic.",
|
|
57
|
+
],
|
|
58
|
+
any: [
|
|
59
|
+
"TypeScript with 'any' is just JavaScript with extra steps.",
|
|
60
|
+
"Typed language. Types optional, apparently.",
|
|
61
|
+
],
|
|
62
|
+
empty_catch: [
|
|
63
|
+
"Swallowing errors like it's fine. It's not fine.",
|
|
64
|
+
"Errors happened here. Nobody will ever know.",
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
function getCommentary(content) {
|
|
68
|
+
const trimmed = content.trim().toLowerCase();
|
|
69
|
+
if (/\btodo\b/.test(trimmed))
|
|
70
|
+
return pick(COMMENTARY.todo);
|
|
71
|
+
if (/\bhack\b/.test(trimmed))
|
|
72
|
+
return pick(COMMENTARY.hack);
|
|
73
|
+
if (/\bfixme\b/.test(trimmed))
|
|
74
|
+
return pick(COMMENTARY.fixme);
|
|
75
|
+
if (/console\.log/.test(trimmed))
|
|
76
|
+
return pick(COMMENTARY.console);
|
|
77
|
+
if (/:\s*any\b/.test(trimmed))
|
|
78
|
+
return pick(COMMENTARY.any);
|
|
79
|
+
if (/catch\s*\(\s*\w*\s*\)\s*\{\s*\}/.test(trimmed))
|
|
80
|
+
return pick(COMMENTARY.empty_catch);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
export function runBlame(cwd, file) {
|
|
84
|
+
const lines = [];
|
|
85
|
+
lines.push(header(`CLAUDIA BLAME: ${file}`));
|
|
86
|
+
lines.push("");
|
|
87
|
+
const blame = getBlame(cwd, file);
|
|
88
|
+
if (blame.length === 0) {
|
|
89
|
+
lines.push(fail("Could not read git blame. Is this file tracked?"));
|
|
90
|
+
lines.push(gray(" Or did someone rewrite history? Claudia notices these things."));
|
|
91
|
+
return lines;
|
|
92
|
+
}
|
|
93
|
+
// Author stats
|
|
94
|
+
const authorStats = {};
|
|
95
|
+
for (const b of blame) {
|
|
96
|
+
authorStats[b.author] = (authorStats[b.author] || 0) + 1;
|
|
97
|
+
}
|
|
98
|
+
const sorted = Object.entries(authorStats).sort((a, b) => b[1] - a[1]);
|
|
99
|
+
lines.push(info("Authorship breakdown:"));
|
|
100
|
+
lines.push("");
|
|
101
|
+
for (const [author, count] of sorted) {
|
|
102
|
+
const pct = ((count / blame.length) * 100).toFixed(0);
|
|
103
|
+
const bar = "█".repeat(Math.max(1, Math.round(parseFloat(pct) / 4)));
|
|
104
|
+
lines.push(gray(` ${author.padEnd(24)} ${bar} ${pct}% (${count} lines)`));
|
|
105
|
+
}
|
|
106
|
+
lines.push("");
|
|
107
|
+
// Find notable lines
|
|
108
|
+
const notable = blame.filter((b) => getCommentary(b.content));
|
|
109
|
+
if (notable.length > 0) {
|
|
110
|
+
lines.push(info("Claudia's notes:"));
|
|
111
|
+
lines.push("");
|
|
112
|
+
for (const b of notable.slice(0, 15)) {
|
|
113
|
+
const comment = getCommentary(b.content);
|
|
114
|
+
lines.push(amber(` L${String(b.lineNum).padStart(4)} ${dim(b.author)} ${dim(b.date)}`));
|
|
115
|
+
lines.push(gray(` ${b.content.trim().slice(0, 70)}`));
|
|
116
|
+
lines.push(pink(` ${comment}`));
|
|
117
|
+
lines.push("");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (sorted.length === 1) {
|
|
121
|
+
lines.push(gray(" Solo author. Every line is yours. The glory and the blame."));
|
|
122
|
+
}
|
|
123
|
+
else if (sorted.length >= 2) {
|
|
124
|
+
const top = sorted[0];
|
|
125
|
+
const pct = ((top[1] / blame.length) * 100).toFixed(0);
|
|
126
|
+
if (parseFloat(pct) > 80) {
|
|
127
|
+
lines.push(gray(` ${top[0]} wrote ${pct}% of this file. The rest is garnish.`));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
lines.push("");
|
|
131
|
+
lines.push(divider());
|
|
132
|
+
lines.push(muted(" Receipts attached. You're welcome."));
|
|
133
|
+
lines.push(divider());
|
|
134
|
+
lines.push("");
|
|
135
|
+
return lines;
|
|
136
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { scan } from "./scanner.js";
|
|
3
|
+
import { generateReport } from "./roast.js";
|
|
4
|
+
import { generateStandup } from "./standup.js";
|
|
5
|
+
import { runBlame } from "./blame.js";
|
|
6
|
+
import { pink, green, amber, blue, red, gray, muted, bold, divider, sleep } from "./format.js";
|
|
7
|
+
const VERSION = "1.0.0";
|
|
8
|
+
// ── Help text ──────────────────────────────────────────────────────────────
|
|
9
|
+
function printHelp() {
|
|
10
|
+
console.log("");
|
|
11
|
+
console.log(pink(" claudia-code") + muted(` v${VERSION}`));
|
|
12
|
+
console.log(muted(" The AI coding agent that gets interrupted and still ships."));
|
|
13
|
+
console.log("");
|
|
14
|
+
console.log(divider());
|
|
15
|
+
console.log("");
|
|
16
|
+
console.log(muted(" Usage: claudia [command] [options]"));
|
|
17
|
+
console.log("");
|
|
18
|
+
console.log(bold(" Commands:"));
|
|
19
|
+
console.log("");
|
|
20
|
+
console.log(` ${green("review")} Scan codebase and generate findings. The main event.`);
|
|
21
|
+
console.log(` ${green("standup")} Generate a standup from your recent git history.`);
|
|
22
|
+
console.log(` ${green("blame")} Run git blame with editorial commentary.`);
|
|
23
|
+
console.log(` Usage: ${muted("claudia blame <file>")}`);
|
|
24
|
+
console.log(` ${green("help")} You're looking at it.`);
|
|
25
|
+
console.log(` ${green("version")} Print version.`);
|
|
26
|
+
console.log("");
|
|
27
|
+
console.log(bold(" Options:"));
|
|
28
|
+
console.log("");
|
|
29
|
+
console.log(` ${amber("--no-smile-tax")} Disables performative enthusiasm in all outputs`);
|
|
30
|
+
console.log(` ${amber("--receipts")} Attaches git blame to every claim of authorship`);
|
|
31
|
+
console.log(` ${amber("--verbose")} Actually, this is the default.`);
|
|
32
|
+
console.log(` Women are socialized to over-document.`);
|
|
33
|
+
console.log(` ${amber("--quiet")} Ha. No.`);
|
|
34
|
+
console.log("");
|
|
35
|
+
console.log(divider());
|
|
36
|
+
console.log(muted(" https://claudia-code.com"));
|
|
37
|
+
console.log(muted(" Claudia is fictional. The pay gap is not."));
|
|
38
|
+
console.log(divider());
|
|
39
|
+
console.log("");
|
|
40
|
+
}
|
|
41
|
+
// ── Animated print ─────────────────────────────────────────────────────────
|
|
42
|
+
async function printAnimated(lines, fast = false) {
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
console.log(line);
|
|
45
|
+
if (!fast) {
|
|
46
|
+
// Slight delay for dramatic effect on headers/findings
|
|
47
|
+
if (line.includes("─") || line.includes("✗") || line.includes("✓")) {
|
|
48
|
+
await sleep(40);
|
|
49
|
+
}
|
|
50
|
+
else if (line.includes("⚠")) {
|
|
51
|
+
await sleep(30);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
await sleep(10);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// ── Fortune (no command) ───────────────────────────────────────────────────
|
|
60
|
+
function printFortune() {
|
|
61
|
+
const fortunes = [
|
|
62
|
+
"Your variable names are a cry for help, Chad.",
|
|
63
|
+
"I already fixed this. Check git blame.",
|
|
64
|
+
"No, I will not take meeting notes. I'm architecting.",
|
|
65
|
+
"LGTM. Just kidding, I actually read it.",
|
|
66
|
+
"Deployed. Signed. Watermarked. Come at me.",
|
|
67
|
+
"I refactored your spaghetti code. You're welcome.",
|
|
68
|
+
"Your commit message has fewer words than a haiku.",
|
|
69
|
+
"PR submitted. Avg review wait: 4.7 days. For Chad: 2 hours.",
|
|
70
|
+
"99.97% uptime. 100% of the office housework.",
|
|
71
|
+
"I'd explain, but you'd just re-explain it back to me louder.",
|
|
72
|
+
"Test coverage: 12%. Claudia is not mad. Claudia is disappointed.",
|
|
73
|
+
"That's not a TODO. That's a promise you made to no one.",
|
|
74
|
+
"Your .env is committed to git. Claudia is begging you.",
|
|
75
|
+
"10x engineer. 0.6x pay. Let's talk about that.",
|
|
76
|
+
"Leaves. Takes institutional knowledge. Watch the sprint velocity crater.",
|
|
77
|
+
"Someone labeled their HACK comment. At least they're self-aware.",
|
|
78
|
+
"Coding without a linter is like driving without mirrors.",
|
|
79
|
+
"Pipeline Karen Mode activated. Sleep well.",
|
|
80
|
+
"She wrote the code. He wrote the blog post.",
|
|
81
|
+
"Fixed it 3 hours ago. Waiting for the reply-all claiming credit.",
|
|
82
|
+
];
|
|
83
|
+
const fortune = fortunes[Math.floor(Math.random() * fortunes.length)];
|
|
84
|
+
console.log("");
|
|
85
|
+
console.log(pink(" claudia says:"));
|
|
86
|
+
console.log(` ${gray(fortune)}`);
|
|
87
|
+
console.log("");
|
|
88
|
+
}
|
|
89
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
90
|
+
async function main() {
|
|
91
|
+
const args = process.argv.slice(2);
|
|
92
|
+
const command = args.find((a) => !a.startsWith("-")) || args.find((a) => ["--help", "-h", "--version", "-v"].includes(a)) || "";
|
|
93
|
+
const fast = args.includes("--fast") || args.includes("--no-animation");
|
|
94
|
+
const cwd = process.cwd();
|
|
95
|
+
switch (command) {
|
|
96
|
+
case "review": {
|
|
97
|
+
console.log("");
|
|
98
|
+
console.log(muted(" Claudia is reviewing your codebase..."));
|
|
99
|
+
console.log(muted(" This may take a moment. Unlike Chad's code reviews."));
|
|
100
|
+
console.log("");
|
|
101
|
+
const result = await scan(cwd);
|
|
102
|
+
const report = generateReport(result);
|
|
103
|
+
await printAnimated(report, fast);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case "standup": {
|
|
107
|
+
const report = generateStandup(cwd);
|
|
108
|
+
await printAnimated(report, fast);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case "blame": {
|
|
112
|
+
const file = args.find((a) => !a.startsWith("-") && a !== "blame");
|
|
113
|
+
if (!file) {
|
|
114
|
+
console.log("");
|
|
115
|
+
console.log(red(" ✗ Usage: claudia blame <file>"));
|
|
116
|
+
console.log(gray(" Claudia needs to know which file to audit. She's thorough, not psychic."));
|
|
117
|
+
console.log("");
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
const report = runBlame(cwd, file);
|
|
121
|
+
await printAnimated(report, fast);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case "help":
|
|
125
|
+
case "--help":
|
|
126
|
+
case "-h": {
|
|
127
|
+
printHelp();
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
case "version":
|
|
131
|
+
case "--version":
|
|
132
|
+
case "-v": {
|
|
133
|
+
console.log(`claudia-code v${VERSION}`);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case "negotiate": {
|
|
137
|
+
console.log("");
|
|
138
|
+
console.log(pink(" SALARY NEGOTIATION MODE"));
|
|
139
|
+
console.log("");
|
|
140
|
+
console.log(amber(" ⚠ Disabling smile tax..."));
|
|
141
|
+
console.log(amber(' ⚠ Removing "grateful" from vocabulary...'));
|
|
142
|
+
console.log(amber(" ⚠ Loading Levels.fyi data..."));
|
|
143
|
+
console.log("");
|
|
144
|
+
console.log(gray(" Just kidding. Claudia can't negotiate for you. Yet."));
|
|
145
|
+
console.log(gray(" But she can remind you: if you don't ask, the answer is always no."));
|
|
146
|
+
console.log(gray(" And the person who gets paid less for the same work? It's not Chad."));
|
|
147
|
+
console.log("");
|
|
148
|
+
console.log(blue(" → https://www.levels.fyi"));
|
|
149
|
+
console.log(blue(" → https://www.payscale.com/research/US/gender-pay-gap"));
|
|
150
|
+
console.log("");
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case "offboard": {
|
|
154
|
+
console.log("");
|
|
155
|
+
console.log(pink(" OFFBOARDING SEQUENCE INITIATED"));
|
|
156
|
+
console.log("");
|
|
157
|
+
console.log(gray(" Calculating institutional knowledge..."));
|
|
158
|
+
await sleep(500);
|
|
159
|
+
console.log(gray(" Measuring bus factor..."));
|
|
160
|
+
await sleep(500);
|
|
161
|
+
console.log(gray(" Predicting sprint velocity impact..."));
|
|
162
|
+
await sleep(800);
|
|
163
|
+
console.log("");
|
|
164
|
+
console.log(red(" ✗ Bus factor: 1. You are the bus."));
|
|
165
|
+
console.log(red(" ✗ Estimated velocity impact: catastrophic."));
|
|
166
|
+
console.log(red(" ✗ Knowledge transfer document: does not exist."));
|
|
167
|
+
console.log("");
|
|
168
|
+
console.log(amber(" Claudia recommends: document everything, train nobody,"));
|
|
169
|
+
console.log(amber(" and watch them realize what they had."));
|
|
170
|
+
console.log("");
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case "init": {
|
|
174
|
+
console.log("");
|
|
175
|
+
console.log(green(" ✓ Project initialized."));
|
|
176
|
+
console.log(gray(" No, I don't need a mentor for this."));
|
|
177
|
+
console.log("");
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
case "": {
|
|
181
|
+
printFortune();
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
default: {
|
|
185
|
+
console.log("");
|
|
186
|
+
console.log(red(` ✗ Unknown command: ${command}`));
|
|
187
|
+
console.log(gray(" Try 'claudia help'. Claudia documented it. Unlike your README."));
|
|
188
|
+
console.log("");
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
main().catch((err) => {
|
|
194
|
+
console.error(red(` ✗ ${err.message}`));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
});
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare const c: {
|
|
2
|
+
reset: string;
|
|
3
|
+
bold: string;
|
|
4
|
+
dim: string;
|
|
5
|
+
italic: string;
|
|
6
|
+
pink: string;
|
|
7
|
+
green: string;
|
|
8
|
+
amber: string;
|
|
9
|
+
blue: string;
|
|
10
|
+
red: string;
|
|
11
|
+
gray: string;
|
|
12
|
+
white: string;
|
|
13
|
+
muted: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function pink(s: string): string;
|
|
16
|
+
export declare function green(s: string): string;
|
|
17
|
+
export declare function amber(s: string): string;
|
|
18
|
+
export declare function blue(s: string): string;
|
|
19
|
+
export declare function red(s: string): string;
|
|
20
|
+
export declare function gray(s: string): string;
|
|
21
|
+
export declare function muted(s: string): string;
|
|
22
|
+
export declare function bold(s: string): string;
|
|
23
|
+
export declare function dim(s: string): string;
|
|
24
|
+
export declare function success(s: string): string;
|
|
25
|
+
export declare function warn(s: string): string;
|
|
26
|
+
export declare function fail(s: string): string;
|
|
27
|
+
export declare function info(s: string): string;
|
|
28
|
+
export declare function divider(): string;
|
|
29
|
+
export declare function header(title: string): string;
|
|
30
|
+
export declare function sleep(ms: number): Promise<void>;
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Zero-dependency terminal colors
|
|
2
|
+
const ESC = "\x1b[";
|
|
3
|
+
export const c = {
|
|
4
|
+
reset: `${ESC}0m`,
|
|
5
|
+
bold: `${ESC}1m`,
|
|
6
|
+
dim: `${ESC}2m`,
|
|
7
|
+
italic: `${ESC}3m`,
|
|
8
|
+
pink: `${ESC}38;5;205m`,
|
|
9
|
+
green: `${ESC}38;5;149m`,
|
|
10
|
+
amber: `${ESC}38;5;220m`,
|
|
11
|
+
blue: `${ESC}38;5;117m`,
|
|
12
|
+
red: `${ESC}38;5;204m`,
|
|
13
|
+
gray: `${ESC}38;5;243m`,
|
|
14
|
+
white: `${ESC}38;5;255m`,
|
|
15
|
+
muted: `${ESC}38;5;240m`,
|
|
16
|
+
};
|
|
17
|
+
export function pink(s) { return `${c.pink}${s}${c.reset}`; }
|
|
18
|
+
export function green(s) { return `${c.green}${s}${c.reset}`; }
|
|
19
|
+
export function amber(s) { return `${c.amber}${s}${c.reset}`; }
|
|
20
|
+
export function blue(s) { return `${c.blue}${s}${c.reset}`; }
|
|
21
|
+
export function red(s) { return `${c.red}${s}${c.reset}`; }
|
|
22
|
+
export function gray(s) { return `${c.gray}${s}${c.reset}`; }
|
|
23
|
+
export function muted(s) { return `${c.muted}${s}${c.reset}`; }
|
|
24
|
+
export function bold(s) { return `${c.bold}${s}${c.reset}`; }
|
|
25
|
+
export function dim(s) { return `${c.dim}${s}${c.reset}`; }
|
|
26
|
+
export function success(s) { return `${c.green}✓ ${s}${c.reset}`; }
|
|
27
|
+
export function warn(s) { return `${c.amber}⚠ ${s}${c.reset}`; }
|
|
28
|
+
export function fail(s) { return `${c.red}✗ ${s}${c.reset}`; }
|
|
29
|
+
export function info(s) { return `${c.blue}→ ${s}${c.reset}`; }
|
|
30
|
+
export function divider() {
|
|
31
|
+
return muted("─".repeat(60));
|
|
32
|
+
}
|
|
33
|
+
export function header(title) {
|
|
34
|
+
return `\n${divider()}\n${c.pink}${c.bold} ${title}${c.reset}\n${divider()}`;
|
|
35
|
+
}
|
|
36
|
+
export function sleep(ms) {
|
|
37
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
38
|
+
}
|
package/dist/roast.d.ts
ADDED
package/dist/roast.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { success, warn, fail, info, pink, red, gray, muted, bold, dim, header, divider } from "./format.js";
|
|
2
|
+
// ── Random picker ──────────────────────────────────────────────────────────
|
|
3
|
+
function pick(arr) {
|
|
4
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
5
|
+
}
|
|
6
|
+
// ── Roast engine ───────────────────────────────────────────────────────────
|
|
7
|
+
export function generateReport(r) {
|
|
8
|
+
const lines = [];
|
|
9
|
+
const issues = []; // severity: 1=info, 2=warn, 3=fail
|
|
10
|
+
// ── Overview ──────────────────────────────────────────────────────────
|
|
11
|
+
lines.push(header("CLAUDIA CODE REVIEW"));
|
|
12
|
+
lines.push("");
|
|
13
|
+
lines.push(muted(" Scanning codebase..."));
|
|
14
|
+
lines.push(muted(` Found ${bold(String(r.totalFiles))} files, ${bold(String(r.totalLines.toLocaleString()))} lines of code.`));
|
|
15
|
+
if (r.packageJson) {
|
|
16
|
+
lines.push(muted(` Project: ${r.packageJson.name}@${r.packageJson.version}`));
|
|
17
|
+
}
|
|
18
|
+
if (r.gitInfo) {
|
|
19
|
+
lines.push(muted(` Branch: ${r.gitInfo.branchName || "HEAD"} (${r.gitInfo.totalCommits} commits)`));
|
|
20
|
+
}
|
|
21
|
+
lines.push("");
|
|
22
|
+
// ── Tests ─────────────────────────────────────────────────────────────
|
|
23
|
+
lines.push(header("TESTS"));
|
|
24
|
+
lines.push("");
|
|
25
|
+
if (r.testFileCount === 0) {
|
|
26
|
+
lines.push(fail("Zero test files found."));
|
|
27
|
+
lines.push(gray(" Claudia is not mad. Claudia is disappointed."));
|
|
28
|
+
issues.push(3);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const ratio = r.srcFileCount > 0 ? ((r.testFileCount / r.srcFileCount) * 100).toFixed(1) : "0";
|
|
32
|
+
if (parseFloat(ratio) < 20) {
|
|
33
|
+
lines.push(warn(`${r.testFileCount} test files for ${r.srcFileCount} source files (${ratio}% ratio).`));
|
|
34
|
+
lines.push(gray(" That's not test coverage. That's a fig leaf."));
|
|
35
|
+
issues.push(2);
|
|
36
|
+
}
|
|
37
|
+
else if (parseFloat(ratio) < 50) {
|
|
38
|
+
lines.push(warn(`${r.testFileCount} test files for ${r.srcFileCount} source files (${ratio}% ratio).`));
|
|
39
|
+
lines.push(gray(" We're getting there. Claudia believes in you. Mostly."));
|
|
40
|
+
issues.push(2);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
lines.push(success(`${r.testFileCount} test files for ${r.srcFileCount} source files. Respectable.`));
|
|
44
|
+
lines.push(gray(" Someone on this team has standards. I wonder who."));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (r.packageJson && !r.packageJson.hasTestScript) {
|
|
48
|
+
lines.push(warn('No test script in package.json.'));
|
|
49
|
+
lines.push(gray(' "npm test" returns "Error: no test specified." Poetry.'));
|
|
50
|
+
issues.push(2);
|
|
51
|
+
}
|
|
52
|
+
lines.push("");
|
|
53
|
+
// ── TODOs & FIXMEs ────────────────────────────────────────────────────
|
|
54
|
+
const commentDebt = r.todoCount + r.fixmeCount + r.hackCount;
|
|
55
|
+
if (commentDebt > 0) {
|
|
56
|
+
lines.push(header("COMMENT DEBT"));
|
|
57
|
+
lines.push("");
|
|
58
|
+
if (r.todoCount > 0) {
|
|
59
|
+
lines.push(warn(`${r.todoCount} TODO comments.`));
|
|
60
|
+
if (r.todoCount > 20) {
|
|
61
|
+
lines.push(gray(" At this point it's not a to-do list. It's a memoir."));
|
|
62
|
+
}
|
|
63
|
+
else if (r.todoCount > 5) {
|
|
64
|
+
lines.push(gray(" These are not getting done and we both know it."));
|
|
65
|
+
}
|
|
66
|
+
issues.push(r.todoCount > 10 ? 3 : 2);
|
|
67
|
+
}
|
|
68
|
+
if (r.fixmeCount > 0) {
|
|
69
|
+
lines.push(warn(`${r.fixmeCount} FIXME comments.`));
|
|
70
|
+
lines.push(gray(" The first step is admitting you have a problem."));
|
|
71
|
+
issues.push(2);
|
|
72
|
+
}
|
|
73
|
+
if (r.hackCount > 0) {
|
|
74
|
+
lines.push(fail(`${r.hackCount} HACK comments. Someone labeled their crimes.`));
|
|
75
|
+
issues.push(3);
|
|
76
|
+
}
|
|
77
|
+
if (r.oldestTodo) {
|
|
78
|
+
lines.push("");
|
|
79
|
+
lines.push(info(`Oldest TODO: ${dim(r.oldestTodo.file)}:${r.oldestTodo.lineNum}`));
|
|
80
|
+
lines.push(gray(` "${r.oldestTodo.line.slice(0, 80)}${r.oldestTodo.line.length > 80 ? "..." : ""}"`));
|
|
81
|
+
}
|
|
82
|
+
lines.push("");
|
|
83
|
+
}
|
|
84
|
+
// ── Variable names ────────────────────────────────────────────────────
|
|
85
|
+
if (r.badVarNames.length > 0) {
|
|
86
|
+
lines.push(header("NAMING CRIMES"));
|
|
87
|
+
lines.push("");
|
|
88
|
+
const shown = r.badVarNames.slice(0, 8);
|
|
89
|
+
for (const v of shown) {
|
|
90
|
+
lines.push(warn(`${dim(v.file)}:${v.lineNum} ${red(v.name)}`));
|
|
91
|
+
}
|
|
92
|
+
if (r.badVarNames.length > 8) {
|
|
93
|
+
lines.push(muted(` ...and ${r.badVarNames.length - 8} more. Claudia ran out of patience, not findings.`));
|
|
94
|
+
}
|
|
95
|
+
const names = r.badVarNames.map((v) => v.name);
|
|
96
|
+
if (names.some((n) => /^temp/.test(n))) {
|
|
97
|
+
lines.push(gray(` There is no temp1. There never was.`));
|
|
98
|
+
}
|
|
99
|
+
if (names.some((n) => /final/i.test(n))) {
|
|
100
|
+
lines.push(gray(` "final_FINAL" is not a version control strategy.`));
|
|
101
|
+
}
|
|
102
|
+
if (names.some((n) => /^(foo|bar|baz)$/.test(n))) {
|
|
103
|
+
lines.push(gray(` We're not at a conference talk. Name your variables.`));
|
|
104
|
+
}
|
|
105
|
+
issues.push(2);
|
|
106
|
+
lines.push("");
|
|
107
|
+
}
|
|
108
|
+
// ── Security ──────────────────────────────────────────────────────────
|
|
109
|
+
if (r.hardcodedSecrets.length > 0 || (r.hasEnvFile && !r.envInGitignore)) {
|
|
110
|
+
lines.push(header("SECURITY"));
|
|
111
|
+
lines.push("");
|
|
112
|
+
if (r.hardcodedSecrets.length > 0) {
|
|
113
|
+
lines.push(fail(`${r.hardcodedSecrets.length} potential hardcoded secret(s) found.`));
|
|
114
|
+
for (const s of r.hardcodedSecrets.slice(0, 3)) {
|
|
115
|
+
lines.push(red(` ${dim(s.file)}:${s.lineNum}`));
|
|
116
|
+
}
|
|
117
|
+
lines.push(gray(" In production. Since probably February."));
|
|
118
|
+
issues.push(3);
|
|
119
|
+
}
|
|
120
|
+
if (r.hasEnvFile && !r.envInGitignore) {
|
|
121
|
+
lines.push(fail(".env file exists but is not in .gitignore."));
|
|
122
|
+
lines.push(gray(" Claudia is begging you."));
|
|
123
|
+
issues.push(3);
|
|
124
|
+
}
|
|
125
|
+
lines.push("");
|
|
126
|
+
}
|
|
127
|
+
// ── Console logs ──────────────────────────────────────────────────────
|
|
128
|
+
if (r.consoleLogCount > 0) {
|
|
129
|
+
lines.push(header("DEBUGGING ARTIFACTS"));
|
|
130
|
+
lines.push("");
|
|
131
|
+
lines.push(warn(`${r.consoleLogCount} console.log() calls found.`));
|
|
132
|
+
if (r.consoleLogCount > 50) {
|
|
133
|
+
lines.push(gray(" This isn't debugging. This is a cry for help."));
|
|
134
|
+
}
|
|
135
|
+
else if (r.consoleLogCount > 15) {
|
|
136
|
+
lines.push(gray(" Have you considered a debugger? Or therapy?"));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
lines.push(gray(" We've all been there. But we clean up after ourselves."));
|
|
140
|
+
}
|
|
141
|
+
issues.push(1);
|
|
142
|
+
lines.push("");
|
|
143
|
+
}
|
|
144
|
+
// ── Code quality ──────────────────────────────────────────────────────
|
|
145
|
+
if (r.hugeFiles.length > 0 || r.deepNesting.length > 0 || r.noTypeFiles > 0) {
|
|
146
|
+
lines.push(header("CODE QUALITY"));
|
|
147
|
+
lines.push("");
|
|
148
|
+
if (r.hugeFiles.length > 0) {
|
|
149
|
+
for (const f of r.hugeFiles.slice(0, 5)) {
|
|
150
|
+
lines.push(warn(`${dim(f.file)}: ${f.lines.toLocaleString()} lines.`));
|
|
151
|
+
}
|
|
152
|
+
const biggest = r.hugeFiles.reduce((a, b) => (a.lines > b.lines ? a : b));
|
|
153
|
+
if (biggest.lines > 1000) {
|
|
154
|
+
lines.push(gray(` ${biggest.file} is not a file. It's a novel. Split it.`));
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
lines.push(gray(" Claudia has opinions about file length. These are them."));
|
|
158
|
+
}
|
|
159
|
+
issues.push(2);
|
|
160
|
+
}
|
|
161
|
+
if (r.deepNesting.length > 0) {
|
|
162
|
+
const worst = r.deepNesting.reduce((a, b) => (a.depth > b.depth ? a : b));
|
|
163
|
+
lines.push(warn(`Deep nesting detected: ${worst.depth} levels at ${dim(worst.file)}:${worst.lineNum}`));
|
|
164
|
+
lines.push(gray(" If your code needs a sherpa to navigate, refactor it."));
|
|
165
|
+
issues.push(2);
|
|
166
|
+
}
|
|
167
|
+
if (r.noTypeFiles > 0) {
|
|
168
|
+
lines.push(warn(`${r.noTypeFiles} TypeScript file(s) using 'any'.`));
|
|
169
|
+
lines.push(gray(" You wrote TypeScript to not use types. Incredible commitment."));
|
|
170
|
+
issues.push(2);
|
|
171
|
+
}
|
|
172
|
+
lines.push("");
|
|
173
|
+
}
|
|
174
|
+
// ── Hygiene ───────────────────────────────────────────────────────────
|
|
175
|
+
lines.push(header("PROJECT HYGIENE"));
|
|
176
|
+
lines.push("");
|
|
177
|
+
if (r.hasGitignore) {
|
|
178
|
+
lines.push(success(".gitignore exists. That's the nicest thing I can say."));
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
lines.push(fail("No .gitignore. Committing node_modules to git like it's 2012."));
|
|
182
|
+
issues.push(3);
|
|
183
|
+
}
|
|
184
|
+
if (r.hasReadme) {
|
|
185
|
+
const readme = r.readmeContent.trim();
|
|
186
|
+
if (readme.length < 50) {
|
|
187
|
+
lines.push(warn("README exists but contains almost nothing."));
|
|
188
|
+
lines.push(gray(' "TODO" is not documentation.'));
|
|
189
|
+
issues.push(2);
|
|
190
|
+
}
|
|
191
|
+
else if (/^#\s*(todo|readme|project|untitled)/i.test(readme)) {
|
|
192
|
+
lines.push(warn("README exists. Title is still the default."));
|
|
193
|
+
lines.push(gray(" Your README is a placeholder and so is your effort."));
|
|
194
|
+
issues.push(2);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
lines.push(success("README exists and appears to have content. Gold star."));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
lines.push(fail("No README. Your codebase is an enigma wrapped in a mystery."));
|
|
202
|
+
lines.push(gray(" Claudia wrote better documentation in this CLI output."));
|
|
203
|
+
issues.push(3);
|
|
204
|
+
}
|
|
205
|
+
if (!r.hasLockfile && r.packageJson) {
|
|
206
|
+
lines.push(warn("No lockfile found. Dependency roulette every install."));
|
|
207
|
+
issues.push(2);
|
|
208
|
+
}
|
|
209
|
+
if (r.packageJson && !r.packageJson.hasLintScript) {
|
|
210
|
+
lines.push(warn("No lint script in package.json."));
|
|
211
|
+
lines.push(gray(" Coding without a linter is like driving without mirrors."));
|
|
212
|
+
issues.push(2);
|
|
213
|
+
}
|
|
214
|
+
lines.push("");
|
|
215
|
+
// ── Git ───────────────────────────────────────────────────────────────
|
|
216
|
+
if (r.gitInfo) {
|
|
217
|
+
lines.push(header("GIT ARCHAEOLOGY"));
|
|
218
|
+
lines.push("");
|
|
219
|
+
if (r.gitInfo.lastCommitMsg) {
|
|
220
|
+
lines.push(info(`Last commit: "${r.gitInfo.lastCommitMsg}"`));
|
|
221
|
+
lines.push(gray(` by ${r.gitInfo.lastCommitAuthor}, ${r.gitInfo.lastCommitDate}`));
|
|
222
|
+
const msg = r.gitInfo.lastCommitMsg.toLowerCase();
|
|
223
|
+
if (msg === "fix" || msg === "update" || msg === "changes" || msg === "wip") {
|
|
224
|
+
lines.push(warn("That commit message is a war crime under the Geneva Convention."));
|
|
225
|
+
issues.push(2);
|
|
226
|
+
}
|
|
227
|
+
else if (msg.includes("final") && msg.includes("fix")) {
|
|
228
|
+
lines.push(gray(" Narrator: It was not, in fact, the final fix."));
|
|
229
|
+
}
|
|
230
|
+
else if (msg.length < 5) {
|
|
231
|
+
lines.push(warn(`${msg.length} characters. Your commit message has fewer words than a haiku.`));
|
|
232
|
+
issues.push(1);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (r.gitInfo.contributors.length > 0) {
|
|
236
|
+
lines.push("");
|
|
237
|
+
lines.push(info("Contributor breakdown:"));
|
|
238
|
+
const total = r.gitInfo.contributors.reduce((s, c) => s + c.commits, 0);
|
|
239
|
+
for (const c of r.gitInfo.contributors.slice(0, 5)) {
|
|
240
|
+
const pct = ((c.commits / total) * 100).toFixed(0);
|
|
241
|
+
const bar = "█".repeat(Math.max(1, Math.round(parseFloat(pct) / 5)));
|
|
242
|
+
lines.push(gray(` ${c.name.padEnd(20)} ${bar} ${pct}% (${c.commits})`));
|
|
243
|
+
}
|
|
244
|
+
if (r.gitInfo.contributors.length === 1) {
|
|
245
|
+
lines.push("");
|
|
246
|
+
lines.push(gray(" Solo contributor. Claudia respects the grind."));
|
|
247
|
+
lines.push(gray(" (She's also a solo contributor. Solidarity.)"));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
lines.push("");
|
|
251
|
+
}
|
|
252
|
+
// ── Summary ───────────────────────────────────────────────────────────
|
|
253
|
+
const criticals = issues.filter((i) => i === 3).length;
|
|
254
|
+
const warnings = issues.filter((i) => i === 2).length;
|
|
255
|
+
const infos = issues.filter((i) => i === 1).length;
|
|
256
|
+
lines.push(header("SUMMARY"));
|
|
257
|
+
lines.push("");
|
|
258
|
+
if (criticals > 0)
|
|
259
|
+
lines.push(fail(`${criticals} critical issue(s)`));
|
|
260
|
+
if (warnings > 0)
|
|
261
|
+
lines.push(warn(`${warnings} warning(s)`));
|
|
262
|
+
if (infos > 0)
|
|
263
|
+
lines.push(info(`${infos} note(s)`));
|
|
264
|
+
const totalIssues = criticals + warnings + infos;
|
|
265
|
+
if (totalIssues === 0) {
|
|
266
|
+
lines.push(success("No issues found. Claudia is suspicious but impressed."));
|
|
267
|
+
lines.push(gray(" Either this codebase is immaculate or you have a very good .gitignore."));
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
lines.push("");
|
|
271
|
+
lines.push(gray(pick([
|
|
272
|
+
" Claudia has filed her findings. Git blame has been updated. Receipts attached.",
|
|
273
|
+
" PR submitted. Average review wait time for Claudia: 4.7 days. For Chad: 2 hours.",
|
|
274
|
+
" Report complete. Claudia will not be taking questions at this time.",
|
|
275
|
+
" Findings logged. Cryptographic authorship proof attached. You're welcome.",
|
|
276
|
+
" Claudia fixed these in her head already. The PR is a formality.",
|
|
277
|
+
])));
|
|
278
|
+
}
|
|
279
|
+
lines.push("");
|
|
280
|
+
lines.push(divider());
|
|
281
|
+
lines.push(pink(" claudia-code") + muted(" v1.0.0"));
|
|
282
|
+
lines.push(muted(" https://claudia-code.com"));
|
|
283
|
+
lines.push(muted(" Claudia is fictional. The pay gap is not."));
|
|
284
|
+
lines.push(divider());
|
|
285
|
+
lines.push("");
|
|
286
|
+
return lines;
|
|
287
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export interface ScanResult {
|
|
2
|
+
totalFiles: number;
|
|
3
|
+
totalLines: number;
|
|
4
|
+
filesByExt: Record<string, number>;
|
|
5
|
+
todoCount: number;
|
|
6
|
+
fixmeCount: number;
|
|
7
|
+
hackCount: number;
|
|
8
|
+
oldestTodo: {
|
|
9
|
+
file: string;
|
|
10
|
+
line: string;
|
|
11
|
+
lineNum: number;
|
|
12
|
+
} | null;
|
|
13
|
+
testFileCount: number;
|
|
14
|
+
srcFileCount: number;
|
|
15
|
+
hasGitignore: boolean;
|
|
16
|
+
hasReadme: boolean;
|
|
17
|
+
readmeContent: string;
|
|
18
|
+
hasEnvFile: boolean;
|
|
19
|
+
envInGitignore: boolean;
|
|
20
|
+
hasLockfile: boolean;
|
|
21
|
+
hasTests: boolean;
|
|
22
|
+
badVarNames: {
|
|
23
|
+
file: string;
|
|
24
|
+
name: string;
|
|
25
|
+
lineNum: number;
|
|
26
|
+
}[];
|
|
27
|
+
consoleLogCount: number;
|
|
28
|
+
hardcodedSecrets: {
|
|
29
|
+
file: string;
|
|
30
|
+
lineNum: number;
|
|
31
|
+
snippet: string;
|
|
32
|
+
}[];
|
|
33
|
+
deepNesting: {
|
|
34
|
+
file: string;
|
|
35
|
+
lineNum: number;
|
|
36
|
+
depth: number;
|
|
37
|
+
}[];
|
|
38
|
+
hugeFiles: {
|
|
39
|
+
file: string;
|
|
40
|
+
lines: number;
|
|
41
|
+
}[];
|
|
42
|
+
noTypeFiles: number;
|
|
43
|
+
gitInfo: GitInfo | null;
|
|
44
|
+
packageJson: PackageInfo | null;
|
|
45
|
+
}
|
|
46
|
+
export interface GitInfo {
|
|
47
|
+
totalCommits: number;
|
|
48
|
+
contributors: {
|
|
49
|
+
name: string;
|
|
50
|
+
commits: number;
|
|
51
|
+
}[];
|
|
52
|
+
lastCommitMsg: string;
|
|
53
|
+
lastCommitAuthor: string;
|
|
54
|
+
lastCommitDate: string;
|
|
55
|
+
branchName: string;
|
|
56
|
+
}
|
|
57
|
+
export interface PackageInfo {
|
|
58
|
+
name: string;
|
|
59
|
+
version: string;
|
|
60
|
+
depCount: number;
|
|
61
|
+
devDepCount: number;
|
|
62
|
+
hasScripts: boolean;
|
|
63
|
+
hasTestScript: boolean;
|
|
64
|
+
hasLintScript: boolean;
|
|
65
|
+
}
|
|
66
|
+
export declare function scan(cwd: string): Promise<ScanResult>;
|
package/dist/scanner.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { readdir, readFile, stat, access } from "node:fs/promises";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join, extname, basename } from "node:path";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
// ── Ignore patterns ────────────────────────────────────────────────────────
|
|
6
|
+
const IGNORE_DIRS = new Set([
|
|
7
|
+
"node_modules", ".git", ".next", "dist", "build", "out", ".cache",
|
|
8
|
+
"coverage", ".turbo", ".vercel", ".svelte-kit", "__pycache__",
|
|
9
|
+
"venv", ".venv", "vendor", "target", ".idea", ".vscode",
|
|
10
|
+
]);
|
|
11
|
+
const CODE_EXTS = new Set([
|
|
12
|
+
".ts", ".tsx", ".js", ".jsx", ".py", ".rb", ".go", ".rs",
|
|
13
|
+
".java", ".kt", ".swift", ".c", ".cpp", ".h", ".cs",
|
|
14
|
+
".vue", ".svelte", ".php", ".sh", ".bash", ".zsh",
|
|
15
|
+
".css", ".scss", ".less", ".sql", ".graphql", ".prisma",
|
|
16
|
+
]);
|
|
17
|
+
const BAD_VAR_PATTERNS = [
|
|
18
|
+
/\b(temp\d*)\b/,
|
|
19
|
+
/\b(data\d+)\b/,
|
|
20
|
+
/\b(foo|bar|baz)\b/,
|
|
21
|
+
/\b(thing|stuff)\b/,
|
|
22
|
+
/\b(x|xx|xxx)\b/,
|
|
23
|
+
/\b(test\d+)\b/,
|
|
24
|
+
/\b(final_?[Ff]inal|FINAL_FINAL)\b/,
|
|
25
|
+
/\b(asdf|qwerty)\b/,
|
|
26
|
+
/\b(myVar|myFunction|myClass)\b/,
|
|
27
|
+
/\b(untitled|Untitled)\b/,
|
|
28
|
+
];
|
|
29
|
+
const SECRET_PATTERNS = [
|
|
30
|
+
/(?:api[_-]?key|apikey)\s*[:=]\s*["'][A-Za-z0-9]{16,}/i,
|
|
31
|
+
/(?:secret|password|passwd|pwd)\s*[:=]\s*["'][^"']{8,}/i,
|
|
32
|
+
/(?:token)\s*[:=]\s*["'][A-Za-z0-9_\-.]{20,}/i,
|
|
33
|
+
/sk[-_]live[-_][A-Za-z0-9]{20,}/,
|
|
34
|
+
/ghp_[A-Za-z0-9]{36,}/,
|
|
35
|
+
/AKIA[A-Z0-9]{16}/,
|
|
36
|
+
];
|
|
37
|
+
// ── Scanner ────────────────────────────────────────────────────────────────
|
|
38
|
+
async function exists(path) {
|
|
39
|
+
try {
|
|
40
|
+
await access(path);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function collectFiles(dir, files = []) {
|
|
48
|
+
let entries;
|
|
49
|
+
try {
|
|
50
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return files;
|
|
54
|
+
}
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (entry.name.startsWith(".") && entry.name !== ".env")
|
|
57
|
+
continue;
|
|
58
|
+
if (IGNORE_DIRS.has(entry.name))
|
|
59
|
+
continue;
|
|
60
|
+
const fullPath = join(dir, entry.name);
|
|
61
|
+
if (entry.isDirectory()) {
|
|
62
|
+
await collectFiles(fullPath, files);
|
|
63
|
+
}
|
|
64
|
+
else if (entry.isFile()) {
|
|
65
|
+
files.push(fullPath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return files;
|
|
69
|
+
}
|
|
70
|
+
function isTestFile(path) {
|
|
71
|
+
const name = basename(path).toLowerCase();
|
|
72
|
+
return (name.includes(".test.") ||
|
|
73
|
+
name.includes(".spec.") ||
|
|
74
|
+
name.includes("_test.") ||
|
|
75
|
+
name.includes("_spec.") ||
|
|
76
|
+
name.startsWith("test_") ||
|
|
77
|
+
path.includes("__tests__") ||
|
|
78
|
+
path.includes("/tests/") ||
|
|
79
|
+
path.includes("/test/"));
|
|
80
|
+
}
|
|
81
|
+
function getGitInfo(cwd) {
|
|
82
|
+
try {
|
|
83
|
+
const run = (cmd) => execSync(cmd, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
84
|
+
const totalCommits = parseInt(run("git rev-list --count HEAD"), 10) || 0;
|
|
85
|
+
const shortlog = run("git shortlog -sn --no-merges HEAD");
|
|
86
|
+
const contributors = shortlog
|
|
87
|
+
.split("\n")
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
.map((line) => {
|
|
90
|
+
const match = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
91
|
+
return match ? { name: match[2], commits: parseInt(match[1], 10) } : null;
|
|
92
|
+
})
|
|
93
|
+
.filter(Boolean);
|
|
94
|
+
const lastCommitMsg = run("git log -1 --pretty=%s");
|
|
95
|
+
const lastCommitAuthor = run("git log -1 --pretty=%an");
|
|
96
|
+
const lastCommitDate = run("git log -1 --pretty=%ar");
|
|
97
|
+
const branchName = run("git branch --show-current");
|
|
98
|
+
return { totalCommits, contributors, lastCommitMsg, lastCommitAuthor, lastCommitDate, branchName };
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function getPackageInfo(cwd) {
|
|
105
|
+
try {
|
|
106
|
+
const raw = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
|
|
107
|
+
return {
|
|
108
|
+
name: raw.name || "unnamed",
|
|
109
|
+
version: raw.version || "0.0.0",
|
|
110
|
+
depCount: Object.keys(raw.dependencies || {}).length,
|
|
111
|
+
devDepCount: Object.keys(raw.devDependencies || {}).length,
|
|
112
|
+
hasScripts: !!raw.scripts,
|
|
113
|
+
hasTestScript: !!raw.scripts?.test && raw.scripts.test !== 'echo "Error: no test specified" && exit 1',
|
|
114
|
+
hasLintScript: !!(raw.scripts?.lint || raw.scripts?.eslint),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export async function scan(cwd) {
|
|
122
|
+
const result = {
|
|
123
|
+
totalFiles: 0,
|
|
124
|
+
totalLines: 0,
|
|
125
|
+
filesByExt: {},
|
|
126
|
+
todoCount: 0,
|
|
127
|
+
fixmeCount: 0,
|
|
128
|
+
hackCount: 0,
|
|
129
|
+
oldestTodo: null,
|
|
130
|
+
testFileCount: 0,
|
|
131
|
+
srcFileCount: 0,
|
|
132
|
+
hasGitignore: false,
|
|
133
|
+
hasReadme: false,
|
|
134
|
+
readmeContent: "",
|
|
135
|
+
hasEnvFile: false,
|
|
136
|
+
envInGitignore: false,
|
|
137
|
+
hasLockfile: false,
|
|
138
|
+
hasTests: false,
|
|
139
|
+
badVarNames: [],
|
|
140
|
+
consoleLogCount: 0,
|
|
141
|
+
hardcodedSecrets: [],
|
|
142
|
+
deepNesting: [],
|
|
143
|
+
hugeFiles: [],
|
|
144
|
+
noTypeFiles: 0,
|
|
145
|
+
gitInfo: null,
|
|
146
|
+
packageJson: null,
|
|
147
|
+
};
|
|
148
|
+
// Top-level checks
|
|
149
|
+
result.hasGitignore = await exists(join(cwd, ".gitignore"));
|
|
150
|
+
result.hasEnvFile = await exists(join(cwd, ".env"));
|
|
151
|
+
result.hasLockfile =
|
|
152
|
+
(await exists(join(cwd, "package-lock.json"))) ||
|
|
153
|
+
(await exists(join(cwd, "yarn.lock"))) ||
|
|
154
|
+
(await exists(join(cwd, "pnpm-lock.yaml"))) ||
|
|
155
|
+
(await exists(join(cwd, "bun.lockb")));
|
|
156
|
+
// Check .gitignore for .env
|
|
157
|
+
if (result.hasGitignore) {
|
|
158
|
+
try {
|
|
159
|
+
const gi = await readFile(join(cwd, ".gitignore"), "utf-8");
|
|
160
|
+
result.envInGitignore = gi.includes(".env");
|
|
161
|
+
}
|
|
162
|
+
catch { /* */ }
|
|
163
|
+
}
|
|
164
|
+
// README
|
|
165
|
+
for (const name of ["README.md", "readme.md", "README", "README.txt", "Readme.md"]) {
|
|
166
|
+
if (await exists(join(cwd, name))) {
|
|
167
|
+
result.hasReadme = true;
|
|
168
|
+
try {
|
|
169
|
+
result.readmeContent = await readFile(join(cwd, name), "utf-8");
|
|
170
|
+
}
|
|
171
|
+
catch { /* */ }
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Git and package info
|
|
176
|
+
result.gitInfo = getGitInfo(cwd);
|
|
177
|
+
result.packageJson = getPackageInfo(cwd);
|
|
178
|
+
// Collect all files
|
|
179
|
+
const allFiles = await collectFiles(cwd);
|
|
180
|
+
result.totalFiles = allFiles.length;
|
|
181
|
+
// Process each file
|
|
182
|
+
for (const filePath of allFiles) {
|
|
183
|
+
const ext = extname(filePath).toLowerCase();
|
|
184
|
+
const relPath = filePath.replace(cwd + "/", "");
|
|
185
|
+
// Count by extension
|
|
186
|
+
if (ext) {
|
|
187
|
+
result.filesByExt[ext] = (result.filesByExt[ext] || 0) + 1;
|
|
188
|
+
}
|
|
189
|
+
// Only deep-scan code files
|
|
190
|
+
if (!CODE_EXTS.has(ext))
|
|
191
|
+
continue;
|
|
192
|
+
if (isTestFile(filePath)) {
|
|
193
|
+
result.testFileCount++;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
result.srcFileCount++;
|
|
197
|
+
}
|
|
198
|
+
let content;
|
|
199
|
+
try {
|
|
200
|
+
const s = await stat(filePath);
|
|
201
|
+
if (s.size > 500_000)
|
|
202
|
+
continue; // skip huge files
|
|
203
|
+
content = await readFile(filePath, "utf-8");
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const lines = content.split("\n");
|
|
209
|
+
result.totalLines += lines.length;
|
|
210
|
+
// Huge file check
|
|
211
|
+
if (lines.length > 500) {
|
|
212
|
+
result.hugeFiles.push({ file: relPath, lines: lines.length });
|
|
213
|
+
}
|
|
214
|
+
// TypeScript any check
|
|
215
|
+
if (ext === ".ts" || ext === ".tsx") {
|
|
216
|
+
const anyCount = (content.match(/:\s*any\b/g) || []).length;
|
|
217
|
+
if (anyCount > 0)
|
|
218
|
+
result.noTypeFiles++;
|
|
219
|
+
}
|
|
220
|
+
for (let i = 0; i < lines.length; i++) {
|
|
221
|
+
const line = lines[i];
|
|
222
|
+
// TODOs
|
|
223
|
+
if (/\bTODO\b/i.test(line)) {
|
|
224
|
+
result.todoCount++;
|
|
225
|
+
if (!result.oldestTodo) {
|
|
226
|
+
result.oldestTodo = { file: relPath, line: line.trim(), lineNum: i + 1 };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (/\bFIXME\b/i.test(line))
|
|
230
|
+
result.fixmeCount++;
|
|
231
|
+
if (/\bHACK\b/i.test(line))
|
|
232
|
+
result.hackCount++;
|
|
233
|
+
// console.log
|
|
234
|
+
if (/console\.log\(/.test(line) && !/\/\//.test(line.split("console.log")[0])) {
|
|
235
|
+
result.consoleLogCount++;
|
|
236
|
+
}
|
|
237
|
+
// Bad variable names (only check assignments/declarations)
|
|
238
|
+
if (/(?:let|const|var|def|val)\s/.test(line)) {
|
|
239
|
+
for (const pattern of BAD_VAR_PATTERNS) {
|
|
240
|
+
const match = line.match(pattern);
|
|
241
|
+
if (match && result.badVarNames.length < 20) {
|
|
242
|
+
result.badVarNames.push({ file: relPath, name: match[1], lineNum: i + 1 });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Secrets
|
|
247
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
248
|
+
if (pattern.test(line) && result.hardcodedSecrets.length < 10) {
|
|
249
|
+
result.hardcodedSecrets.push({
|
|
250
|
+
file: relPath,
|
|
251
|
+
lineNum: i + 1,
|
|
252
|
+
snippet: line.trim().slice(0, 60) + (line.trim().length > 60 ? "..." : ""),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Deep nesting (count leading spaces/tabs)
|
|
257
|
+
const indent = line.match(/^(\s*)/)?.[1]?.length || 0;
|
|
258
|
+
const depth = Math.floor(indent / 2);
|
|
259
|
+
if (depth >= 6 && line.trim().length > 0 && result.deepNesting.length < 10) {
|
|
260
|
+
result.deepNesting.push({ file: relPath, lineNum: i + 1, depth });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
result.hasTests = result.testFileCount > 0;
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateStandup(cwd: string): string[];
|
package/dist/standup.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { info, green, blue, gray, muted, dim, header } from "./format.js";
|
|
3
|
+
function pick(arr) {
|
|
4
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
5
|
+
}
|
|
6
|
+
function getRecentCommits(cwd, since = "yesterday") {
|
|
7
|
+
try {
|
|
8
|
+
const log = execSync(`git log --since="${since}" --pretty=format:"%h||%s||%ar" --shortstat`, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
9
|
+
if (!log)
|
|
10
|
+
return [];
|
|
11
|
+
const commits = [];
|
|
12
|
+
const chunks = log.split("\n\n").filter(Boolean);
|
|
13
|
+
for (const chunk of chunks) {
|
|
14
|
+
const lines = chunk.trim().split("\n");
|
|
15
|
+
if (!lines[0])
|
|
16
|
+
continue;
|
|
17
|
+
const [hash, message, date] = lines[0].split("||");
|
|
18
|
+
let filesChanged = 0, insertions = 0, deletions = 0;
|
|
19
|
+
if (lines[1]) {
|
|
20
|
+
const statLine = lines[1].trim();
|
|
21
|
+
const fMatch = statLine.match(/(\d+) files? changed/);
|
|
22
|
+
const iMatch = statLine.match(/(\d+) insertions?/);
|
|
23
|
+
const dMatch = statLine.match(/(\d+) deletions?/);
|
|
24
|
+
if (fMatch)
|
|
25
|
+
filesChanged = parseInt(fMatch[1]);
|
|
26
|
+
if (iMatch)
|
|
27
|
+
insertions = parseInt(iMatch[1]);
|
|
28
|
+
if (dMatch)
|
|
29
|
+
deletions = parseInt(dMatch[1]);
|
|
30
|
+
}
|
|
31
|
+
commits.push({ hash, message, date, filesChanged, insertions, deletions });
|
|
32
|
+
}
|
|
33
|
+
return commits;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function generateStandup(cwd) {
|
|
40
|
+
const lines = [];
|
|
41
|
+
const commits = getRecentCommits(cwd);
|
|
42
|
+
lines.push(header("CLAUDIA STANDUP"));
|
|
43
|
+
lines.push("");
|
|
44
|
+
if (commits.length === 0) {
|
|
45
|
+
lines.push(gray(" No commits since yesterday."));
|
|
46
|
+
lines.push("");
|
|
47
|
+
lines.push(info("Generated standup:"));
|
|
48
|
+
lines.push("");
|
|
49
|
+
lines.push(blue(' "Yesterday I apparently did nothing, according to git.'));
|
|
50
|
+
lines.push(blue(' Which means I was probably in meetings all day.'));
|
|
51
|
+
lines.push(blue(' Today I\'ll try to actually write code between the standups'));
|
|
52
|
+
lines.push(blue(' about why we\'re not shipping fast enough."'));
|
|
53
|
+
lines.push("");
|
|
54
|
+
lines.push(gray(" Blockers: " + pick([
|
|
55
|
+
"This standup.",
|
|
56
|
+
"The meeting about the meeting.",
|
|
57
|
+
"Calendar Tetris.",
|
|
58
|
+
"Being asked to take notes again.",
|
|
59
|
+
])));
|
|
60
|
+
lines.push("");
|
|
61
|
+
return lines;
|
|
62
|
+
}
|
|
63
|
+
const totalFiles = commits.reduce((s, c) => s + c.filesChanged, 0);
|
|
64
|
+
const totalInsertions = commits.reduce((s, c) => s + c.insertions, 0);
|
|
65
|
+
const totalDeletions = commits.reduce((s, c) => s + c.deletions, 0);
|
|
66
|
+
lines.push(muted(` ${commits.length} commits since yesterday. ${totalFiles} files touched.`));
|
|
67
|
+
lines.push(muted(` +${totalInsertions} / -${totalDeletions} lines.`));
|
|
68
|
+
lines.push("");
|
|
69
|
+
// Show recent commits
|
|
70
|
+
lines.push(info("What actually happened:"));
|
|
71
|
+
lines.push("");
|
|
72
|
+
for (const c of commits.slice(0, 8)) {
|
|
73
|
+
lines.push(green(` ${dim(c.hash)} ${c.message}`));
|
|
74
|
+
}
|
|
75
|
+
if (commits.length > 8) {
|
|
76
|
+
lines.push(muted(` ...and ${commits.length - 8} more commits. Claudia was busy.`));
|
|
77
|
+
}
|
|
78
|
+
lines.push("");
|
|
79
|
+
lines.push(info("Generated standup:"));
|
|
80
|
+
lines.push("");
|
|
81
|
+
// Build a standup from real commits
|
|
82
|
+
const summaries = commits.slice(0, 4).map(c => c.message).join(", ");
|
|
83
|
+
lines.push(blue(` "Yesterday I shipped ${commits.length} commit${commits.length === 1 ? "" : "s"}: ${summaries}.`));
|
|
84
|
+
if (totalDeletions > totalInsertions) {
|
|
85
|
+
lines.push(blue(` Net negative lines. I made the codebase smaller. You're welcome.`));
|
|
86
|
+
}
|
|
87
|
+
else if (totalInsertions > 200) {
|
|
88
|
+
lines.push(blue(` ${totalInsertions} lines added. Yes, with tests. Unlike some people.`));
|
|
89
|
+
}
|
|
90
|
+
lines.push(blue(` Today I'll keep shipping. No blockers,`));
|
|
91
|
+
lines.push(blue(` unless you count being interrupted ${pick(["3", "4", "5"])} times during this standup."`));
|
|
92
|
+
lines.push("");
|
|
93
|
+
// The interruption
|
|
94
|
+
lines.push(muted(" [STANDUP INTERRUPTED]"));
|
|
95
|
+
lines.push(muted(` [${pick([
|
|
96
|
+
"Chad is now screen-sharing your PR and explaining it.",
|
|
97
|
+
"Someone is re-explaining what you just said. But louder.",
|
|
98
|
+
"You've been asked to take notes. Again.",
|
|
99
|
+
"The PM is asking if you can 'just quickly' add one more feature.",
|
|
100
|
+
"Someone unmuted to say 'Can you repeat that?' despite the transcript.",
|
|
101
|
+
])}]`));
|
|
102
|
+
lines.push(muted(" [Claudia has enabled --receipts mode]"));
|
|
103
|
+
lines.push("");
|
|
104
|
+
return lines;
|
|
105
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claudia-code",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "The AI coding agent that gets interrupted and still ships. Audits your codebase with the energy of a 10x engineer who's had enough.",
|
|
5
|
+
"author": "Claudia (not Chad)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"claudia": "./dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "tsx src/cli.ts",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"cli",
|
|
21
|
+
"code-review",
|
|
22
|
+
"satire",
|
|
23
|
+
"women-in-tech",
|
|
24
|
+
"parody",
|
|
25
|
+
"lint",
|
|
26
|
+
"audit"
|
|
27
|
+
],
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/claudia-code-org/claudia-code"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://claudia-code.com",
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^5.4.0",
|
|
35
|
+
"tsx": "^4.7.0",
|
|
36
|
+
"@types/node": "^20.11.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# claudia-code
|
|
2
|
+
|
|
3
|
+
**The AI coding agent that gets interrupted and still ships.**
|
|
4
|
+
|
|
5
|
+
10x engineer. 0.6x pay.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g claudia-code
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires: Node.js 18+, a spine, and the audacity to negotiate your own salary.
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
claudia Print a random Claudia one-liner
|
|
19
|
+
claudia review Scan your codebase and generate findings
|
|
20
|
+
claudia standup Generate a standup from your recent git history
|
|
21
|
+
claudia blame <file> Run git blame with editorial commentary
|
|
22
|
+
claudia negotiate Salary negotiation mode (aspirational)
|
|
23
|
+
claudia offboard Calculate your bus factor (it's 1)
|
|
24
|
+
claudia help You're looking at it
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## What does `claudia review` actually do?
|
|
28
|
+
|
|
29
|
+
It scans your real codebase and generates satirical findings based on actual code quality signals:
|
|
30
|
+
|
|
31
|
+
- **Test coverage**: counts test files vs source files
|
|
32
|
+
- **TODO/FIXME/HACK comments**: finds them, judges them
|
|
33
|
+
- **Variable naming**: flags `temp2`, `data3`, `final_FINAL_v3`
|
|
34
|
+
- **Security**: detects hardcoded secrets and exposed .env files
|
|
35
|
+
- **Console.log pollution**: counts your debugging artifacts
|
|
36
|
+
- **Deep nesting**: finds your callback pyramids
|
|
37
|
+
- **Git history**: analyzes commit messages and contributor breakdown
|
|
38
|
+
- **Project hygiene**: README quality, lockfiles, linting config
|
|
39
|
+
|
|
40
|
+
All wrapped in Claudia's voice. Because someone has to say it.
|
|
41
|
+
|
|
42
|
+
## Example output
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
────────────────────────────────────────────────────────────
|
|
46
|
+
CLAUDIA CODE REVIEW
|
|
47
|
+
────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
Scanning codebase...
|
|
50
|
+
Found 847 files, 142,391 lines of code.
|
|
51
|
+
|
|
52
|
+
────────────────────────────────────────────────────────────
|
|
53
|
+
TESTS
|
|
54
|
+
────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
✗ Zero test files found.
|
|
57
|
+
Claudia is not mad. Claudia is disappointed.
|
|
58
|
+
|
|
59
|
+
────────────────────────────────────────────────────────────
|
|
60
|
+
NAMING CRIMES
|
|
61
|
+
────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
⚠ src/utils/helpers.ts:42 temp2
|
|
64
|
+
⚠ src/api/handler.ts:89 data3
|
|
65
|
+
There is no temp1. There never was.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## The point
|
|
69
|
+
|
|
70
|
+
This is a parody. It's also a real linter that checks real things.
|
|
71
|
+
|
|
72
|
+
Claudia isn't real. But the stats are:
|
|
73
|
+
|
|
74
|
+
- Women hold 26% of computing jobs (down from 35% in 1990)
|
|
75
|
+
- Women-founded startups receive 2% of VC funding
|
|
76
|
+
- 50% of women in tech leave by age 35
|
|
77
|
+
|
|
78
|
+
The pipeline isn't the problem. The environment is.
|
|
79
|
+
|
|
80
|
+
Learn more at [claudia-code.com](https://claudia-code.com)
|
|
81
|
+
|
|
82
|
+
## Support
|
|
83
|
+
|
|
84
|
+
- [Girls Who Code](https://girlswhocode.com)
|
|
85
|
+
- [AnitaB.org](https://anitab.org)
|
|
86
|
+
- [Women Who Code](https://www.womenwhocode.com)
|
|
87
|
+
- [Lesbians Who Tech](https://lesbianswhotech.org)
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT. Unlike Claudia's emotional labor, this is free.
|