claude-crap 0.3.5 → 0.3.7

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/dashboard/file-detail.d.ts +77 -0
  3. package/dist/dashboard/file-detail.d.ts.map +1 -0
  4. package/dist/dashboard/file-detail.js +120 -0
  5. package/dist/dashboard/file-detail.js.map +1 -0
  6. package/dist/dashboard/server.d.ts +3 -0
  7. package/dist/dashboard/server.d.ts.map +1 -1
  8. package/dist/dashboard/server.js +108 -1
  9. package/dist/dashboard/server.js.map +1 -1
  10. package/dist/index.js +19 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/scanner/auto-scan.d.ts +8 -1
  13. package/dist/scanner/auto-scan.d.ts.map +1 -1
  14. package/dist/scanner/auto-scan.js +14 -1
  15. package/dist/scanner/auto-scan.js.map +1 -1
  16. package/dist/scanner/complexity-scanner.d.ts +54 -0
  17. package/dist/scanner/complexity-scanner.d.ts.map +1 -0
  18. package/dist/scanner/complexity-scanner.js +176 -0
  19. package/dist/scanner/complexity-scanner.js.map +1 -0
  20. package/package.json +1 -1
  21. package/plugin/.claude-plugin/plugin.json +1 -1
  22. package/plugin/bundle/dashboard/public/index.html +432 -12
  23. package/plugin/bundle/mcp-server.mjs +429 -71
  24. package/plugin/bundle/mcp-server.mjs.map +4 -4
  25. package/plugin/package-lock.json +2 -2
  26. package/plugin/package.json +1 -1
  27. package/scripts/bundle-plugin.mjs +53 -2
  28. package/src/dashboard/file-detail.ts +197 -0
  29. package/src/dashboard/public/index.html +432 -12
  30. package/src/dashboard/server.ts +141 -1
  31. package/src/index.ts +20 -2
  32. package/src/scanner/auto-scan.ts +26 -0
  33. package/src/scanner/complexity-scanner.ts +233 -0
  34. package/src/tests/complexity-scanner.test.ts +263 -0
  35. package/src/tests/file-detail-api.test.ts +258 -0
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-crap-plugin",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "claude-crap-plugin",
9
- "version": "0.3.5",
9
+ "version": "0.3.7",
10
10
  "dependencies": {
11
11
  "@fastify/static": "^8.0.3",
12
12
  "@modelcontextprotocol/sdk": "^1.0.4",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-crap-plugin",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "private": true,
5
5
  "description": "Runtime dependencies for the claude-crap plugin bundle",
6
6
  "type": "module",
@@ -1,8 +1,9 @@
1
1
  // scripts/bundle-plugin.mjs
2
2
  import { build } from "esbuild";
3
- import { cp, mkdir, rm } from "node:fs/promises";
3
+ import { cp, mkdir, readdir, rm, stat } from "node:fs/promises";
4
4
  import { execFileSync } from "node:child_process";
5
- import { resolve } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { join, resolve } from "node:path";
6
7
  import { fileURLToPath } from "node:url";
7
8
 
8
9
  const ROOT = resolve(fileURLToPath(import.meta.url), "../..");
@@ -96,6 +97,56 @@ async function main() {
96
97
  `warning: could not generate plugin/package-lock.json: ${err.message}\n`,
97
98
  );
98
99
  }
100
+ // 6. Sync to plugin cache — if Claude Code has cached a previous
101
+ // version of this plugin, update it in-place so the next session
102
+ // picks up the new build without a manual `/plugin install`.
103
+ // This is the developer fast path: `npm run build:plugin` is all
104
+ // you need after editing source. Normal users who install from
105
+ // the marketplace never hit this code.
106
+ await syncToPluginCache(resolve(ROOT, "plugin"));
107
+ }
108
+
109
+ /**
110
+ * Find every claude-crap version directory under the Claude Code
111
+ * plugin cache and copy the freshly built plugin files into each one.
112
+ * Skips `node_modules/` and `.claude-crap/` (runtime state that
113
+ * belongs to the installed copy, not the source).
114
+ */
115
+ async function syncToPluginCache(pluginDir) {
116
+ const cacheBase = join(homedir(), ".claude", "plugins", "cache", "herz", "claude-crap");
117
+
118
+ let versionDirs;
119
+ try {
120
+ versionDirs = await readdir(cacheBase);
121
+ } catch {
122
+ // No cache exists — nothing to sync (first install hasn't happened).
123
+ return;
124
+ }
125
+
126
+ for (const version of versionDirs) {
127
+ const cacheDir = join(cacheBase, version);
128
+ const s = await stat(cacheDir).catch(() => null);
129
+ if (!s?.isDirectory()) continue;
130
+
131
+ try {
132
+ // Copy every plugin file/directory EXCEPT node_modules and .claude-crap
133
+ const entries = await readdir(pluginDir);
134
+ for (const entry of entries) {
135
+ if (entry === "node_modules" || entry === ".claude-crap") continue;
136
+ const src = join(pluginDir, entry);
137
+ const dst = join(cacheDir, entry);
138
+ const srcStat = await stat(src);
139
+ if (srcStat.isDirectory()) {
140
+ await cp(src, dst, { recursive: true, force: true });
141
+ } else {
142
+ await cp(src, dst, { force: true });
143
+ }
144
+ }
145
+ process.stderr.write(` ✓ synced plugin cache: ${cacheDir}\n`);
146
+ } catch (err) {
147
+ process.stderr.write(` ⚠ cache sync failed for ${cacheDir}: ${err.message}\n`);
148
+ }
149
+ }
99
150
  }
100
151
 
101
152
  main().catch((err) => {
@@ -0,0 +1,197 @@
1
+ /**
2
+ * File detail builder for the dashboard.
3
+ *
4
+ * Given a workspace-relative file path, this module produces a rich
5
+ * detail payload combining source code, per-function AST metrics, and
6
+ * SARIF findings filtered to that file. The dashboard uses this to
7
+ * render a ReportGenerator-style annotated code view.
8
+ *
9
+ * The builder is extracted into its own module (rather than inlined in
10
+ * `server.ts`) so that:
11
+ * - The logic is unit-testable without booting the HTTP server.
12
+ * - The types are importable by both the Fastify route and tests.
13
+ *
14
+ * @module dashboard/file-detail
15
+ */
16
+
17
+ import { promises as fs } from "node:fs";
18
+ import { join } from "node:path";
19
+
20
+ import { resolveWithinWorkspace } from "../workspace-guard.js";
21
+ import { detectLanguageFromPath, type SupportedLanguage } from "../ast/language-config.js";
22
+ import type { TreeSitterEngine, FunctionMetrics } from "../ast/tree-sitter-engine.js";
23
+ import type { SarifStore, IngestedFinding } from "../sarif/sarif-store.js";
24
+
25
+ // ── Types ─────────────────────────────────────────────────────────
26
+
27
+ /** Per-function entry in the detail response. */
28
+ export interface FileDetailFunction {
29
+ readonly name: string;
30
+ readonly startLine: number;
31
+ readonly endLine: number;
32
+ readonly cyclomaticComplexity: number;
33
+ readonly lineCount: number;
34
+ }
35
+
36
+ /** Per-finding entry in the detail response. */
37
+ export interface FileDetailFinding {
38
+ readonly ruleId: string;
39
+ readonly level: string;
40
+ readonly message: string;
41
+ readonly sourceTool: string;
42
+ readonly startLine: number;
43
+ readonly startColumn: number;
44
+ readonly endLine: number;
45
+ readonly endColumn: number;
46
+ readonly effortMinutes: number;
47
+ }
48
+
49
+ /** Summary statistics for the file. */
50
+ export interface FileDetailSummary {
51
+ readonly totalFindings: number;
52
+ readonly errorCount: number;
53
+ readonly warningCount: number;
54
+ readonly noteCount: number;
55
+ readonly totalEffortMinutes: number;
56
+ readonly avgComplexity: number;
57
+ readonly maxComplexity: number;
58
+ }
59
+
60
+ /** Full response payload for the file detail endpoint. */
61
+ export interface FileDetailResponse {
62
+ readonly filePath: string;
63
+ readonly language: SupportedLanguage | null;
64
+ readonly physicalLoc: number;
65
+ readonly logicalLoc: number;
66
+ readonly cyclomaticMax: number;
67
+ readonly sourceLines: string[];
68
+ readonly functions: FileDetailFunction[];
69
+ readonly findings: FileDetailFinding[];
70
+ readonly summary: FileDetailSummary;
71
+ }
72
+
73
+ /** Input accepted by {@link buildFileDetail}. */
74
+ export interface BuildFileDetailInput {
75
+ readonly relativePath: string;
76
+ readonly workspaceRoot: string;
77
+ readonly astEngine?: TreeSitterEngine | undefined;
78
+ readonly sarifStore: SarifStore;
79
+ readonly cyclomaticMax: number;
80
+ }
81
+
82
+ // ── Builder ───────────────────────────────────────────────────────
83
+
84
+ /**
85
+ * Build the file detail payload. Pure function aside from the file
86
+ * read and the tree-sitter analysis (both deterministic for a given
87
+ * file).
88
+ *
89
+ * @throws When the file does not exist or the path escapes the workspace.
90
+ */
91
+ export async function buildFileDetail(
92
+ input: BuildFileDetailInput,
93
+ ): Promise<FileDetailResponse> {
94
+ const { relativePath, workspaceRoot, astEngine, sarifStore, cyclomaticMax } = input;
95
+
96
+ // 1. Guard against path traversal
97
+ const absolutePath = resolveWithinWorkspace(workspaceRoot, relativePath);
98
+
99
+ // 2. Read source
100
+ const source = await fs.readFile(absolutePath, "utf8");
101
+ const sourceLines = source.split(/\r?\n/);
102
+ // Remove trailing empty line from files ending with \n
103
+ if (sourceLines.length > 0 && sourceLines[sourceLines.length - 1] === "") {
104
+ sourceLines.pop();
105
+ }
106
+
107
+ const physicalLoc = sourceLines.length;
108
+ let logicalLoc = 0;
109
+ for (const line of sourceLines) {
110
+ if (line.trim().length > 0) logicalLoc += 1;
111
+ }
112
+
113
+ // 3. AST analysis (if language is supported)
114
+ const language = detectLanguageFromPath(relativePath);
115
+ let functions: FileDetailFunction[] = [];
116
+
117
+ if (language && astEngine) {
118
+ try {
119
+ const metrics = await astEngine.analyzeFile({
120
+ filePath: absolutePath,
121
+ language,
122
+ });
123
+ functions = metrics.functions.map((fn: FunctionMetrics) => ({
124
+ name: fn.name,
125
+ startLine: fn.startLine,
126
+ endLine: fn.endLine,
127
+ cyclomaticComplexity: fn.cyclomaticComplexity,
128
+ lineCount: fn.lineCount,
129
+ }));
130
+ } catch {
131
+ // Analysis failure is non-fatal — return empty functions
132
+ }
133
+ }
134
+
135
+ // 4. Filter SARIF findings for this file
136
+ const allFindings = sarifStore.list();
137
+ const fileFindings = allFindings.filter(
138
+ (f: IngestedFinding) => f.location.uri === relativePath,
139
+ );
140
+
141
+ const findings: FileDetailFinding[] = fileFindings.map((f: IngestedFinding) => ({
142
+ ruleId: f.ruleId,
143
+ level: f.level,
144
+ message: f.message,
145
+ sourceTool: f.sourceTool,
146
+ startLine: f.location.startLine,
147
+ startColumn: f.location.startColumn,
148
+ endLine: f.location.endLine ?? f.location.startLine,
149
+ endColumn: f.location.endColumn ?? 0,
150
+ effortMinutes:
151
+ typeof f.properties?.effortMinutes === "number"
152
+ ? f.properties.effortMinutes
153
+ : 0,
154
+ }));
155
+
156
+ // 5. Build summary
157
+ let errorCount = 0;
158
+ let warningCount = 0;
159
+ let noteCount = 0;
160
+ let totalEffortMinutes = 0;
161
+
162
+ for (const f of findings) {
163
+ if (f.level === "error") errorCount += 1;
164
+ else if (f.level === "warning") warningCount += 1;
165
+ else if (f.level === "note") noteCount += 1;
166
+ totalEffortMinutes += f.effortMinutes;
167
+ }
168
+
169
+ const complexities = functions.map((f) => f.cyclomaticComplexity);
170
+ const maxComplexity = complexities.length > 0 ? Math.max(...complexities) : 0;
171
+ const avgComplexity =
172
+ complexities.length > 0
173
+ ? Math.round(
174
+ (complexities.reduce((a, b) => a + b, 0) / complexities.length) * 100,
175
+ ) / 100
176
+ : 0;
177
+
178
+ return {
179
+ filePath: relativePath,
180
+ language,
181
+ physicalLoc,
182
+ logicalLoc,
183
+ cyclomaticMax,
184
+ sourceLines,
185
+ functions,
186
+ findings,
187
+ summary: {
188
+ totalFindings: findings.length,
189
+ errorCount,
190
+ warningCount,
191
+ noteCount,
192
+ totalEffortMinutes,
193
+ avgComplexity,
194
+ maxComplexity,
195
+ },
196
+ };
197
+ }