claude-crap 0.1.2
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/CHANGELOG.md +308 -0
- package/LICENSE +21 -0
- package/README.md +550 -0
- package/bin/claude-crap.mjs +141 -0
- package/dist/adapters/bandit.d.ts +48 -0
- package/dist/adapters/bandit.d.ts.map +1 -0
- package/dist/adapters/bandit.js +145 -0
- package/dist/adapters/bandit.js.map +1 -0
- package/dist/adapters/common.d.ts +73 -0
- package/dist/adapters/common.d.ts.map +1 -0
- package/dist/adapters/common.js +78 -0
- package/dist/adapters/common.js.map +1 -0
- package/dist/adapters/eslint.d.ts +52 -0
- package/dist/adapters/eslint.d.ts.map +1 -0
- package/dist/adapters/eslint.js +142 -0
- package/dist/adapters/eslint.js.map +1 -0
- package/dist/adapters/index.d.ts +47 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +64 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/semgrep.d.ts +30 -0
- package/dist/adapters/semgrep.d.ts.map +1 -0
- package/dist/adapters/semgrep.js +130 -0
- package/dist/adapters/semgrep.js.map +1 -0
- package/dist/adapters/stryker.d.ts +55 -0
- package/dist/adapters/stryker.d.ts.map +1 -0
- package/dist/adapters/stryker.js +165 -0
- package/dist/adapters/stryker.js.map +1 -0
- package/dist/ast/cyclomatic.d.ts +48 -0
- package/dist/ast/cyclomatic.d.ts.map +1 -0
- package/dist/ast/cyclomatic.js +106 -0
- package/dist/ast/cyclomatic.js.map +1 -0
- package/dist/ast/index.d.ts +26 -0
- package/dist/ast/index.d.ts.map +1 -0
- package/dist/ast/index.js +23 -0
- package/dist/ast/index.js.map +1 -0
- package/dist/ast/language-config.d.ts +70 -0
- package/dist/ast/language-config.d.ts.map +1 -0
- package/dist/ast/language-config.js +192 -0
- package/dist/ast/language-config.js.map +1 -0
- package/dist/ast/tree-sitter-engine.d.ts +133 -0
- package/dist/ast/tree-sitter-engine.d.ts.map +1 -0
- package/dist/ast/tree-sitter-engine.js +270 -0
- package/dist/ast/tree-sitter-engine.js.map +1 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +78 -0
- package/dist/config.js.map +1 -0
- package/dist/crap-config.d.ts +97 -0
- package/dist/crap-config.d.ts.map +1 -0
- package/dist/crap-config.js +144 -0
- package/dist/crap-config.js.map +1 -0
- package/dist/dashboard/server.d.ts +65 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +147 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +574 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/crap.d.ts +71 -0
- package/dist/metrics/crap.d.ts.map +1 -0
- package/dist/metrics/crap.js +67 -0
- package/dist/metrics/crap.js.map +1 -0
- package/dist/metrics/index.d.ts +31 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +27 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/score.d.ts +143 -0
- package/dist/metrics/score.d.ts.map +1 -0
- package/dist/metrics/score.js +224 -0
- package/dist/metrics/score.js.map +1 -0
- package/dist/metrics/tdr.d.ts +106 -0
- package/dist/metrics/tdr.d.ts.map +1 -0
- package/dist/metrics/tdr.js +117 -0
- package/dist/metrics/tdr.js.map +1 -0
- package/dist/metrics/workspace-walker.d.ts +43 -0
- package/dist/metrics/workspace-walker.d.ts.map +1 -0
- package/dist/metrics/workspace-walker.js +137 -0
- package/dist/metrics/workspace-walker.js.map +1 -0
- package/dist/sarif/index.d.ts +21 -0
- package/dist/sarif/index.d.ts.map +1 -0
- package/dist/sarif/index.js +19 -0
- package/dist/sarif/index.js.map +1 -0
- package/dist/sarif/sarif-builder.d.ts +128 -0
- package/dist/sarif/sarif-builder.d.ts.map +1 -0
- package/dist/sarif/sarif-builder.js +79 -0
- package/dist/sarif/sarif-builder.js.map +1 -0
- package/dist/sarif/sarif-store.d.ts +205 -0
- package/dist/sarif/sarif-store.d.ts.map +1 -0
- package/dist/sarif/sarif-store.js +246 -0
- package/dist/sarif/sarif-store.js.map +1 -0
- package/dist/sarif/sarif-validator.d.ts +45 -0
- package/dist/sarif/sarif-validator.d.ts.map +1 -0
- package/dist/sarif/sarif-validator.js +138 -0
- package/dist/sarif/sarif-validator.js.map +1 -0
- package/dist/schemas/tool-schemas.d.ts +216 -0
- package/dist/schemas/tool-schemas.d.ts.map +1 -0
- package/dist/schemas/tool-schemas.js +208 -0
- package/dist/schemas/tool-schemas.js.map +1 -0
- package/dist/sdk.d.ts +45 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +44 -0
- package/dist/sdk.js.map +1 -0
- package/dist/tools/index.d.ts +24 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/test-harness.d.ts +75 -0
- package/dist/tools/test-harness.d.ts.map +1 -0
- package/dist/tools/test-harness.js +137 -0
- package/dist/tools/test-harness.js.map +1 -0
- package/dist/workspace-guard.d.ts +53 -0
- package/dist/workspace-guard.d.ts.map +1 -0
- package/dist/workspace-guard.js +61 -0
- package/dist/workspace-guard.js.map +1 -0
- package/package.json +133 -0
- package/plugin/.claude-plugin/plugin.json +29 -0
- package/plugin/.mcp.json +18 -0
- package/plugin/CLAUDE.md +143 -0
- package/plugin/bundle/dashboard/public/index.html +368 -0
- package/plugin/bundle/dashboard/public/vendor/vue.global.prod.js +9 -0
- package/plugin/bundle/mcp-server.mjs +8718 -0
- package/plugin/bundle/mcp-server.mjs.map +7 -0
- package/plugin/bundle/tdr-engine.mjs +50 -0
- package/plugin/bundle/tdr-engine.mjs.map +7 -0
- package/plugin/hooks/hooks.json +62 -0
- package/plugin/hooks/lib/crap-config.mjs +152 -0
- package/plugin/hooks/lib/gatekeeper-rules.mjs +257 -0
- package/plugin/hooks/lib/hook-io.mjs +151 -0
- package/plugin/hooks/lib/quality-gate.mjs +329 -0
- package/plugin/hooks/lib/test-harness.mjs +152 -0
- package/plugin/hooks/post-tool-use.mjs +245 -0
- package/plugin/hooks/pre-tool-use.mjs +290 -0
- package/plugin/hooks/session-start.mjs +109 -0
- package/plugin/hooks/stop-quality-gate.mjs +226 -0
- package/plugin/package.json +18 -0
- package/plugin/skills/adopt/SKILL.md +74 -0
- package/plugin/skills/analyze/SKILL.md +77 -0
- package/plugin/skills/check-test/SKILL.md +50 -0
- package/plugin/skills/score/SKILL.md +31 -0
- package/scripts/bug-report.mjs +328 -0
- package/scripts/build-fast.mjs +130 -0
- package/scripts/bundle-plugin.mjs +74 -0
- package/scripts/doctor.mjs +320 -0
- package/scripts/install.mjs +192 -0
- package/scripts/lib/cli-ui.mjs +122 -0
- package/scripts/postinstall.mjs +127 -0
- package/scripts/run-tests.mjs +95 -0
- package/scripts/status.mjs +110 -0
- package/scripts/uninstall.mjs +72 -0
- package/src/adapters/bandit.ts +191 -0
- package/src/adapters/common.ts +133 -0
- package/src/adapters/eslint.ts +187 -0
- package/src/adapters/index.ts +78 -0
- package/src/adapters/semgrep.ts +150 -0
- package/src/adapters/stryker.ts +218 -0
- package/src/ast/cyclomatic.ts +131 -0
- package/src/ast/index.ts +33 -0
- package/src/ast/language-config.ts +231 -0
- package/src/ast/tree-sitter-engine.ts +385 -0
- package/src/config.ts +109 -0
- package/src/crap-config.ts +196 -0
- package/src/dashboard/public/index.html +368 -0
- package/src/dashboard/public/vendor/vue.global.prod.js +9 -0
- package/src/dashboard/server.ts +205 -0
- package/src/index.ts +696 -0
- package/src/metrics/crap.ts +101 -0
- package/src/metrics/index.ts +51 -0
- package/src/metrics/score.ts +329 -0
- package/src/metrics/tdr.ts +155 -0
- package/src/metrics/workspace-walker.ts +146 -0
- package/src/sarif/index.ts +31 -0
- package/src/sarif/sarif-builder.ts +139 -0
- package/src/sarif/sarif-store.ts +347 -0
- package/src/sarif/sarif-validator.ts +145 -0
- package/src/schemas/tool-schemas.ts +225 -0
- package/src/sdk.ts +110 -0
- package/src/tests/adapters/bandit.test.ts +111 -0
- package/src/tests/adapters/dispatch.test.ts +100 -0
- package/src/tests/adapters/eslint.test.ts +138 -0
- package/src/tests/adapters/semgrep.test.ts +125 -0
- package/src/tests/adapters/stryker.test.ts +103 -0
- package/src/tests/crap-config.test.ts +228 -0
- package/src/tests/crap.test.ts +59 -0
- package/src/tests/cyclomatic.test.ts +87 -0
- package/src/tests/dashboard-http.test.ts +108 -0
- package/src/tests/dashboard-integrity.test.ts +128 -0
- package/src/tests/integration/mcp-server.integration.test.ts +352 -0
- package/src/tests/pre-tool-use-hook.test.ts +178 -0
- package/src/tests/sarif-store.test.ts +241 -0
- package/src/tests/sarif-validator.test.ts +164 -0
- package/src/tests/score.test.ts +260 -0
- package/src/tests/skills-frontmatter.test.ts +172 -0
- package/src/tests/stop-quality-gate-strictness.test.ts +243 -0
- package/src/tests/tdr.test.ts +86 -0
- package/src/tests/test-harness.test.ts +153 -0
- package/src/tests/workspace-guard.test.ts +111 -0
- package/src/tools/index.ts +24 -0
- package/src/tools/test-harness.ts +158 -0
- package/src/workspace-guard.ts +64 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* `claude-crap doctor` — full diagnostic pass.
|
|
4
|
+
*
|
|
5
|
+
* Runs every check the install script runs, plus several deeper probes
|
|
6
|
+
* that make sure the plugin can actually function against the current
|
|
7
|
+
* workspace:
|
|
8
|
+
*
|
|
9
|
+
* - Node.js runtime ≥ 20
|
|
10
|
+
* - Plugin structure sanity (package.json, .claude-plugin/plugin.json,
|
|
11
|
+
* .mcp.json, hooks/hooks.json)
|
|
12
|
+
* - `dist/index.js` exists (built)
|
|
13
|
+
* - Hook scripts are executable
|
|
14
|
+
* - tree-sitter runtime WASM is reachable
|
|
15
|
+
* - tree-sitter language grammars (c_sharp, javascript, typescript,
|
|
16
|
+
* python, java) are all present
|
|
17
|
+
* - Dashboard port is free on 127.0.0.1
|
|
18
|
+
* - SARIF reports directory is writable in the current workspace
|
|
19
|
+
*
|
|
20
|
+
* Exits 0 when all checks pass, 1 when any check fails, and 2 when
|
|
21
|
+
* there are only warnings. This makes it easy to embed in CI:
|
|
22
|
+
* `claude-crap doctor && echo ok`.
|
|
23
|
+
*
|
|
24
|
+
* @module scripts/doctor
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { promises as fs, constants as fsConstants } from "node:fs";
|
|
28
|
+
import { createServer } from "node:net";
|
|
29
|
+
import { join, resolve } from "node:path";
|
|
30
|
+
|
|
31
|
+
import { printBanner, printStep, paint, icons } from "./lib/cli-ui.mjs";
|
|
32
|
+
|
|
33
|
+
const SUPPORTED_LANGUAGES = /** @type {const} */ ([
|
|
34
|
+
{ id: "c_sharp", wasm: "tree-sitter-c_sharp.wasm" },
|
|
35
|
+
{ id: "javascript", wasm: "tree-sitter-javascript.wasm" },
|
|
36
|
+
{ id: "typescript", wasm: "tree-sitter-typescript.wasm" },
|
|
37
|
+
{ id: "python", wasm: "tree-sitter-python.wasm" },
|
|
38
|
+
{ id: "java", wasm: "tree-sitter-java.wasm" },
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {Object} CommandContext
|
|
43
|
+
* @property {string} pluginRoot
|
|
44
|
+
* @property {string[]} argv
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Doctor entrypoint.
|
|
49
|
+
*
|
|
50
|
+
* @param {CommandContext} ctx
|
|
51
|
+
* @returns {Promise<number>}
|
|
52
|
+
*/
|
|
53
|
+
export default async function doctor(ctx) {
|
|
54
|
+
printBanner("claude-crap :: doctor");
|
|
55
|
+
|
|
56
|
+
const checks = [];
|
|
57
|
+
checks.push(await checkNodeVersion());
|
|
58
|
+
checks.push(await checkPluginStructure(ctx.pluginRoot));
|
|
59
|
+
checks.push(await checkDist(ctx.pluginRoot));
|
|
60
|
+
checks.push(await checkHooksExecutable(ctx.pluginRoot));
|
|
61
|
+
checks.push(await checkTreeSitterRuntime(ctx.pluginRoot));
|
|
62
|
+
checks.push(...(await checkGrammars(ctx.pluginRoot)));
|
|
63
|
+
checks.push(await checkDashboardPort());
|
|
64
|
+
checks.push(await checkReportsWritable(process.cwd()));
|
|
65
|
+
|
|
66
|
+
for (const step of checks) printStep(step);
|
|
67
|
+
|
|
68
|
+
const fails = checks.filter((c) => c.status === "fail").length;
|
|
69
|
+
const warns = checks.filter((c) => c.status === "warn").length;
|
|
70
|
+
|
|
71
|
+
process.stdout.write(
|
|
72
|
+
`\n${paint.bold("Summary:")} ${checks.length - fails - warns} ok, ${warns} warn, ${fails} fail\n`,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (fails > 0) {
|
|
76
|
+
process.stdout.write(
|
|
77
|
+
`${paint.red(icons.fail)} At least one check failed. Fix the issues above and re-run ${paint.bold("claude-crap doctor")}.\n`,
|
|
78
|
+
);
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
if (warns > 0) {
|
|
82
|
+
process.stdout.write(
|
|
83
|
+
`${paint.yellow(icons.warn)} Checks passed with warnings. The plugin should still work.\n`,
|
|
84
|
+
);
|
|
85
|
+
return 2;
|
|
86
|
+
}
|
|
87
|
+
process.stdout.write(`${paint.green(icons.ok)} All checks passed. claude-crap is ready.\n`);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @returns {Promise<import("./lib/cli-ui.mjs").StepResult>} */
|
|
92
|
+
async function checkNodeVersion() {
|
|
93
|
+
const raw = process.versions.node;
|
|
94
|
+
const major = Number(raw.split(".")[0]);
|
|
95
|
+
if (!Number.isFinite(major) || major < 20) {
|
|
96
|
+
return {
|
|
97
|
+
status: "fail",
|
|
98
|
+
label: `Node.js runtime (${raw})`,
|
|
99
|
+
detail: `claude-crap requires Node.js ≥ 20.0.0`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return { status: "ok", label: `Node.js runtime (${raw})` };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {string} pluginRoot
|
|
107
|
+
* @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
|
|
108
|
+
*/
|
|
109
|
+
async function checkPluginStructure(pluginRoot) {
|
|
110
|
+
const required = [
|
|
111
|
+
"package.json",
|
|
112
|
+
"plugin/.claude-plugin/plugin.json",
|
|
113
|
+
"plugin/.mcp.json",
|
|
114
|
+
"plugin/CLAUDE.md",
|
|
115
|
+
"plugin/hooks/hooks.json",
|
|
116
|
+
"plugin/hooks/pre-tool-use.mjs",
|
|
117
|
+
"plugin/hooks/post-tool-use.mjs",
|
|
118
|
+
"plugin/hooks/stop-quality-gate.mjs",
|
|
119
|
+
"plugin/hooks/session-start.mjs",
|
|
120
|
+
];
|
|
121
|
+
const missing = [];
|
|
122
|
+
for (const rel of required) {
|
|
123
|
+
try {
|
|
124
|
+
await fs.access(join(pluginRoot, rel));
|
|
125
|
+
} catch {
|
|
126
|
+
missing.push(rel);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (missing.length > 0) {
|
|
130
|
+
return {
|
|
131
|
+
status: "fail",
|
|
132
|
+
label: `Plugin files present`,
|
|
133
|
+
detail: `Missing: ${missing.join(", ")}`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return { status: "ok", label: `Plugin files present (${required.length} checked)` };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {string} pluginRoot
|
|
141
|
+
* @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
|
|
142
|
+
*/
|
|
143
|
+
async function checkDist(pluginRoot) {
|
|
144
|
+
const npmEntry = join(pluginRoot, "dist", "index.js");
|
|
145
|
+
const gitEntry = join(pluginRoot, "plugin", "bundle", "mcp-server.mjs");
|
|
146
|
+
|
|
147
|
+
let npmOk = false;
|
|
148
|
+
let gitOk = false;
|
|
149
|
+
let npmAge, gitAge;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const stat = await fs.stat(npmEntry);
|
|
153
|
+
npmAge = Math.round((Date.now() - stat.mtimeMs) / (1000 * 60 * 60));
|
|
154
|
+
npmOk = true;
|
|
155
|
+
} catch {}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const stat = await fs.stat(gitEntry);
|
|
159
|
+
gitAge = Math.round((Date.now() - stat.mtimeMs) / (1000 * 60 * 60));
|
|
160
|
+
gitOk = true;
|
|
161
|
+
} catch {}
|
|
162
|
+
|
|
163
|
+
const details = [];
|
|
164
|
+
if (npmOk) details.push(`dist/index.js (~${npmAge}h)`);
|
|
165
|
+
if (gitOk) details.push(`plugin/bundle/mcp-server.mjs (~${gitAge}h)`);
|
|
166
|
+
|
|
167
|
+
if (!npmOk && !gitOk) {
|
|
168
|
+
return {
|
|
169
|
+
status: "fail",
|
|
170
|
+
label: `Server entrypoints built`,
|
|
171
|
+
detail: `Both dist/ and plugin/bundle/ are missing. Run \`npm run build && npm run build:plugin\`.`
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
status: npmOk && gitOk ? "ok" : "warn",
|
|
177
|
+
label: `Server entrypoints built`,
|
|
178
|
+
detail: details.join(", ") + (!npmOk || !gitOk ? " (one is missing)" : "")
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @param {string} pluginRoot
|
|
184
|
+
* @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
|
|
185
|
+
*/
|
|
186
|
+
async function checkHooksExecutable(pluginRoot) {
|
|
187
|
+
const hooks = ["pre-tool-use.mjs", "post-tool-use.mjs", "stop-quality-gate.mjs", "session-start.mjs"];
|
|
188
|
+
const notExec = [];
|
|
189
|
+
for (const name of hooks) {
|
|
190
|
+
const full = join(pluginRoot, "plugin", "hooks", name);
|
|
191
|
+
try {
|
|
192
|
+
await fs.access(full, fsConstants.X_OK);
|
|
193
|
+
} catch {
|
|
194
|
+
notExec.push(name);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (notExec.length > 0) {
|
|
198
|
+
return {
|
|
199
|
+
status: "warn",
|
|
200
|
+
label: `Hooks are executable`,
|
|
201
|
+
detail:
|
|
202
|
+
`${notExec.length} hook(s) lack the executable bit: ${notExec.join(", ")}. ` +
|
|
203
|
+
`Run \`claude-crap install\` to fix.`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return { status: "ok", label: `Hooks are executable (4 checked)` };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @param {string} pluginRoot
|
|
211
|
+
* @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
|
|
212
|
+
*/
|
|
213
|
+
async function checkTreeSitterRuntime(pluginRoot) {
|
|
214
|
+
const runtime = join(pluginRoot, "node_modules", "web-tree-sitter", "tree-sitter.wasm");
|
|
215
|
+
try {
|
|
216
|
+
const stat = await fs.stat(runtime);
|
|
217
|
+
return {
|
|
218
|
+
status: "ok",
|
|
219
|
+
label: `tree-sitter runtime WASM present`,
|
|
220
|
+
detail: `${runtime} (${stat.size} bytes)`,
|
|
221
|
+
};
|
|
222
|
+
} catch {
|
|
223
|
+
return {
|
|
224
|
+
status: "fail",
|
|
225
|
+
label: `tree-sitter runtime WASM present`,
|
|
226
|
+
detail: `Missing ${runtime}. Run \`npm install\` from ${pluginRoot}.`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @param {string} pluginRoot
|
|
233
|
+
* @returns {Promise<import("./lib/cli-ui.mjs").StepResult[]>}
|
|
234
|
+
*/
|
|
235
|
+
async function checkGrammars(pluginRoot) {
|
|
236
|
+
const base = join(pluginRoot, "node_modules", "tree-sitter-wasms", "out");
|
|
237
|
+
/** @type {import("./lib/cli-ui.mjs").StepResult[]} */
|
|
238
|
+
const results = [];
|
|
239
|
+
for (const lang of SUPPORTED_LANGUAGES) {
|
|
240
|
+
const full = join(base, lang.wasm);
|
|
241
|
+
try {
|
|
242
|
+
await fs.access(full);
|
|
243
|
+
results.push({ status: "ok", label: `Grammar: ${lang.id}` });
|
|
244
|
+
} catch {
|
|
245
|
+
results.push({
|
|
246
|
+
status: "fail",
|
|
247
|
+
label: `Grammar: ${lang.id}`,
|
|
248
|
+
detail: `Missing ${full}. Run \`npm install\`.`,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return results;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Probe the configured dashboard port on 127.0.0.1. We attempt to
|
|
257
|
+
* open a TCP listener ourselves — if it succeeds the port is free,
|
|
258
|
+
* if it fails with EADDRINUSE then something is already holding it.
|
|
259
|
+
*
|
|
260
|
+
* @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
|
|
261
|
+
*/
|
|
262
|
+
async function checkDashboardPort() {
|
|
263
|
+
const raw = process.env.CLAUDE_PLUGIN_OPTION_DASHBOARD_PORT;
|
|
264
|
+
const port = Number(raw ?? 5117);
|
|
265
|
+
if (!Number.isFinite(port)) {
|
|
266
|
+
return {
|
|
267
|
+
status: "warn",
|
|
268
|
+
label: `Dashboard port configured`,
|
|
269
|
+
detail: `CLAUDE_PLUGIN_OPTION_DASHBOARD_PORT=${raw} is not a number`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
return await new Promise((resolvePromise) => {
|
|
273
|
+
const server = createServer();
|
|
274
|
+
server.once("error", (err) => {
|
|
275
|
+
const nodeErr = /** @type {NodeJS.ErrnoException} */ (err);
|
|
276
|
+
if (nodeErr.code === "EADDRINUSE") {
|
|
277
|
+
resolvePromise({
|
|
278
|
+
status: "warn",
|
|
279
|
+
label: `Dashboard port ${port} is free`,
|
|
280
|
+
detail:
|
|
281
|
+
`Port ${port} is already in use. The dashboard will refuse to start ` +
|
|
282
|
+
`but the MCP server will keep running.`,
|
|
283
|
+
});
|
|
284
|
+
} else {
|
|
285
|
+
resolvePromise({
|
|
286
|
+
status: "warn",
|
|
287
|
+
label: `Dashboard port ${port} is free`,
|
|
288
|
+
detail: nodeErr.message,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
server.listen({ port, host: "127.0.0.1" }, () => {
|
|
293
|
+
server.close(() => {
|
|
294
|
+
resolvePromise({ status: "ok", label: `Dashboard port ${port} is free` });
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* @param {string} workspace
|
|
302
|
+
* @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
|
|
303
|
+
*/
|
|
304
|
+
async function checkReportsWritable(workspace) {
|
|
305
|
+
const dir = resolve(workspace, ".claude-crap", "reports");
|
|
306
|
+
try {
|
|
307
|
+
await fs.mkdir(dir, { recursive: true });
|
|
308
|
+
// Try to write a tiny probe file and then immediately unlink it.
|
|
309
|
+
const probe = join(dir, `.doctor-${process.pid}`);
|
|
310
|
+
await fs.writeFile(probe, "ok");
|
|
311
|
+
await fs.unlink(probe);
|
|
312
|
+
return { status: "ok", label: `SARIF reports directory writable`, detail: dir };
|
|
313
|
+
} catch (err) {
|
|
314
|
+
return {
|
|
315
|
+
status: "fail",
|
|
316
|
+
label: `SARIF reports directory writable`,
|
|
317
|
+
detail: `${dir} :: ${/** @type {Error} */ (err).message}`,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* `claude-crap install` — prepare the workspace and print the Claude
|
|
4
|
+
* Code registration command.
|
|
5
|
+
*
|
|
6
|
+
* This subcommand does every bit of side-effecty work that the plugin
|
|
7
|
+
* needs to run cleanly AND nothing else. We intentionally do NOT try to
|
|
8
|
+
* edit Claude Code's own settings file — that surface is owned by
|
|
9
|
+
* Claude Code's `/plugin install` command, and manipulating it behind
|
|
10
|
+
* the user's back would be a support nightmare. Instead, we:
|
|
11
|
+
*
|
|
12
|
+
* 1. Verify Node.js and the plugin directory look sane.
|
|
13
|
+
* 2. Ensure `dist/` exists (postinstall should have built it, but we
|
|
14
|
+
* still check so a manual clone also works).
|
|
15
|
+
* 3. `chmod +x` the hook scripts and the bin entrypoint (defensive —
|
|
16
|
+
* npm should handle this but tarballs sometimes lose the bits).
|
|
17
|
+
* 4. Create `.claude-crap/reports/` inside the current workspace so
|
|
18
|
+
* the SARIF store can write without a race on its first ingestion.
|
|
19
|
+
* 5. Print the exact Claude Code command the user needs to run next.
|
|
20
|
+
*
|
|
21
|
+
* Exits 0 on success and 1 on any preparation failure.
|
|
22
|
+
*
|
|
23
|
+
* @module scripts/install
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { promises as fs, constants as fsConstants } from "node:fs";
|
|
27
|
+
import { resolve, join } from "node:path";
|
|
28
|
+
|
|
29
|
+
import { printBanner, printStep, paint, icons } from "./lib/cli-ui.mjs";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {Object} CommandContext
|
|
33
|
+
* @property {string} pluginRoot Absolute path to the plugin root (this dir).
|
|
34
|
+
* @property {string[]} argv Remaining CLI arguments after the subcommand name.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Entrypoint invoked by `bin/claude-crap.mjs`.
|
|
39
|
+
*
|
|
40
|
+
* @param {CommandContext} ctx
|
|
41
|
+
* @returns {Promise<number>} Exit code (0 = success, 1 = failure).
|
|
42
|
+
*/
|
|
43
|
+
export default async function install(ctx) {
|
|
44
|
+
printBanner("claude-crap :: install");
|
|
45
|
+
|
|
46
|
+
const checks = [];
|
|
47
|
+
checks.push(await checkNodeVersion());
|
|
48
|
+
checks.push(await checkDistBuilt(ctx.pluginRoot));
|
|
49
|
+
checks.push(await chmodHooks(ctx.pluginRoot));
|
|
50
|
+
checks.push(await ensureReportsDir(process.cwd()));
|
|
51
|
+
|
|
52
|
+
for (const step of checks) printStep(step);
|
|
53
|
+
|
|
54
|
+
const hasFailure = checks.some((c) => c.status === "fail");
|
|
55
|
+
if (hasFailure) {
|
|
56
|
+
process.stdout.write(
|
|
57
|
+
`\n${paint.red(icons.fail)} Installation prerequisites failed. ` +
|
|
58
|
+
`Run ${paint.bold("claude-crap doctor")} for details.\n`,
|
|
59
|
+
);
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Success — tell the user exactly what to do next. We print the
|
|
64
|
+
// Claude Code native command and also mention the marketplace path
|
|
65
|
+
// for users who cloned the repo from GitHub.
|
|
66
|
+
const pluginDir = join(ctx.pluginRoot, "plugin");
|
|
67
|
+
process.stdout.write(
|
|
68
|
+
[
|
|
69
|
+
"",
|
|
70
|
+
`${paint.green(icons.ok)} claude-crap is ready to register with Claude Code.`,
|
|
71
|
+
"",
|
|
72
|
+
` Plugin root: ${paint.cyan(pluginDir)}`,
|
|
73
|
+
"",
|
|
74
|
+
paint.bold(" Next steps — pick ONE of the following:"),
|
|
75
|
+
"",
|
|
76
|
+
" 1. Native Claude Code install from this directory:",
|
|
77
|
+
` ${paint.cyan(`/plugin install ${pluginDir}`)}`,
|
|
78
|
+
"",
|
|
79
|
+
" 2. Marketplace install (if the plugin is published to GitHub):",
|
|
80
|
+
` ${paint.cyan("/plugin marketplace add ahernandez-developer/claude-crap")}`,
|
|
81
|
+
` ${paint.cyan("/plugin install claude-crap")}`,
|
|
82
|
+
"",
|
|
83
|
+
paint.dim(" Then open a Claude Code session in this workspace. The"),
|
|
84
|
+
paint.dim(" PreToolUse gatekeeper, PostToolUse verifier, Stop quality"),
|
|
85
|
+
paint.dim(" gate, and the local Vue dashboard will all start on their"),
|
|
86
|
+
paint.dim(" own. Run `claude-crap doctor` any time to re-verify."),
|
|
87
|
+
"",
|
|
88
|
+
].join("\n"),
|
|
89
|
+
);
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Verify the Node.js major version is at least 20 (matches `engines`
|
|
95
|
+
* in package.json).
|
|
96
|
+
*
|
|
97
|
+
* @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
|
|
98
|
+
*/
|
|
99
|
+
async function checkNodeVersion() {
|
|
100
|
+
const raw = process.versions.node;
|
|
101
|
+
const major = Number(raw.split(".")[0]);
|
|
102
|
+
if (!Number.isFinite(major) || major < 20) {
|
|
103
|
+
return {
|
|
104
|
+
status: "fail",
|
|
105
|
+
label: `Node.js runtime`,
|
|
106
|
+
detail: `Found ${raw}, claude-crap requires ≥ 20.0.0. Install a newer Node.js and retry.`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return { status: "ok", label: `Node.js runtime (${raw})` };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check that the plugin bundle exists (`plugin/bundle/mcp-server.mjs`).
|
|
114
|
+
* The postinstall hook should have built it automatically; this is a
|
|
115
|
+
* safety net for users who pulled the repo without running `npm install`.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} pluginRoot
|
|
118
|
+
* @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
|
|
119
|
+
*/
|
|
120
|
+
async function checkDistBuilt(pluginRoot) {
|
|
121
|
+
const entry = join(pluginRoot, "plugin", "bundle", "mcp-server.mjs");
|
|
122
|
+
try {
|
|
123
|
+
await fs.access(entry);
|
|
124
|
+
return { status: "ok", label: `MCP server bundle is built`, detail: entry };
|
|
125
|
+
} catch {
|
|
126
|
+
return {
|
|
127
|
+
status: "fail",
|
|
128
|
+
label: `MCP server bundle is NOT built`,
|
|
129
|
+
detail: `Expected ${entry}. Run \`npm run build:plugin\` from the plugin root.`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Ensure every hook script is marked executable. npm usually handles
|
|
136
|
+
* this via the `files` field in package.json, but tarballs extracted
|
|
137
|
+
* manually or pulled over non-UNIX filesystems sometimes lose the bit.
|
|
138
|
+
*
|
|
139
|
+
* @param {string} pluginRoot
|
|
140
|
+
* @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
|
|
141
|
+
*/
|
|
142
|
+
async function chmodHooks(pluginRoot) {
|
|
143
|
+
const hookDir = join(pluginRoot, "plugin", "hooks");
|
|
144
|
+
const entries = ["pre-tool-use.mjs", "post-tool-use.mjs", "stop-quality-gate.mjs", "session-start.mjs"];
|
|
145
|
+
const fixed = [];
|
|
146
|
+
for (const name of entries) {
|
|
147
|
+
const full = join(hookDir, name);
|
|
148
|
+
try {
|
|
149
|
+
await fs.access(full, fsConstants.F_OK);
|
|
150
|
+
await fs.chmod(full, 0o755);
|
|
151
|
+
fixed.push(name);
|
|
152
|
+
} catch {
|
|
153
|
+
return {
|
|
154
|
+
status: "fail",
|
|
155
|
+
label: `Hook scripts are executable`,
|
|
156
|
+
detail: `Missing ${full}`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
status: "ok",
|
|
162
|
+
label: `Hook scripts are executable`,
|
|
163
|
+
detail: fixed.join(", "),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Ensure the SARIF reports directory exists inside the current
|
|
169
|
+
* workspace so the first ingestion doesn't race with a missing dir.
|
|
170
|
+
* This is a separate directory from the plugin root — it lives in the
|
|
171
|
+
* user's project, not the plugin's install location.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} workspace
|
|
174
|
+
* @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
|
|
175
|
+
*/
|
|
176
|
+
async function ensureReportsDir(workspace) {
|
|
177
|
+
const dir = resolve(workspace, ".claude-crap", "reports");
|
|
178
|
+
try {
|
|
179
|
+
await fs.mkdir(dir, { recursive: true });
|
|
180
|
+
return {
|
|
181
|
+
status: "ok",
|
|
182
|
+
label: `SARIF reports directory ready`,
|
|
183
|
+
detail: dir,
|
|
184
|
+
};
|
|
185
|
+
} catch (err) {
|
|
186
|
+
return {
|
|
187
|
+
status: "warn",
|
|
188
|
+
label: `Could not create reports directory`,
|
|
189
|
+
detail: `${dir} :: ${/** @type {Error} */ (err).message}`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Tiny CLI UI helpers shared by every `claude-crap <cmd>` subcommand.
|
|
4
|
+
*
|
|
5
|
+
* Provides ANSI color wrappers (with a `NO_COLOR` fallback), a unified
|
|
6
|
+
* `printStep` formatter for doctor-style checklists, and a handful of
|
|
7
|
+
* icon constants so the output looks consistent across subcommands.
|
|
8
|
+
*
|
|
9
|
+
* Zero runtime dependencies — uses only Node built-ins so it keeps the
|
|
10
|
+
* CLI startup fast and works in any environment the plugin supports.
|
|
11
|
+
*
|
|
12
|
+
* @module scripts/lib/cli-ui
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check whether the current terminal supports ANSI color escapes. We
|
|
17
|
+
* follow the `NO_COLOR` env var (https://no-color.org/) and also
|
|
18
|
+
* respect the conventional `FORCE_COLOR` override for CI environments.
|
|
19
|
+
*/
|
|
20
|
+
const useColor = (() => {
|
|
21
|
+
if (process.env.NO_COLOR) return false;
|
|
22
|
+
if (process.env.FORCE_COLOR) return true;
|
|
23
|
+
return Boolean(process.stdout.isTTY);
|
|
24
|
+
})();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Wrap a string in an ANSI color escape when color is enabled. Returns
|
|
28
|
+
* the plain string otherwise.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} code Numeric ANSI color code (e.g. `"32"` for green).
|
|
31
|
+
* @param {string} text Text to wrap.
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
function color(code, text) {
|
|
35
|
+
return useColor ? `\x1b[${code}m${text}\x1b[0m` : text;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Color helpers. Each function takes a string and returns a potentially
|
|
40
|
+
* colored version suitable for direct `process.stdout.write()` output.
|
|
41
|
+
*/
|
|
42
|
+
export const paint = Object.freeze({
|
|
43
|
+
dim: (s) => color("2", s),
|
|
44
|
+
bold: (s) => color("1", s),
|
|
45
|
+
green: (s) => color("32", s),
|
|
46
|
+
yellow: (s) => color("33", s),
|
|
47
|
+
red: (s) => color("31", s),
|
|
48
|
+
cyan: (s) => color("36", s),
|
|
49
|
+
magenta: (s) => color("35", s),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Icons used by `printStep`. Kept ASCII-safe so the output renders
|
|
54
|
+
* correctly inside Claude Code's plain-text hook transcript.
|
|
55
|
+
*/
|
|
56
|
+
export const icons = Object.freeze({
|
|
57
|
+
ok: "✓",
|
|
58
|
+
warn: "!",
|
|
59
|
+
fail: "✗",
|
|
60
|
+
info: "•",
|
|
61
|
+
step: "▸",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Print a heading banner to stdout. Used once per subcommand to make
|
|
66
|
+
* the CLI output easy to scan.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} title Short heading text (kept under ~40 chars).
|
|
69
|
+
*/
|
|
70
|
+
export function printBanner(title) {
|
|
71
|
+
const line = "─".repeat(Math.max(1, Math.min(title.length + 4, 76)));
|
|
72
|
+
process.stdout.write(`\n${paint.cyan(line)}\n`);
|
|
73
|
+
process.stdout.write(` ${paint.bold(title)}\n`);
|
|
74
|
+
process.stdout.write(`${paint.cyan(line)}\n\n`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Structured result from a diagnostic check, suitable for both
|
|
79
|
+
* rendering with `printStep` and aggregating into a summary exit code.
|
|
80
|
+
*
|
|
81
|
+
* @typedef {"ok" | "warn" | "fail" | "info"} StepStatus
|
|
82
|
+
*
|
|
83
|
+
* @typedef {Object} StepResult
|
|
84
|
+
* @property {StepStatus} status
|
|
85
|
+
* @property {string} label
|
|
86
|
+
* @property {string} [detail]
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Print a single checklist step.
|
|
91
|
+
*
|
|
92
|
+
* @param {StepResult} step
|
|
93
|
+
*/
|
|
94
|
+
export function printStep(step) {
|
|
95
|
+
const icon =
|
|
96
|
+
step.status === "ok"
|
|
97
|
+
? paint.green(icons.ok)
|
|
98
|
+
: step.status === "warn"
|
|
99
|
+
? paint.yellow(icons.warn)
|
|
100
|
+
: step.status === "fail"
|
|
101
|
+
? paint.red(icons.fail)
|
|
102
|
+
: paint.dim(icons.info);
|
|
103
|
+
process.stdout.write(` ${icon} ${step.label}\n`);
|
|
104
|
+
if (step.detail) {
|
|
105
|
+
for (const line of step.detail.split("\n")) {
|
|
106
|
+
process.stdout.write(` ${paint.dim(line)}\n`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Print a key/value pair right-aligned at a fixed label width. Used
|
|
113
|
+
* by `status` to render the "resolved paths" block.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} label
|
|
116
|
+
* @param {string} value
|
|
117
|
+
* @param {number} [width]
|
|
118
|
+
*/
|
|
119
|
+
export function printKv(label, value, width = 20) {
|
|
120
|
+
const padded = label.padEnd(width, " ");
|
|
121
|
+
process.stdout.write(` ${paint.dim(padded)} ${value}\n`);
|
|
122
|
+
}
|