guardvibe 2.5.0 → 2.7.3

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/build/cli.js CHANGED
@@ -1,580 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "module";
3
- import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, unlinkSync, statSync } from "fs";
4
- import { join, dirname } from "path";
5
- import { homedir } from "os";
6
3
  const require = createRequire(import.meta.url);
7
4
  const pkg = require("../package.json");
8
- const GUARDVIBE_MCP_CONFIG = {
9
- command: "npx",
10
- args: ["-y", "guardvibe"],
11
- };
12
- const platforms = {
13
- claude: {
14
- path: join(process.cwd(), ".claude.json"),
15
- description: "Claude Code (.claude.json)",
16
- },
17
- gemini: {
18
- path: join(homedir(), ".gemini", "settings.json"),
19
- description: "Gemini CLI (~/.gemini/settings.json)",
20
- },
21
- cursor: {
22
- path: join(process.cwd(), ".cursor", "mcp.json"),
23
- description: "Cursor (.cursor/mcp.json)",
24
- },
25
- };
26
- function readJsonFile(filePath) {
27
- try {
28
- const content = readFileSync(filePath, "utf-8");
29
- return JSON.parse(content);
30
- }
31
- catch {
32
- return null;
33
- }
34
- }
35
- function writeJsonFile(filePath, data) {
36
- const dir = dirname(filePath);
37
- if (!existsSync(dir)) {
38
- mkdirSync(dir, { recursive: true });
39
- }
40
- writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
41
- }
42
- function setupPlatform(name) {
43
- const platform = platforms[name];
44
- if (!platform)
45
- return false;
46
- const existing = readJsonFile(platform.path);
47
- if (existing) {
48
- if (!existing.mcpServers) {
49
- existing.mcpServers = {};
50
- }
51
- if (existing.mcpServers["guardvibe"]) {
52
- console.log(` [OK] GuardVibe already configured in ${platform.description}`);
53
- // Still ensure security guidance and .gitignore are set up
54
- setupSecurityGuide(name);
55
- return true;
56
- }
57
- existing.mcpServers["guardvibe"] = GUARDVIBE_MCP_CONFIG;
58
- writeJsonFile(platform.path, existing);
59
- }
60
- else {
61
- writeJsonFile(platform.path, {
62
- mcpServers: {
63
- guardvibe: GUARDVIBE_MCP_CONFIG,
64
- },
65
- });
66
- }
67
- console.log(` [OK] Added MCP server to ${platform.description}`);
68
- // Set up platform-specific security guidance + universal security rules
69
- setupSecurityGuide(name);
70
- return true;
71
- }
72
- // ── Universal security rules (shared across all AI platforms) ────────
73
- const SECURITY_RULES = `## Security Rules (enforced by GuardVibe MCP)
74
-
75
- ### Mandatory checks
76
- - AFTER writing any API route, Server Action, or database query: use the guardvibe scan_file MCP tool on the file
77
- - BEFORE committing: use the guardvibe scan_staged MCP tool
78
- - When GuardVibe reports HIGH or CRITICAL: fix immediately before moving on
79
- - BEFORE creating a PR: use scan_changed_files to review all changes
80
-
81
- ### Scan visibility — ALWAYS report GuardVibe results to the user
82
- - AFTER every scan: ALWAYS tell the user the GuardVibe result, even when clean
83
- - When issues found: "GuardVibe: [count] issues found ([severity]). Fixing..."
84
- - When clean: "GuardVibe: scanned [filename] — no issues found."
85
- - Include the summary stats line from scan output when present
86
- - NEVER silently consume scan results — the user must see that GuardVibe is protecting their code
87
- - Periodically (every ~10 scans or at session start): run security_stats tool and share the dashboard with the user
88
-
89
- ### Writing secure code
90
- - Every API route handler MUST have auth check before DB access
91
- - Every POST endpoint MUST have input validation (zod/joi schema)
92
- - Never pass user input to innerHTML, eval, exec, or raw SQL
93
- - Never pass user-controlled URLs to fetch() without allowlist validation
94
- - Always use select: in Prisma/Drizzle queries from Server Actions (never return full objects to client)
95
- - Always validate redirect URLs against trusted domain allowlist
96
- - Set security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)
97
- - Verify webhook signatures before processing events
98
- - Use parameterized queries, never string concatenation/template literals for SQL
99
-
100
- ### When in doubt
101
- - Use the guardvibe explain_remediation MCP tool with the rule ID for detailed fix guidance
102
- - Use the guardvibe check_code MCP tool to verify a code snippet is secure before applying
103
- `;
104
- function setupSecurityGuide(platformName) {
105
- // 1. Platform-specific guidance file
106
- if (platformName === "claude")
107
- setupClaudeGuide();
108
- else if (platformName === "cursor")
109
- setupCursorGuide();
110
- else if (platformName === "gemini")
111
- setupGeminiGuide();
112
- // 2. Platform-specific .gitignore entries
113
- const gitignoreEntries = {
114
- claude: [".claude.json", ".claude/", "CLAUDE.md"],
115
- cursor: [".cursor/", ".cursorrules"],
116
- gemini: ["GEMINI.md"],
117
- };
118
- const entries = gitignoreEntries[platformName] || [];
119
- // Always add .guardvibe/ (stats directory) to .gitignore
120
- entries.push(".guardvibe/");
121
- if (entries.length > 0)
122
- addToGitignore(entries);
123
- }
124
- function setupClaudeGuide() {
125
- // Claude Code hooks
126
- const claudeSettingsDir = join(process.cwd(), ".claude");
127
- if (!existsSync(claudeSettingsDir))
128
- mkdirSync(claudeSettingsDir, { recursive: true });
129
- const claudeSettingsPath = join(claudeSettingsDir, "settings.json");
130
- const existingSettings = readJsonFile(claudeSettingsPath) || {};
131
- if (!existingSettings.hooks)
132
- existingSettings.hooks = {};
133
- if (!existingSettings.hooks.PostToolUse) {
134
- existingSettings.hooks.PostToolUse = [
135
- {
136
- matcher: "Edit|Write",
137
- hooks: [{
138
- type: "command",
139
- command: "jq -r '.tool_input.file_path' | xargs npx -y guardvibe check --format buddy 2>/dev/null || true"
140
- }]
141
- }
142
- ];
143
- }
144
- writeJsonFile(claudeSettingsPath, existingSettings);
145
- console.log(` [OK] Claude Code hooks configured (.claude/settings.json)`);
146
- // CLAUDE.md
147
- const claudeMdPath = join(process.cwd(), "CLAUDE.md");
148
- if (existsSync(claudeMdPath)) {
149
- const content = readFileSync(claudeMdPath, "utf-8");
150
- if (!content.includes("GuardVibe")) {
151
- writeFileSync(claudeMdPath, content + "\n" + SECURITY_RULES, "utf-8");
152
- console.log(` [OK] GuardVibe rules added to CLAUDE.md`);
153
- }
154
- }
155
- else {
156
- writeFileSync(claudeMdPath, `# Project Guidelines\n\n${SECURITY_RULES}`, "utf-8");
157
- console.log(` [OK] Created CLAUDE.md with security rules`);
158
- }
159
- }
160
- function setupCursorGuide() {
161
- // .cursorrules — Cursor reads this file for AI instructions
162
- const cursorrules = join(process.cwd(), ".cursorrules");
163
- if (existsSync(cursorrules)) {
164
- const content = readFileSync(cursorrules, "utf-8");
165
- if (!content.includes("GuardVibe")) {
166
- writeFileSync(cursorrules, content + "\n" + SECURITY_RULES, "utf-8");
167
- console.log(` [OK] GuardVibe rules added to .cursorrules`);
168
- }
169
- }
170
- else {
171
- writeFileSync(cursorrules, SECURITY_RULES, "utf-8");
172
- console.log(` [OK] Created .cursorrules with security rules`);
173
- }
174
- }
175
- function setupGeminiGuide() {
176
- // GEMINI.md — Gemini CLI reads this for project context
177
- const geminiMd = join(process.cwd(), "GEMINI.md");
178
- if (existsSync(geminiMd)) {
179
- const content = readFileSync(geminiMd, "utf-8");
180
- if (!content.includes("GuardVibe")) {
181
- writeFileSync(geminiMd, content + "\n" + SECURITY_RULES, "utf-8");
182
- console.log(` [OK] GuardVibe rules added to GEMINI.md`);
183
- }
184
- }
185
- else {
186
- writeFileSync(geminiMd, `# Project Guidelines\n\n${SECURITY_RULES}`, "utf-8");
187
- console.log(` [OK] Created GEMINI.md with security rules`);
188
- }
189
- }
190
- function addToGitignore(entries) {
191
- const gitignorePath = join(process.cwd(), ".gitignore");
192
- let content = "";
193
- try {
194
- content = readFileSync(gitignorePath, "utf-8");
195
- }
196
- catch { /* no .gitignore yet */ }
197
- const missing = entries.filter(e => !content.split("\n").some(line => line.trim() === e));
198
- if (missing.length === 0)
199
- return;
200
- const block = `\n# GuardVibe (auto-added by guardvibe init)\n${missing.join("\n")}\n`;
201
- writeFileSync(gitignorePath, content.trimEnd() + block, "utf-8");
202
- console.log(` [OK] Added ${missing.join(", ")} to .gitignore`);
203
- }
204
- // ── Pre-commit hook ──────────────────────────────────────────────────
205
- const HOOK_SCRIPT = `#!/bin/sh
206
- # GuardVibe pre-commit security hook
207
- # Installed by: npx guardvibe hook install
208
-
209
- echo "🔒 GuardVibe: scanning staged files..."
210
-
211
- # Run guardvibe scan on staged files
212
- RESULT=$(npx -y guardvibe-scan 2>&1)
213
- EXIT_CODE=$?
214
-
215
- if [ $EXIT_CODE -ne 0 ]; then
216
- echo ""
217
- echo "$RESULT"
218
- echo ""
219
- echo "❌ GuardVibe: security issues found. Fix them or commit with --no-verify to skip."
220
- exit 1
221
- fi
222
-
223
- echo "✅ GuardVibe: all checks passed."
224
- `;
225
- function installHook() {
226
- const gitDir = join(process.cwd(), ".git");
227
- if (!existsSync(gitDir)) {
228
- console.error(" [ERR] Not a git repository. Run this from your project root.");
229
- process.exit(1);
230
- }
231
- const hooksDir = join(gitDir, "hooks");
232
- if (!existsSync(hooksDir)) {
233
- mkdirSync(hooksDir, { recursive: true });
234
- }
235
- const hookPath = join(hooksDir, "pre-commit");
236
- if (existsSync(hookPath)) {
237
- const existing = readFileSync(hookPath, "utf-8");
238
- if (existing.includes("GuardVibe")) {
239
- console.log(" [OK] GuardVibe pre-commit hook already installed.");
240
- return;
241
- }
242
- // Append to existing hook
243
- writeFileSync(hookPath, existing + "\n" + HOOK_SCRIPT, "utf-8");
244
- console.log(" [OK] GuardVibe added to existing pre-commit hook.");
245
- }
246
- else {
247
- writeFileSync(hookPath, HOOK_SCRIPT, "utf-8");
248
- chmodSync(hookPath, 0o755);
249
- console.log(" [OK] Pre-commit hook installed at .git/hooks/pre-commit");
250
- }
251
- }
252
- function uninstallHook() {
253
- const hookPath = join(process.cwd(), ".git", "hooks", "pre-commit");
254
- if (!existsSync(hookPath)) {
255
- console.log(" [OK] No pre-commit hook found.");
256
- return;
257
- }
258
- const content = readFileSync(hookPath, "utf-8");
259
- if (!content.includes("GuardVibe")) {
260
- console.log(" [OK] Pre-commit hook exists but doesn't contain GuardVibe.");
261
- return;
262
- }
263
- // Remove GuardVibe section
264
- const cleaned = content
265
- .replace(/\n?# GuardVibe pre-commit security hook[\s\S]*?GuardVibe: all checks passed[."]*\n?/g, "")
266
- .trim();
267
- if (!cleaned || cleaned === "#!/bin/sh") {
268
- unlinkSync(hookPath);
269
- console.log(" [OK] Pre-commit hook removed.");
270
- }
271
- else {
272
- writeFileSync(hookPath, cleaned + "\n", "utf-8");
273
- console.log(" [OK] GuardVibe removed from pre-commit hook (other hooks preserved).");
274
- }
275
- }
276
- // ── GitHub Actions workflow ──────────────────────────────────────────
277
- const GITHUB_ACTIONS_WORKFLOW = `name: GuardVibe Security Scan
278
-
279
- on:
280
- pull_request:
281
- branches: [main, master]
282
- push:
283
- branches: [main, master]
284
-
285
- permissions:
286
- contents: read
287
- security-events: write
288
-
289
- jobs:
290
- security-scan:
291
- name: Security Scan
292
- runs-on: ubuntu-latest
293
- steps:
294
- - uses: actions/checkout@v4
295
- with:
296
- persist-credentials: false
297
-
298
- - uses: actions/setup-node@v4
299
- with:
300
- node-version: "22"
301
-
302
- - name: Run GuardVibe security scan
303
- run: npx -y guardvibe-scan --format sarif --output guardvibe-results.sarif
304
-
305
- - name: Upload SARIF to GitHub Security
306
- if: always()
307
- uses: github/codeql-action/upload-sarif@v3
308
- with:
309
- sarif_file: guardvibe-results.sarif
310
- category: guardvibe
311
- `;
312
- function generateGitHubActions() {
313
- const workflowDir = join(process.cwd(), ".github", "workflows");
314
- if (!existsSync(workflowDir)) {
315
- mkdirSync(workflowDir, { recursive: true });
316
- }
317
- const workflowPath = join(workflowDir, "guardvibe.yml");
318
- if (existsSync(workflowPath)) {
319
- console.log(" [OK] .github/workflows/guardvibe.yml already exists.");
320
- return;
321
- }
322
- writeFileSync(workflowPath, GITHUB_ACTIONS_WORKFLOW, "utf-8");
323
- console.log(" [OK] Created .github/workflows/guardvibe.yml");
324
- console.log(" [OK] SARIF results will appear in GitHub Security tab.");
325
- }
326
- // ── Scan CLI (for pre-commit hook and CI) ────────────────────────────
5
+ // ── Scan entry point detection ──────────────────────────────────────
327
6
  const SCAN_SCRIPT_DETECTED = process.argv[1]?.endsWith("guardvibe-scan") ||
328
7
  process.argv[1]?.endsWith("guardvibe-scan.js");
329
8
  if (SCAN_SCRIPT_DETECTED) {
330
- runScan();
9
+ const { runScan } = await import("./cli/scan.js");
10
+ await runScan();
331
11
  }
332
12
  else {
333
- main();
334
- }
335
- /**
336
- * Check if scan results should cause a non-zero exit based on --fail-on flag.
337
- * Default: "critical" — only exit 1 on critical findings.
338
- * Options: "critical", "high", "medium", "low", "none"
339
- */
340
- function shouldFail(result, failOn) {
341
- if (failOn === "none")
342
- return false;
343
- const levels = {
344
- low: ["critical", "high", "medium", "low"],
345
- medium: ["critical", "high", "medium"],
346
- high: ["critical", "high"],
347
- critical: ["critical"],
348
- };
349
- const failLevels = levels[failOn] || levels.critical;
350
- // Try JSON format first
351
- try {
352
- const parsed = JSON.parse(result);
353
- if (parsed.summary) {
354
- return failLevels.some(level => (parsed.summary[level] ?? 0) > 0);
355
- }
356
- if (parsed.findings) {
357
- return parsed.findings.some((f) => failLevels.includes(f.severity));
358
- }
359
- }
360
- catch { /* not JSON, try markdown tags */ }
361
- // Markdown format: check for [SEVERITY] tags
362
- const tags = failLevels.map(l => `[${l.toUpperCase()}]`);
363
- return tags.some(tag => result.includes(tag));
13
+ await main();
364
14
  }
365
- function parseArgs(args) {
366
- const flags = {};
367
- const positional = [];
368
- for (let i = 0; i < args.length; i++) {
369
- if (args[i].startsWith("--")) {
370
- const key = args[i].slice(2);
371
- const next = args[i + 1];
372
- if (next && !next.startsWith("--")) {
373
- flags[key] = next;
374
- i++;
375
- }
376
- else {
377
- flags[key] = true;
378
- }
379
- }
380
- else {
381
- positional.push(args[i]);
382
- }
383
- }
384
- return { flags, positional };
385
- }
386
- async function runScan() {
387
- const args = process.argv.slice(2);
388
- const { flags } = parseArgs(args);
389
- const format = flags.format ?? "markdown";
390
- const outputFile = flags.output ?? null;
391
- let result;
392
- if (format === "sarif") {
393
- const { exportSarif } = await import("./tools/export-sarif.js");
394
- result = exportSarif(process.cwd());
395
- }
396
- else {
397
- const { scanStaged } = await import("./tools/scan-staged.js");
398
- result = scanStaged(process.cwd(), format === "json" ? "json" : "markdown");
399
- }
400
- if (outputFile) {
401
- writeFileSync(outputFile, result, "utf-8");
402
- console.log(` [OK] Results written to ${outputFile}`);
403
- }
404
- else {
405
- console.log(result);
406
- }
407
- if (format !== "sarif") {
408
- const failOn = flags["fail-on"] ?? "high";
409
- if (shouldFail(result, failOn))
410
- process.exit(1);
411
- }
412
- }
413
- async function runDirectoryScan(targetPath, flags) {
414
- const { scanDirectory } = await import("./tools/scan-directory.js");
415
- const { resolve } = await import("path");
416
- const format = flags.format ?? "markdown";
417
- const outputFile = flags.output ?? null;
418
- const baselinePath = flags.baseline ?? null;
419
- const saveBaseline = flags["save-baseline"] === true || typeof flags["save-baseline"] === "string";
420
- const scanPath = resolve(targetPath);
421
- let result;
422
- if (format === "sarif") {
423
- const { exportSarif } = await import("./tools/export-sarif.js");
424
- result = exportSarif(scanPath);
425
- }
426
- else {
427
- result = scanDirectory(scanPath, true, [], format === "json" ? "json" : "markdown", undefined, baselinePath ?? undefined);
428
- }
429
- if (outputFile) {
430
- writeFileSync(outputFile, result, "utf-8");
431
- console.log(` [OK] Results written to ${outputFile}`);
432
- }
433
- else {
434
- console.log(result);
435
- }
436
- // Auto-save baseline for future diff comparisons
437
- if (saveBaseline && format === "json") {
438
- const baselineFile = typeof flags["save-baseline"] === "string"
439
- ? flags["save-baseline"]
440
- : join(scanPath, ".guardvibe-baseline.json");
441
- writeFileSync(baselineFile, result, "utf-8");
442
- console.log(` [OK] Baseline saved to ${baselineFile}`);
443
- }
444
- if (format !== "sarif") {
445
- const failOn = flags["fail-on"] ?? "critical";
446
- if (shouldFail(result, failOn))
447
- process.exit(1);
448
- }
449
- }
450
- async function runDiffScan(base, flags) {
451
- const { execFileSync } = await import("child_process");
452
- const { resolve, extname, basename } = await import("path");
453
- const { analyzeCode } = await import("./tools/check-code.js");
454
- const { EXTENSION_MAP, CONFIG_FILE_MAP } = await import("./utils/constants.js");
455
- const format = flags.format ?? "markdown";
456
- const outputFile = flags.output ?? null;
457
- const root = resolve(".");
458
- let changedFiles;
459
- try {
460
- const output = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACMR", base], { cwd: root, encoding: "utf-8" });
461
- changedFiles = output.trim().split("\n").filter(Boolean);
462
- }
463
- catch {
464
- console.error(" [ERR] Failed to get git diff. Ensure you're in a git repository.");
465
- process.exit(1);
466
- }
467
- if (changedFiles.length === 0) {
468
- console.log(" No changed files to scan.");
469
- return;
470
- }
471
- const allFindings = [];
472
- for (const relPath of changedFiles) {
473
- const fullPath = resolve(root, relPath);
474
- if (!existsSync(fullPath))
475
- continue;
476
- const ext = extname(relPath).toLowerCase();
477
- let language = EXTENSION_MAP[ext];
478
- if (!language && basename(relPath).startsWith("Dockerfile"))
479
- language = "dockerfile";
480
- if (!language)
481
- language = CONFIG_FILE_MAP[basename(relPath)];
482
- if (!language)
483
- continue;
484
- try {
485
- const content = readFileSync(fullPath, "utf-8");
486
- const findings = analyzeCode(content, language, undefined, fullPath, root);
487
- for (const f of findings) {
488
- allFindings.push({ file: relPath, severity: f.rule.severity, name: f.rule.name, id: f.rule.id, line: f.line, fix: f.rule.fix });
489
- }
490
- }
491
- catch { /* skip */ }
492
- }
493
- let result;
494
- if (format === "json") {
495
- const critical = allFindings.filter(f => f.severity === "critical").length;
496
- const high = allFindings.filter(f => f.severity === "high").length;
497
- const medium = allFindings.filter(f => f.severity === "medium").length;
498
- result = JSON.stringify({
499
- summary: { total: allFindings.length, critical, high, medium, changedFiles: changedFiles.length, blocked: critical > 0 || high > 0 },
500
- findings: allFindings,
501
- });
502
- }
503
- else {
504
- const lines = [`# GuardVibe Diff Report`, ``, `Base: ${base}`, `Changed files: ${changedFiles.length}`, `Issues: ${allFindings.length}`, ``];
505
- if (allFindings.length === 0) {
506
- lines.push(`All changed files passed security checks.`);
507
- }
508
- else {
509
- const sev = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
510
- allFindings.sort((a, b) => (sev[a.severity] ?? 99) - (sev[b.severity] ?? 99));
511
- for (const f of allFindings) {
512
- lines.push(`- [${f.severity.toUpperCase()}] **${f.name}** (${f.id}) in ${f.file}:${f.line}`);
513
- lines.push(` Fix: ${f.fix}`);
514
- }
515
- }
516
- result = lines.join("\n");
517
- }
518
- if (outputFile) {
519
- writeFileSync(outputFile, result, "utf-8");
520
- console.log(` [OK] Results written to ${outputFile}`);
521
- }
522
- else {
523
- console.log(result);
524
- }
525
- const failOn = flags["fail-on"] ?? "critical";
526
- if (failOn !== "none") {
527
- const failLevels = {
528
- low: ["critical", "high", "medium", "low"],
529
- medium: ["critical", "high", "medium"],
530
- high: ["critical", "high"],
531
- critical: ["critical"],
532
- };
533
- const levels = failLevels[failOn] || failLevels.critical;
534
- if (allFindings.some(f => levels.includes(f.severity)))
535
- process.exit(1);
536
- }
537
- }
538
- async function runFileCheck(filePath, flags) {
539
- const { checkCode } = await import("./tools/check-code.js");
540
- const { resolve, extname, basename } = await import("path");
541
- const resolved = resolve(filePath);
542
- if (!existsSync(resolved)) {
543
- console.error(` [ERR] File not found: ${resolved}`);
544
- process.exit(1);
545
- }
546
- const content = readFileSync(resolved, "utf-8");
547
- const ext = extname(resolved).toLowerCase();
548
- const extMap = {
549
- ".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
550
- ".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
551
- ".py": "python", ".go": "go", ".html": "html", ".sql": "sql",
552
- ".sh": "shell", ".bash": "shell", ".yml": "yaml", ".yaml": "yaml",
553
- ".tf": "terraform", ".toml": "toml", ".json": "json",
554
- };
555
- let language = extMap[ext];
556
- if (!language && basename(resolved).startsWith("Dockerfile"))
557
- language = "dockerfile";
558
- if (!language) {
559
- console.error(` [ERR] Unsupported file type: ${ext}`);
560
- process.exit(1);
561
- }
562
- const format = flags.format ?? "markdown";
563
- const formatArg = format === "json" ? "json" : format === "buddy" ? "buddy" : "markdown";
564
- const result = checkCode(content, language, undefined, resolved, undefined, formatArg);
565
- const outputFile = flags.output ?? null;
566
- if (outputFile) {
567
- writeFileSync(outputFile, result, "utf-8");
568
- console.log(` [OK] Results written to ${outputFile}`);
569
- }
570
- else {
571
- console.log(result);
572
- }
573
- const failOn = flags["fail-on"] ?? "critical";
574
- if (shouldFail(result, failOn))
575
- process.exit(1);
576
- }
577
- // ── Main CLI ─────────────────────────────────────────────────────────
15
+ // ── Help ────────────────────────────────────────────────────────────
578
16
  function printUsage() {
579
17
  console.log(`
580
18
  GuardVibe Security - CLI
@@ -583,6 +21,7 @@ function printUsage() {
583
21
  npx guardvibe scan [path] Scan a directory for security issues
584
22
  npx guardvibe diff [base] Scan only changed files since a git ref
585
23
  npx guardvibe check <file> Scan a single file for security issues
24
+ npx guardvibe doctor [path] Run host security audit
586
25
  npx guardvibe init <platform> Setup MCP server configuration
587
26
  npx guardvibe hook install Install pre-commit security hook
588
27
  npx guardvibe hook uninstall Remove pre-commit security hook
@@ -602,6 +41,12 @@ function printUsage() {
602
41
  --version, -V Print version and exit
603
42
  --help, -h Show this help message
604
43
 
44
+ Doctor Options:
45
+ --scope <scope> project (default) | host | full
46
+ project: only project config files
47
+ host: + shell profiles, global MCP configs
48
+ full: + home dir configs, npm global
49
+
605
50
  MCP Platforms:
606
51
  claude Claude Code (.claude.json + CLAUDE.md + hooks)
607
52
  cursor Cursor (.cursor/mcp.json + .cursorrules)
@@ -615,16 +60,16 @@ function printUsage() {
615
60
  Examples:
616
61
  npx guardvibe scan .
617
62
  npx guardvibe scan ./src --format json
618
- npx guardvibe scan . --format sarif --output results.sarif
619
- npx guardvibe scan . --format json --save-baseline
620
- npx guardvibe scan . --baseline .guardvibe-baseline.json
63
+ npx guardvibe doctor
64
+ npx guardvibe doctor --scope host
65
+ npx guardvibe doctor --scope full --format json
621
66
  npx guardvibe check src/app/api/route.ts
622
- npx guardvibe check package.json
623
67
  npx guardvibe init claude
624
68
  npx guardvibe hook install
625
69
  npx guardvibe ci github
626
70
  `);
627
71
  }
72
+ // ── Main dispatcher ─────────────────────────────────────────────────
628
73
  async function main() {
629
74
  const args = process.argv.slice(2);
630
75
  if (args.includes("--version") || args.includes("-V")) {
@@ -642,91 +87,40 @@ async function main() {
642
87
  process.exit(0);
643
88
  }
644
89
  else {
645
- // Started by MCP client (Claude Code, Cursor, etc.) — launch MCP server
646
90
  const { startMcpServer } = await import("./index.js");
647
91
  await startMcpServer();
648
92
  return;
649
93
  }
650
94
  }
651
95
  const command = args[0];
96
+ const subArgs = args.slice(1);
652
97
  if (command === "init") {
653
- const platform = args[1]?.toLowerCase();
654
- if (!platform) {
655
- console.error(" Please specify a platform: claude, gemini, cursor, or all");
656
- process.exit(1);
657
- }
658
- console.log(`\n GuardVibe Security Setup\n`);
659
- if (platform === "all") {
660
- for (const name of Object.keys(platforms)) {
661
- setupPlatform(name);
662
- }
663
- }
664
- else if (platforms[platform]) {
665
- setupPlatform(platform);
666
- }
667
- else {
668
- console.error(` Unknown platform: ${platform}`);
669
- console.error(` Available: claude, gemini, cursor, all`);
670
- process.exit(1);
671
- }
672
- console.log(`\n [OK] Ready! Start coding securely.\n`);
98
+ const { runInit } = await import("./cli/init.js");
99
+ runInit(subArgs);
673
100
  }
674
101
  else if (command === "hook") {
675
- const action = args[1]?.toLowerCase();
676
- console.log(`\n GuardVibe Pre-Commit Hook\n`);
677
- if (action === "install") {
678
- installHook();
679
- }
680
- else if (action === "uninstall") {
681
- uninstallHook();
682
- }
683
- else {
684
- console.error(" Usage: npx guardvibe hook install|uninstall");
685
- process.exit(1);
686
- }
687
- console.log();
102
+ const { runHook } = await import("./cli/hook.js");
103
+ runHook(subArgs);
688
104
  }
689
105
  else if (command === "scan") {
690
- const cliArgs = args.slice(1);
691
- const { flags, positional } = parseArgs(cliArgs);
692
- const targetPath = positional[0] ?? ".";
693
- // If target is a file (not directory), auto-redirect to check mode
694
- if (targetPath !== "." && existsSync(targetPath) && !statSync(targetPath).isDirectory()) {
695
- console.log(` [INFO] "${targetPath}" is a file. Running: guardvibe check ${targetPath}\n`);
696
- await runFileCheck(targetPath, flags);
697
- }
698
- else {
699
- await runDirectoryScan(targetPath, flags);
700
- }
106
+ const { handleScanCommand } = await import("./cli/scan.js");
107
+ await handleScanCommand(subArgs);
701
108
  }
702
109
  else if (command === "diff") {
703
- const cliArgs = args.slice(1);
704
- const { flags, positional } = parseArgs(cliArgs);
705
- const base = positional[0] ?? "main";
706
- await runDiffScan(base, flags);
110
+ const { handleDiffCommand } = await import("./cli/scan.js");
111
+ await handleDiffCommand(subArgs);
707
112
  }
708
113
  else if (command === "check") {
709
- const cliArgs = args.slice(1);
710
- const { flags, positional } = parseArgs(cliArgs);
711
- const filePath = positional[0];
712
- if (!filePath) {
713
- console.error(" Please specify a file: npx guardvibe check <file>");
714
- process.exit(1);
715
- }
716
- await runFileCheck(filePath, flags);
114
+ const { handleCheckCommand } = await import("./cli/scan.js");
115
+ await handleCheckCommand(subArgs);
717
116
  }
718
117
  else if (command === "ci") {
719
- const provider = args[1]?.toLowerCase();
720
- console.log(`\n GuardVibe CI/CD Setup\n`);
721
- if (provider === "github") {
722
- generateGitHubActions();
723
- }
724
- else {
725
- console.error(" Usage: npx guardvibe ci github");
726
- console.error(" (more CI providers coming soon)");
727
- process.exit(1);
728
- }
729
- console.log();
118
+ const { runCi } = await import("./cli/ci.js");
119
+ runCi(subArgs);
120
+ }
121
+ else if (command === "doctor") {
122
+ const { runDoctor } = await import("./cli/doctor.js");
123
+ await runDoctor(subArgs);
730
124
  }
731
125
  else {
732
126
  console.error(` Unknown command: ${command}`);