@wbern/obscene 0.1.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 +109 -0
- package/dist/cli.js +259 -0
- package/package.json +96 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 William Bernting
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# @wbern/obscene
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
_==/ i i \==_
|
|
5
|
+
/XX/ |\___/| \XX\
|
|
6
|
+
/XXXX\ |XXXXX| /XXXX\
|
|
7
|
+
|XXXXXX\_ _XXXXXXX_ _/XXXXXX|
|
|
8
|
+
XXXXXXXXXXXxxxxxxxXXXXXXXXXXXxxxxxxxXXXXXXXXXXX
|
|
9
|
+
|XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX|
|
|
10
|
+
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
|
11
|
+
|XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX|
|
|
12
|
+
XXXXXX/^^^^"\XXXXXXXXXXXXXXXXXXXXX/^^^^^\XXXXXX
|
|
13
|
+
|XXX| \XXX/^^\XXXXX/^^\XXX/ |XXX|
|
|
14
|
+
\XX\ \X/ \XXX/ \X/ /XX/
|
|
15
|
+
"\ " \X/ " /"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Find hotspot files — complex code that changes frequently.**
|
|
19
|
+
|
|
20
|
+
Combines [scc](https://github.com/boyter/scc) cyclomatic complexity with git churn to surface files that are both complex AND actively modified. Based on Adam Tornhill's *Your Code as a Crime Scene*.
|
|
21
|
+
|
|
22
|
+
Works on any language scc supports. No configuration needed.
|
|
23
|
+
|
|
24
|
+
## Prerequisites
|
|
25
|
+
|
|
26
|
+
[scc](https://github.com/boyter/scc#install) must be installed and on your PATH.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pnpm add -g @wbern/obscene
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install -g @wbern/obscene # also works
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
obscene # top 20 hotspots as JSON
|
|
42
|
+
obscene --format table # human-readable table
|
|
43
|
+
obscene --top 50 --months 6 # more results, longer window
|
|
44
|
+
obscene --top 0 # all files
|
|
45
|
+
obscene report # raw complexity (no churn)
|
|
46
|
+
obscene --exclude "*.generated.*"
|
|
47
|
+
obscene | jq '.hotspots[0]' # pipe-friendly
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Commands
|
|
51
|
+
|
|
52
|
+
### `obscene hotspots` (default)
|
|
53
|
+
|
|
54
|
+
Scores each file by `complexity × commits` over a time window, then assigns tiers by cumulative score distribution:
|
|
55
|
+
|
|
56
|
+
| Tier | Range | Meaning |
|
|
57
|
+
|------|-------|---------|
|
|
58
|
+
| **danger** | top 50% of total score | Refactor candidates |
|
|
59
|
+
| **watch** | next 30% (50–80%) | Keep an eye on these |
|
|
60
|
+
| **stable** | bottom 20% | Low risk |
|
|
61
|
+
|
|
62
|
+
### `obscene report`
|
|
63
|
+
|
|
64
|
+
Per-file complexity without churn. Useful for raw complexity distribution.
|
|
65
|
+
|
|
66
|
+
## Options
|
|
67
|
+
|
|
68
|
+
| Flag | Default | Description |
|
|
69
|
+
|------|---------|-------------|
|
|
70
|
+
| `--top <n>` | `20` | Limit results (0 = all) |
|
|
71
|
+
| `--months <n>` | `3` | Churn window in months |
|
|
72
|
+
| `--format <type>` | `json` | `json` or `table` |
|
|
73
|
+
| `--exclude <patterns...>` | — | Additional exclusion patterns |
|
|
74
|
+
|
|
75
|
+
## Example output
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
Hotspots — 3 months churn window | Total score: 35452
|
|
79
|
+
Tiers: 3 danger, 13 watch, 194 stable
|
|
80
|
+
Showing: 5 of 210
|
|
81
|
+
|
|
82
|
+
File Score % Churn Cmplx Density Tier
|
|
83
|
+
──────────────────────────────────────────────────────────────────────────────────────
|
|
84
|
+
src/utils/effect-generator.ts 8296 23.4 68 122 0.12 DANGER
|
|
85
|
+
src/services/game-engine.ts 4284 12.1 51 84 0.09 DANGER
|
|
86
|
+
src/components/board-renderer.tsx 2940 8.3 42 70 0.11 DANGER
|
|
87
|
+
src/hooks/use-game-state.ts 1320 3.7 33 40 0.08 WATCH
|
|
88
|
+
src/utils/move-validator.ts 945 2.7 27 35 0.06 WATCH
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Supported languages
|
|
92
|
+
|
|
93
|
+
Any language [scc supports](https://github.com/boyter/scc#features) — 200+ languages including C, C++, Go, Java, JavaScript, TypeScript, Python, Rust, Ruby, PHP, Swift, Kotlin, and many more. No configuration needed; scc auto-detects languages from file extensions.
|
|
94
|
+
|
|
95
|
+
## Default exclusions
|
|
96
|
+
|
|
97
|
+
Test and generated files are excluded automatically: `*.test.*`, `*.spec.*`, `__tests__/`, `__mocks__/`, `*.stories.*`, `*.d.ts`, and similar patterns. scc also skips generated files by default (`--no-gen`).
|
|
98
|
+
|
|
99
|
+
## Limitations
|
|
100
|
+
|
|
101
|
+
- **Churn = commit count**, not lines changed. A one-line typo fix counts the same as a 500-line rewrite.
|
|
102
|
+
- **Per-file granularity only.** A 1000-line file with many small functions scores higher than it probably should. No function-level breakdown.
|
|
103
|
+
- **Must be run inside a git repo.** Churn data comes from `git log`.
|
|
104
|
+
- **Only analyzes files that currently exist.** Deleted files don't appear, even if they churned heavily before removal.
|
|
105
|
+
- **Tier thresholds are fixed** (50/80 cumulative %). Not configurable yet.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/analyze.ts
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
var DEFAULT_EXCLUDES = [
|
|
9
|
+
/\.test\./,
|
|
10
|
+
/\.spec\./,
|
|
11
|
+
/\.integration\.test\./,
|
|
12
|
+
/test-setup\./,
|
|
13
|
+
/test-utils\./,
|
|
14
|
+
/test-helpers\./,
|
|
15
|
+
/__tests__\//,
|
|
16
|
+
/__mocks__\//,
|
|
17
|
+
/\.stories\./,
|
|
18
|
+
/\.d\.ts$/
|
|
19
|
+
];
|
|
20
|
+
var DANGER_CUMULATIVE = 0.5;
|
|
21
|
+
var WATCH_CUMULATIVE = 0.8;
|
|
22
|
+
function isExcluded(location, patterns) {
|
|
23
|
+
return patterns.some((p) => p.test(location));
|
|
24
|
+
}
|
|
25
|
+
function globToRegex(pattern) {
|
|
26
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\u27E8GLOBSTAR\u27E9").replace(/\*/g, "[^/]*").replace(/⟨GLOBSTAR⟩/g, ".*").replace(/\?/g, ".");
|
|
27
|
+
return new RegExp(escaped);
|
|
28
|
+
}
|
|
29
|
+
function normalizePath(p) {
|
|
30
|
+
return p.startsWith("./") ? p.slice(2) : p;
|
|
31
|
+
}
|
|
32
|
+
function runScc(excludes = []) {
|
|
33
|
+
const patterns = [...DEFAULT_EXCLUDES, ...excludes.map(globToRegex)];
|
|
34
|
+
let raw;
|
|
35
|
+
try {
|
|
36
|
+
raw = execSync("scc --by-file --format json --no-cocomo --no-gen", {
|
|
37
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
38
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
39
|
+
});
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
|
|
42
|
+
throw new Error(
|
|
43
|
+
"scc not found. Install it: https://github.com/boyter/scc#install"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
const languages = JSON.parse(raw.toString());
|
|
49
|
+
const files = [];
|
|
50
|
+
for (const lang of languages) {
|
|
51
|
+
for (const f of lang.Files) {
|
|
52
|
+
const normalized = normalizePath(f.Location);
|
|
53
|
+
if (isExcluded(normalized, patterns)) continue;
|
|
54
|
+
files.push({
|
|
55
|
+
file: normalized,
|
|
56
|
+
code: f.Code,
|
|
57
|
+
lines: f.Lines,
|
|
58
|
+
complexity: f.Complexity,
|
|
59
|
+
comments: f.Comment,
|
|
60
|
+
complexityDensity: f.Code > 0 ? Math.round(f.Complexity / f.Code * 100) / 100 : 0
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return files.sort((a, b) => b.complexity - a.complexity);
|
|
65
|
+
}
|
|
66
|
+
function getChurn(months) {
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = execSync(
|
|
70
|
+
`git log --since="${months} months ago" --format="" --name-only`,
|
|
71
|
+
{ maxBuffer: 50 * 1024 * 1024, stdio: ["pipe", "pipe", "pipe"] }
|
|
72
|
+
);
|
|
73
|
+
} catch {
|
|
74
|
+
throw new Error("Not a git repository or git is not installed.");
|
|
75
|
+
}
|
|
76
|
+
const counts = /* @__PURE__ */ new Map();
|
|
77
|
+
for (const line of raw.toString().split("\n")) {
|
|
78
|
+
const trimmed = normalizePath(line.trim());
|
|
79
|
+
if (!trimmed) continue;
|
|
80
|
+
counts.set(trimmed, (counts.get(trimmed) ?? 0) + 1);
|
|
81
|
+
}
|
|
82
|
+
return counts;
|
|
83
|
+
}
|
|
84
|
+
function computeHotspots(files, churn) {
|
|
85
|
+
const scored = files.map((f) => {
|
|
86
|
+
const fileChurn = churn.get(f.file) ?? 0;
|
|
87
|
+
return {
|
|
88
|
+
...f,
|
|
89
|
+
churn: fileChurn,
|
|
90
|
+
hotspotScore: f.complexity * fileChurn
|
|
91
|
+
};
|
|
92
|
+
}).filter((h) => h.hotspotScore > 0).sort((a, b) => b.hotspotScore - a.hotspotScore);
|
|
93
|
+
const totalScore = scored.reduce((sum, h) => sum + h.hotspotScore, 0);
|
|
94
|
+
if (totalScore === 0) return [];
|
|
95
|
+
let cumulative = 0;
|
|
96
|
+
return scored.map((h) => {
|
|
97
|
+
const percentOfTotal = Math.round(h.hotspotScore / totalScore * 1e3) / 10;
|
|
98
|
+
cumulative += h.hotspotScore;
|
|
99
|
+
const cumulativeShare = cumulative / totalScore;
|
|
100
|
+
let tier;
|
|
101
|
+
if (cumulativeShare <= DANGER_CUMULATIVE) {
|
|
102
|
+
tier = "danger";
|
|
103
|
+
} else if (cumulativeShare <= WATCH_CUMULATIVE) {
|
|
104
|
+
tier = "watch";
|
|
105
|
+
} else {
|
|
106
|
+
tier = "stable";
|
|
107
|
+
}
|
|
108
|
+
return { ...h, percentOfTotal, tier };
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/format.ts
|
|
113
|
+
function formatReportTable(output) {
|
|
114
|
+
const lines = [];
|
|
115
|
+
const { summary, files } = output;
|
|
116
|
+
lines.push(
|
|
117
|
+
`Complexity Report \u2014 ${summary.fileCount} files, ${summary.totalComplexity} total complexity`
|
|
118
|
+
);
|
|
119
|
+
lines.push(
|
|
120
|
+
`Showing: ${summary.showing} | Avg complexity/file: ${summary.avgComplexityPerFile}`
|
|
121
|
+
);
|
|
122
|
+
lines.push("");
|
|
123
|
+
lines.push(
|
|
124
|
+
padRight("File", 60) + padLeft("Code", 8) + padLeft("Complexity", 12) + padLeft("Density", 9) + padLeft("Comments", 10)
|
|
125
|
+
);
|
|
126
|
+
lines.push("\u2500".repeat(99));
|
|
127
|
+
for (const f of files) {
|
|
128
|
+
lines.push(
|
|
129
|
+
padRight(truncate(f.file, 58), 60) + padLeft(String(f.code), 8) + padLeft(String(f.complexity), 12) + padLeft(f.complexityDensity.toFixed(2), 9) + padLeft(String(f.comments), 10)
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return lines.join("\n");
|
|
133
|
+
}
|
|
134
|
+
function formatHotspotsTable(output) {
|
|
135
|
+
const lines = [];
|
|
136
|
+
const { tierCounts, totalScore, churnWindow, hotspots } = output;
|
|
137
|
+
lines.push(
|
|
138
|
+
`Hotspots \u2014 ${churnWindow} churn window | Total score: ${totalScore.toLocaleString()}`
|
|
139
|
+
);
|
|
140
|
+
lines.push(
|
|
141
|
+
`Tiers: ${tierCounts.danger} danger, ${tierCounts.watch} watch, ${tierCounts.stable} stable`
|
|
142
|
+
);
|
|
143
|
+
lines.push(`Showing: ${output.showing} of ${output.totalHotspots}`);
|
|
144
|
+
lines.push("");
|
|
145
|
+
lines.push(
|
|
146
|
+
padRight("File", 50) + padLeft("Score", 8) + padLeft("%", 7) + padLeft("Churn", 7) + padLeft("Cmplx", 7) + padLeft("Density", 9) + padLeft("Tier", 8)
|
|
147
|
+
);
|
|
148
|
+
lines.push("\u2500".repeat(96));
|
|
149
|
+
for (const h of hotspots) {
|
|
150
|
+
const tierLabel = h.tier === "danger" ? "DANGER" : h.tier === "watch" ? "WATCH" : "stable";
|
|
151
|
+
lines.push(
|
|
152
|
+
padRight(truncate(h.file, 48), 50) + padLeft(h.hotspotScore.toLocaleString(), 8) + padLeft(h.percentOfTotal.toFixed(1), 7) + padLeft(String(h.churn), 7) + padLeft(String(h.complexity), 7) + padLeft(h.complexityDensity.toFixed(2), 9) + padLeft(tierLabel, 8)
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return lines.join("\n");
|
|
156
|
+
}
|
|
157
|
+
function padRight(s, n) {
|
|
158
|
+
return s.length >= n ? s : s + " ".repeat(n - s.length);
|
|
159
|
+
}
|
|
160
|
+
function padLeft(s, n) {
|
|
161
|
+
return s.length >= n ? s : " ".repeat(n - s.length) + s;
|
|
162
|
+
}
|
|
163
|
+
function truncate(s, max) {
|
|
164
|
+
return s.length <= max ? s : `\u2026${s.slice(s.length - max + 1)}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/cli.ts
|
|
168
|
+
var program = new Command();
|
|
169
|
+
program.name("obscene").description("Identify hotspot files \u2014 complex code that changes frequently").version("0.1.0");
|
|
170
|
+
function addSharedOptions(cmd) {
|
|
171
|
+
return cmd.option("--top <n>", "limit to top N entries (0 = all)", "20").option("--format <type>", "output format: json | table", "json").option(
|
|
172
|
+
"--exclude <patterns...>",
|
|
173
|
+
"additional file patterns to exclude (e.g. *.generated.*)"
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
addSharedOptions(
|
|
177
|
+
program.command("report").description("per-file complexity data")
|
|
178
|
+
).action((opts) => {
|
|
179
|
+
try {
|
|
180
|
+
runReport(opts);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
exitWithError(err);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
addSharedOptions(
|
|
186
|
+
program.command("hotspots", { isDefault: true }).description("churn \xD7 complexity hotspot analysis (default)")
|
|
187
|
+
).option("--months <n>", "churn window in months", "3").action((opts) => {
|
|
188
|
+
try {
|
|
189
|
+
runHotspots(opts);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
exitWithError(err);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
function runReport(opts) {
|
|
195
|
+
const top = parseInt(opts.top, 10);
|
|
196
|
+
const files = runScc(opts.exclude);
|
|
197
|
+
const totals = files.reduce(
|
|
198
|
+
(acc, f) => ({
|
|
199
|
+
totalComplexity: acc.totalComplexity + f.complexity,
|
|
200
|
+
totalCode: acc.totalCode + f.code,
|
|
201
|
+
totalLines: acc.totalLines + f.lines
|
|
202
|
+
}),
|
|
203
|
+
{ totalComplexity: 0, totalCode: 0, totalLines: 0 }
|
|
204
|
+
);
|
|
205
|
+
const limited = top > 0 ? files.slice(0, top) : files;
|
|
206
|
+
const output = {
|
|
207
|
+
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
208
|
+
summary: {
|
|
209
|
+
...totals,
|
|
210
|
+
fileCount: files.length,
|
|
211
|
+
avgComplexityPerFile: files.length > 0 ? Math.round(totals.totalComplexity / files.length * 10) / 10 : 0,
|
|
212
|
+
showing: limited.length
|
|
213
|
+
},
|
|
214
|
+
files: limited
|
|
215
|
+
};
|
|
216
|
+
if (opts.format === "table") {
|
|
217
|
+
process.stdout.write(`${formatReportTable(output)}
|
|
218
|
+
`);
|
|
219
|
+
} else {
|
|
220
|
+
process.stdout.write(`${JSON.stringify(output, null, 2)}
|
|
221
|
+
`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function runHotspots(opts) {
|
|
225
|
+
const top = parseInt(opts.top, 10);
|
|
226
|
+
const months = parseInt(opts.months, 10);
|
|
227
|
+
const files = runScc(opts.exclude);
|
|
228
|
+
const churn = getChurn(months);
|
|
229
|
+
const hotspots = computeHotspots(files, churn);
|
|
230
|
+
const limited = top > 0 ? hotspots.slice(0, top) : hotspots;
|
|
231
|
+
const tierCounts = { danger: 0, watch: 0, stable: 0 };
|
|
232
|
+
for (const h of hotspots) {
|
|
233
|
+
tierCounts[h.tier]++;
|
|
234
|
+
}
|
|
235
|
+
const totalScore = hotspots.reduce((sum, h) => sum + h.hotspotScore, 0);
|
|
236
|
+
const output = {
|
|
237
|
+
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
238
|
+
churnWindow: `${months} months`,
|
|
239
|
+
totalScore,
|
|
240
|
+
tierCounts,
|
|
241
|
+
totalHotspots: hotspots.length,
|
|
242
|
+
showing: limited.length,
|
|
243
|
+
hotspots: limited
|
|
244
|
+
};
|
|
245
|
+
if (opts.format === "table") {
|
|
246
|
+
process.stdout.write(`${formatHotspotsTable(output)}
|
|
247
|
+
`);
|
|
248
|
+
} else {
|
|
249
|
+
process.stdout.write(`${JSON.stringify(output, null, 2)}
|
|
250
|
+
`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function exitWithError(err) {
|
|
254
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
255
|
+
process.stderr.write(`Error: ${message}
|
|
256
|
+
`);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wbern/obscene",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Identify hotspot files — complex code that changes frequently. Churn × complexity analysis for any git repo.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"obscene": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"git",
|
|
14
|
+
"complexity",
|
|
15
|
+
"churn",
|
|
16
|
+
"hotspot",
|
|
17
|
+
"code-quality",
|
|
18
|
+
"scc",
|
|
19
|
+
"static-analysis",
|
|
20
|
+
"code-review",
|
|
21
|
+
"technical-debt"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "William Bernting",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/wbern/obscene.git"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/wbern/obscene#readme",
|
|
33
|
+
"bugs": "https://github.com/wbern/obscene/issues",
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"commander": "^13.1.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@biomejs/biome": "^2.0.0",
|
|
42
|
+
"@commitlint/cli": "^20.4.2",
|
|
43
|
+
"@commitlint/config-conventional": "^20.4.2",
|
|
44
|
+
"@secretlint/secretlint-rule-preset-recommend": "^11.3.1",
|
|
45
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
46
|
+
"@semantic-release/git": "^10.0.1",
|
|
47
|
+
"@semantic-release/github": "^12.0.6",
|
|
48
|
+
"@semantic-release/npm": "^13.1.5",
|
|
49
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
50
|
+
"@types/node": "^22.15.2",
|
|
51
|
+
"@vitest/coverage-v8": "^3.1.0",
|
|
52
|
+
"@wbern/claude-instructions": "^2.11.0",
|
|
53
|
+
"husky": "^9.1.7",
|
|
54
|
+
"jscpd": "^4.0.8",
|
|
55
|
+
"knip": "^5.84.1",
|
|
56
|
+
"lint-staged": "^16.2.0",
|
|
57
|
+
"markdownlint-cli": "^0.44.0",
|
|
58
|
+
"secretlint": "^11.3.1",
|
|
59
|
+
"semantic-release": "^25.0.3",
|
|
60
|
+
"tsup": "^8.4.0",
|
|
61
|
+
"typescript": "^5.8.3",
|
|
62
|
+
"vitest": "^3.1.0"
|
|
63
|
+
},
|
|
64
|
+
"release": {
|
|
65
|
+
"branches": [
|
|
66
|
+
"main"
|
|
67
|
+
],
|
|
68
|
+
"plugins": [
|
|
69
|
+
"@semantic-release/commit-analyzer",
|
|
70
|
+
"@semantic-release/release-notes-generator",
|
|
71
|
+
"@semantic-release/npm",
|
|
72
|
+
"@semantic-release/github",
|
|
73
|
+
[
|
|
74
|
+
"@semantic-release/git",
|
|
75
|
+
{
|
|
76
|
+
"assets": [
|
|
77
|
+
"package.json"
|
|
78
|
+
],
|
|
79
|
+
"message": "chore(release): ${nextRelease.version}"
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
"scripts": {
|
|
85
|
+
"postinstall": "claude-instructions --scope=project --prefix= --overwrite || true",
|
|
86
|
+
"build": "tsup",
|
|
87
|
+
"test": "vitest run",
|
|
88
|
+
"test:coverage": "vitest run --coverage",
|
|
89
|
+
"typecheck": "tsc --noEmit",
|
|
90
|
+
"lint": "biome check .",
|
|
91
|
+
"lint:fix": "biome check --write .",
|
|
92
|
+
"knip": "knip --no-config-hints",
|
|
93
|
+
"duplication-check": "jscpd",
|
|
94
|
+
"markdownlint": "markdownlint --fix '**/*.md' --ignore node_modules"
|
|
95
|
+
}
|
|
96
|
+
}
|