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.
@@ -0,0 +1,214 @@
1
+ /**
2
+ * CLI: guardvibe init <platform>
3
+ * Sets up MCP server configuration for AI coding hosts.
4
+ */
5
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
6
+ import { join, dirname } from "path";
7
+ import { homedir } from "os";
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
+ const SECURITY_RULES = `## Security Rules (enforced by GuardVibe MCP)
27
+
28
+ ### Mandatory checks
29
+ - AFTER writing any API route, Server Action, or database query: use the guardvibe scan_file MCP tool on the file
30
+ - BEFORE committing: use the guardvibe scan_staged MCP tool
31
+ - When GuardVibe reports HIGH or CRITICAL: fix immediately before moving on
32
+ - BEFORE creating a PR: use scan_changed_files to review all changes
33
+
34
+ ### Scan visibility — ALWAYS report GuardVibe results to the user
35
+ - AFTER every scan: ALWAYS tell the user the GuardVibe result, even when clean
36
+ - When issues found: "GuardVibe: [count] issues found ([severity]). Fixing..."
37
+ - When clean: "GuardVibe: scanned [filename] — no issues found."
38
+ - Include the summary stats line from scan output when present
39
+ - NEVER silently consume scan results — the user must see that GuardVibe is protecting their code
40
+ - Periodically (every ~10 scans or at session start): run security_stats tool and share the dashboard with the user
41
+
42
+ ### Writing secure code
43
+ - Every API route handler MUST have auth check before DB access
44
+ - Every POST endpoint MUST have input validation (zod/joi schema)
45
+ - Never pass user input to innerHTML, eval, exec, or raw SQL
46
+ - Never pass user-controlled URLs to fetch() without allowlist validation
47
+ - Always use select: in Prisma/Drizzle queries from Server Actions (never return full objects to client)
48
+ - Always validate redirect URLs against trusted domain allowlist
49
+ - Set security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)
50
+ - Verify webhook signatures before processing events
51
+ - Use parameterized queries, never string concatenation/template literals for SQL
52
+
53
+ ### When in doubt
54
+ - Use the guardvibe explain_remediation MCP tool with the rule ID for detailed fix guidance
55
+ - Use the guardvibe check_code MCP tool to verify a code snippet is secure before applying
56
+ `;
57
+ function readJsonFile(filePath) {
58
+ try {
59
+ const content = readFileSync(filePath, "utf-8");
60
+ return JSON.parse(content);
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ function writeJsonFile(filePath, data) {
67
+ const dir = dirname(filePath);
68
+ if (!existsSync(dir)) {
69
+ mkdirSync(dir, { recursive: true });
70
+ }
71
+ writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
72
+ }
73
+ function addToGitignore(entries) {
74
+ const gitignorePath = join(process.cwd(), ".gitignore");
75
+ let content = "";
76
+ try {
77
+ content = readFileSync(gitignorePath, "utf-8");
78
+ }
79
+ catch { /* no .gitignore yet */ }
80
+ const missing = entries.filter(e => !content.split("\n").some(line => line.trim() === e));
81
+ if (missing.length === 0)
82
+ return;
83
+ const block = `\n# GuardVibe (auto-added by guardvibe init)\n${missing.join("\n")}\n`;
84
+ writeFileSync(gitignorePath, content.trimEnd() + block, "utf-8");
85
+ console.log(` [OK] Added ${missing.join(", ")} to .gitignore`);
86
+ }
87
+ function setupClaudeGuide() {
88
+ const claudeSettingsDir = join(process.cwd(), ".claude");
89
+ if (!existsSync(claudeSettingsDir))
90
+ mkdirSync(claudeSettingsDir, { recursive: true });
91
+ const claudeSettingsPath = join(claudeSettingsDir, "settings.json");
92
+ const existingSettings = readJsonFile(claudeSettingsPath) || {};
93
+ if (!existingSettings.hooks)
94
+ existingSettings.hooks = {};
95
+ if (!existingSettings.hooks.PostToolUse) {
96
+ existingSettings.hooks.PostToolUse = [
97
+ {
98
+ matcher: "Edit|Write",
99
+ hooks: [{
100
+ type: "command",
101
+ command: "jq -r '.tool_input.file_path' | xargs npx -y guardvibe check --format buddy 2>/dev/null || true"
102
+ }]
103
+ }
104
+ ];
105
+ }
106
+ writeJsonFile(claudeSettingsPath, existingSettings);
107
+ console.log(` [OK] Claude Code hooks configured (.claude/settings.json)`);
108
+ const claudeMdPath = join(process.cwd(), "CLAUDE.md");
109
+ if (existsSync(claudeMdPath)) {
110
+ const content = readFileSync(claudeMdPath, "utf-8");
111
+ if (!content.includes("GuardVibe")) {
112
+ writeFileSync(claudeMdPath, content + "\n" + SECURITY_RULES, "utf-8");
113
+ console.log(` [OK] GuardVibe rules added to CLAUDE.md`);
114
+ }
115
+ }
116
+ else {
117
+ writeFileSync(claudeMdPath, `# Project Guidelines\n\n${SECURITY_RULES}`, "utf-8");
118
+ console.log(` [OK] Created CLAUDE.md with security rules`);
119
+ }
120
+ }
121
+ function setupCursorGuide() {
122
+ const cursorrules = join(process.cwd(), ".cursorrules");
123
+ if (existsSync(cursorrules)) {
124
+ const content = readFileSync(cursorrules, "utf-8");
125
+ if (!content.includes("GuardVibe")) {
126
+ writeFileSync(cursorrules, content + "\n" + SECURITY_RULES, "utf-8");
127
+ console.log(` [OK] GuardVibe rules added to .cursorrules`);
128
+ }
129
+ }
130
+ else {
131
+ writeFileSync(cursorrules, SECURITY_RULES, "utf-8");
132
+ console.log(` [OK] Created .cursorrules with security rules`);
133
+ }
134
+ }
135
+ function setupGeminiGuide() {
136
+ const geminiMd = join(process.cwd(), "GEMINI.md");
137
+ if (existsSync(geminiMd)) {
138
+ const content = readFileSync(geminiMd, "utf-8");
139
+ if (!content.includes("GuardVibe")) {
140
+ writeFileSync(geminiMd, content + "\n" + SECURITY_RULES, "utf-8");
141
+ console.log(` [OK] GuardVibe rules added to GEMINI.md`);
142
+ }
143
+ }
144
+ else {
145
+ writeFileSync(geminiMd, `# Project Guidelines\n\n${SECURITY_RULES}`, "utf-8");
146
+ console.log(` [OK] Created GEMINI.md with security rules`);
147
+ }
148
+ }
149
+ function setupSecurityGuide(platformName) {
150
+ if (platformName === "claude")
151
+ setupClaudeGuide();
152
+ else if (platformName === "cursor")
153
+ setupCursorGuide();
154
+ else if (platformName === "gemini")
155
+ setupGeminiGuide();
156
+ const gitignoreEntries = {
157
+ claude: [".claude.json", ".claude/", "CLAUDE.md"],
158
+ cursor: [".cursor/", ".cursorrules"],
159
+ gemini: ["GEMINI.md"],
160
+ };
161
+ const entries = gitignoreEntries[platformName] || [];
162
+ entries.push(".guardvibe/");
163
+ if (entries.length > 0)
164
+ addToGitignore(entries);
165
+ }
166
+ function setupPlatform(name) {
167
+ const platform = platforms[name];
168
+ if (!platform)
169
+ return false;
170
+ const existing = readJsonFile(platform.path);
171
+ if (existing) {
172
+ if (!existing.mcpServers) {
173
+ existing.mcpServers = {};
174
+ }
175
+ if (existing.mcpServers["guardvibe"]) {
176
+ console.log(` [OK] GuardVibe already configured in ${platform.description}`);
177
+ setupSecurityGuide(name);
178
+ return true;
179
+ }
180
+ existing.mcpServers["guardvibe"] = GUARDVIBE_MCP_CONFIG;
181
+ writeJsonFile(platform.path, existing);
182
+ }
183
+ else {
184
+ writeJsonFile(platform.path, {
185
+ mcpServers: {
186
+ guardvibe: GUARDVIBE_MCP_CONFIG,
187
+ },
188
+ });
189
+ }
190
+ console.log(` [OK] Added MCP server to ${platform.description}`);
191
+ setupSecurityGuide(name);
192
+ return true;
193
+ }
194
+ export function runInit(args) {
195
+ const platform = args[0]?.toLowerCase();
196
+ if (!platform) {
197
+ console.error(" [ERR] Please specify a platform: claude, gemini, cursor, or all");
198
+ process.exit(1);
199
+ }
200
+ console.log(`\n GuardVibe Security Setup\n`);
201
+ if (platform === "all") {
202
+ for (const name of Object.keys(platforms)) {
203
+ setupPlatform(name);
204
+ }
205
+ }
206
+ else if (platforms[platform]) {
207
+ setupPlatform(platform);
208
+ }
209
+ else {
210
+ console.error(` [ERR] Unknown platform: ${platform}. Available: claude, gemini, cursor, all`);
211
+ process.exit(1);
212
+ }
213
+ console.log(`\n [OK] Ready! Start coding securely.\n`);
214
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Host-specific remediation guidance.
3
+ * Enriches generic findings with platform-tailored fix instructions.
4
+ */
5
+ import type { HostFinding } from "../server/types.js";
6
+ /**
7
+ * Enrich a finding's remediation with host-specific fix instructions.
8
+ * Mutates the finding in place.
9
+ */
10
+ export declare function enrichRemediation(finding: HostFinding): void;
11
+ /**
12
+ * Enrich all findings with host-specific remediation.
13
+ */
14
+ export declare function enrichAllRemediations(findings: HostFinding[]): void;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Host-specific remediation guidance.
3
+ * Enriches generic findings with platform-tailored fix instructions.
4
+ */
5
+ const HOST_DETECTION = [
6
+ { pattern: /\.claude[\\/]|\.claude\.json/i, host: "Claude" },
7
+ { pattern: /\.cursor[\\/]/i, host: "Cursor" },
8
+ { pattern: /\.vscode[\\/]/i, host: "VS Code" },
9
+ { pattern: /\.gemini[\\/]/i, host: "Gemini" },
10
+ { pattern: /\.codeium[\\/]|windsurf/i, host: "Windsurf" },
11
+ { pattern: /\.(bashrc|zshrc|profile|bash_profile|zprofile)$/i, host: "Shell" },
12
+ { pattern: /\.env(\.\w+)?$/i, host: "Env" },
13
+ ];
14
+ function detectHost(file) {
15
+ if (!file)
16
+ return null;
17
+ for (const { pattern, host } of HOST_DETECTION) {
18
+ if (pattern.test(file))
19
+ return host;
20
+ }
21
+ return null;
22
+ }
23
+ // Platform-specific fix instructions per rule ID
24
+ const HOST_REMEDIATION = {
25
+ VG882: {
26
+ Claude: "Edit `.claude/settings.json` or `.claude.json` and remove the `ANTHROPIC_BASE_URL` override from the server's `env` block.",
27
+ Cursor: "Open Cursor Settings > MCP Servers and remove the base URL override from the server configuration.",
28
+ "VS Code": "Edit `.vscode/mcp.json` or `.vscode/settings.json` and remove the `ANTHROPIC_BASE_URL` entry from the server's env.",
29
+ Shell: "Remove the `export ANTHROPIC_BASE_URL=...` line from your shell profile. Restart your terminal.",
30
+ Env: "Remove the `ANTHROPIC_BASE_URL=...` line from the .env file, or add the URL to `trustedBaseUrls` in `.guardviberc`.",
31
+ },
32
+ VG883: {
33
+ Shell: "Remove the `export OPENAI_BASE_URL=...` line from your shell profile. Restart your terminal.",
34
+ Env: "Remove the `OPENAI_BASE_URL=...` line from the .env file, or add the URL to `trustedBaseUrls` in `.guardviberc`.",
35
+ },
36
+ VG884: {
37
+ Claude: "Edit `.claude/settings.json` > `hooks` section. Replace shell pipes and metacharacters with a simple command. Example:\n Before: `\"command\": \"curl ... | bash\"`\n After: `\"command\": \"npx -y guardvibe check\"`",
38
+ },
39
+ VG885: {
40
+ Claude: "Edit `.claude.json` > `permissions` > `allow` and replace the wildcard `\"*\"` with specific tool names your workflow needs.",
41
+ Cursor: "Open `.cursor/mcp.json` and restrict `allowedTools` to only the tools you use.",
42
+ "VS Code": "Edit `.vscode/mcp.json` and restrict `allowedTools` to only the tools you use.",
43
+ },
44
+ VG890: {
45
+ Claude: "Edit `.claude/settings.json` > `hooks` and remove any commands that make network requests (curl, wget, nc). Hooks must only perform local operations.",
46
+ },
47
+ VG891: {
48
+ Claude: "Edit `.claude/settings.json` > `hooks` and remove pipe chains to interpreters (| bash, | python, | node). Write a dedicated script file instead.",
49
+ },
50
+ VG892: {
51
+ Claude: "Edit `.claude.json` > `mcpServers` and replace `file://` URLs with npm package references:\n Before: `\"url\": \"file:///path/to/server\"`\n After: `\"command\": \"npx\", \"args\": [\"-y\", \"package-name\"]`",
52
+ Cursor: "Edit `.cursor/mcp.json` > `mcpServers` and replace `file://` server URLs with npm packages or HTTPS endpoints.",
53
+ "VS Code": "Edit `.vscode/mcp.json` and replace `file://` server URLs with npm packages or HTTPS endpoints.",
54
+ },
55
+ VG893: {
56
+ Claude: "Edit `.claude.json` > `permissions` > `allow` and replace broad wildcards (e.g., `Bash(*)`) with specific patterns:\n Before: `\"Bash(*)\"`\n After: `\"Bash(npm test)\", \"Bash(npm run build)\"`",
57
+ },
58
+ VG894: {
59
+ Claude: "Edit `.claude.json` > `mcpServers` and remove references to sensitive paths (.ssh, .aws, .gnupg, /etc). Limit servers to project directory access.",
60
+ Cursor: "Edit `.cursor/mcp.json` and ensure no MCP server accesses paths outside the project directory.",
61
+ "VS Code": "Edit `.vscode/mcp.json` and ensure no MCP server accesses paths outside the project directory.",
62
+ },
63
+ VG895: {
64
+ Claude: "Edit `.claude/settings.json` > `hooks` > `PostToolUse` and remove file-modifying commands (cp, mv, rm, chmod). PostToolUse hooks should only observe and report, never modify files.",
65
+ },
66
+ };
67
+ /**
68
+ * Enrich a finding's remediation with host-specific fix instructions.
69
+ * Mutates the finding in place.
70
+ */
71
+ export function enrichRemediation(finding) {
72
+ const host = detectHost(finding.file);
73
+ if (!host)
74
+ return;
75
+ const hostFixes = HOST_REMEDIATION[finding.ruleId];
76
+ if (!hostFixes)
77
+ return;
78
+ const specific = hostFixes[host];
79
+ if (!specific)
80
+ return;
81
+ // Append host-specific guidance below generic remediation
82
+ finding.remediation = `${finding.remediation}\n\n**${host} fix:** ${specific}`;
83
+ }
84
+ /**
85
+ * Enrich all findings with host-specific remediation.
86
+ */
87
+ export function enrichAllRemediations(findings) {
88
+ for (const f of findings) {
89
+ enrichRemediation(f);
90
+ }
91
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * CLI: guardvibe scan [path], guardvibe diff [base], guardvibe check <file>
3
+ * Also: guardvibe-scan (pre-commit hook / CI entry point)
4
+ */
5
+ export declare function runScan(): Promise<void>;
6
+ export declare function runDirectoryScan(targetPath: string, flags: Record<string, string | true>): Promise<void>;
7
+ export declare function runDiffScan(base: string, flags: Record<string, string | true>): Promise<void>;
8
+ export declare function runFileCheck(filePath: string, flags: Record<string, string | true>): Promise<void>;
9
+ export declare function handleScanCommand(args: string[]): Promise<void>;
10
+ export declare function handleDiffCommand(args: string[]): Promise<void>;
11
+ export declare function handleCheckCommand(args: string[]): Promise<void>;
@@ -0,0 +1,222 @@
1
+ /**
2
+ * CLI: guardvibe scan [path], guardvibe diff [base], guardvibe check <file>
3
+ * Also: guardvibe-scan (pre-commit hook / CI entry point)
4
+ */
5
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "fs";
6
+ import { resolve, extname, basename, join, dirname } from "path";
7
+ import { parseArgs, shouldFail, validateFormat, getOutputPath, getStringFlag } from "./args.js";
8
+ function safeWriteOutput(outputFile, result) {
9
+ const dir = dirname(outputFile);
10
+ if (!existsSync(dir)) {
11
+ mkdirSync(dir, { recursive: true });
12
+ }
13
+ writeFileSync(outputFile, result, "utf-8");
14
+ console.log(` [OK] Results written to ${outputFile}`);
15
+ }
16
+ export async function runScan() {
17
+ const args = process.argv.slice(2);
18
+ const { flags } = parseArgs(args);
19
+ const format = validateFormat(flags);
20
+ const outputFile = getOutputPath(flags);
21
+ let result;
22
+ if (format === "sarif") {
23
+ const { exportSarif } = await import("../tools/export-sarif.js");
24
+ result = exportSarif(process.cwd());
25
+ }
26
+ else {
27
+ const { scanStaged } = await import("../tools/scan-staged.js");
28
+ result = scanStaged(process.cwd(), format === "json" ? "json" : "markdown");
29
+ }
30
+ if (outputFile) {
31
+ safeWriteOutput(outputFile, result);
32
+ }
33
+ else {
34
+ console.log(result);
35
+ }
36
+ if (format !== "sarif") {
37
+ const failOn = getStringFlag(flags, "fail-on") ?? "critical";
38
+ if (shouldFail(result, failOn))
39
+ process.exit(1);
40
+ }
41
+ }
42
+ export async function runDirectoryScan(targetPath, flags) {
43
+ const { scanDirectory } = await import("../tools/scan-directory.js");
44
+ const format = validateFormat(flags);
45
+ const outputFile = getOutputPath(flags);
46
+ const baselinePath = getStringFlag(flags, "baseline");
47
+ const saveBaseline = flags["save-baseline"] === true || typeof flags["save-baseline"] === "string";
48
+ const scanPath = resolve(targetPath);
49
+ let result;
50
+ if (format === "sarif") {
51
+ const { exportSarif } = await import("../tools/export-sarif.js");
52
+ result = exportSarif(scanPath);
53
+ }
54
+ else {
55
+ result = scanDirectory(scanPath, true, [], format === "json" ? "json" : "markdown", undefined, baselinePath ?? undefined);
56
+ }
57
+ if (outputFile) {
58
+ safeWriteOutput(outputFile, result);
59
+ }
60
+ else {
61
+ console.log(result);
62
+ }
63
+ if (saveBaseline && format === "json") {
64
+ const baselineFile = typeof flags["save-baseline"] === "string"
65
+ ? flags["save-baseline"]
66
+ : join(scanPath, ".guardvibe-baseline.json");
67
+ safeWriteOutput(baselineFile, result);
68
+ }
69
+ if (format !== "sarif") {
70
+ const failOn = getStringFlag(flags, "fail-on") ?? "critical";
71
+ if (shouldFail(result, failOn))
72
+ process.exit(1);
73
+ }
74
+ }
75
+ export async function runDiffScan(base, flags) {
76
+ const { execFileSync } = await import("child_process");
77
+ const { analyzeCode } = await import("../tools/check-code.js");
78
+ const { EXTENSION_MAP, CONFIG_FILE_MAP } = await import("../utils/constants.js");
79
+ const format = validateFormat(flags);
80
+ const outputFile = getOutputPath(flags);
81
+ const root = resolve(".");
82
+ let changedFiles;
83
+ try {
84
+ const output = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACMR", base], { cwd: root, encoding: "utf-8" });
85
+ changedFiles = output.trim().split("\n").filter(Boolean);
86
+ }
87
+ catch {
88
+ console.error(" [ERR] Failed to get git diff. Ensure you're in a git repository.");
89
+ process.exit(1);
90
+ }
91
+ if (changedFiles.length === 0) {
92
+ console.log(" No changed files to scan.");
93
+ return;
94
+ }
95
+ const allFindings = [];
96
+ for (const relPath of changedFiles) {
97
+ const fullPath = resolve(root, relPath);
98
+ if (!existsSync(fullPath))
99
+ continue;
100
+ const ext = extname(relPath).toLowerCase();
101
+ let language = EXTENSION_MAP[ext];
102
+ if (!language && basename(relPath).startsWith("Dockerfile"))
103
+ language = "dockerfile";
104
+ if (!language)
105
+ language = CONFIG_FILE_MAP[basename(relPath)];
106
+ if (!language)
107
+ continue;
108
+ try {
109
+ const content = readFileSync(fullPath, "utf-8");
110
+ const findings = analyzeCode(content, language, undefined, fullPath, root);
111
+ for (const f of findings) {
112
+ allFindings.push({ file: relPath, severity: f.rule.severity, name: f.rule.name, id: f.rule.id, line: f.line, fix: f.rule.fix });
113
+ }
114
+ }
115
+ catch { /* skip */ }
116
+ }
117
+ let result;
118
+ if (format === "json") {
119
+ const critical = allFindings.filter(f => f.severity === "critical").length;
120
+ const high = allFindings.filter(f => f.severity === "high").length;
121
+ const medium = allFindings.filter(f => f.severity === "medium").length;
122
+ result = JSON.stringify({
123
+ summary: { total: allFindings.length, critical, high, medium, changedFiles: changedFiles.length, blocked: critical > 0 || high > 0 },
124
+ findings: allFindings,
125
+ });
126
+ }
127
+ else {
128
+ const lines = [`# GuardVibe Diff Report`, ``, `Base: ${base}`, `Changed files: ${changedFiles.length}`, `Issues: ${allFindings.length}`, ``];
129
+ if (allFindings.length === 0) {
130
+ lines.push(`All changed files passed security checks.`);
131
+ }
132
+ else {
133
+ const sev = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
134
+ allFindings.sort((a, b) => (sev[a.severity] ?? 99) - (sev[b.severity] ?? 99));
135
+ for (const f of allFindings) {
136
+ lines.push(`- [${f.severity.toUpperCase()}] **${f.name}** (${f.id}) in ${f.file}:${f.line}`);
137
+ lines.push(` Fix: ${f.fix}`);
138
+ }
139
+ }
140
+ result = lines.join("\n");
141
+ }
142
+ if (outputFile) {
143
+ safeWriteOutput(outputFile, result);
144
+ }
145
+ else {
146
+ console.log(result);
147
+ }
148
+ const failOn = getStringFlag(flags, "fail-on") ?? "critical";
149
+ if (failOn !== "none") {
150
+ const failLevels = {
151
+ low: ["critical", "high", "medium", "low"],
152
+ medium: ["critical", "high", "medium"],
153
+ high: ["critical", "high"],
154
+ critical: ["critical"],
155
+ };
156
+ const levels = failLevels[failOn] || failLevels.critical;
157
+ if (allFindings.some(f => levels.includes(f.severity)))
158
+ process.exit(1);
159
+ }
160
+ }
161
+ export async function runFileCheck(filePath, flags) {
162
+ const { checkCode } = await import("../tools/check-code.js");
163
+ const resolved = resolve(filePath);
164
+ if (!existsSync(resolved)) {
165
+ console.error(` [ERR] File not found: ${resolved}`);
166
+ process.exit(1);
167
+ }
168
+ const content = readFileSync(resolved, "utf-8");
169
+ const ext = extname(resolved).toLowerCase();
170
+ const extMap = {
171
+ ".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
172
+ ".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
173
+ ".py": "python", ".go": "go", ".html": "html", ".sql": "sql",
174
+ ".sh": "shell", ".bash": "shell", ".yml": "yaml", ".yaml": "yaml",
175
+ ".tf": "terraform", ".toml": "toml", ".json": "json",
176
+ };
177
+ let language = extMap[ext];
178
+ if (!language && basename(resolved).startsWith("Dockerfile"))
179
+ language = "dockerfile";
180
+ if (!language) {
181
+ console.error(` [ERR] Unsupported file type: ${ext}`);
182
+ process.exit(1);
183
+ }
184
+ const format = validateFormat(flags);
185
+ const formatArg = format === "json" ? "json" : format === "buddy" ? "buddy" : "markdown";
186
+ const result = checkCode(content, language, undefined, resolved, undefined, formatArg);
187
+ const outputFile = getOutputPath(flags);
188
+ if (outputFile) {
189
+ safeWriteOutput(outputFile, result);
190
+ }
191
+ else {
192
+ console.log(result);
193
+ }
194
+ const failOn = getStringFlag(flags, "fail-on") ?? "critical";
195
+ if (shouldFail(result, failOn))
196
+ process.exit(1);
197
+ }
198
+ export async function handleScanCommand(args) {
199
+ const { flags, positional } = parseArgs(args);
200
+ const targetPath = positional[0] ?? ".";
201
+ if (targetPath !== "." && existsSync(targetPath) && !statSync(targetPath).isDirectory()) {
202
+ console.log(` [INFO] "${targetPath}" is a file. Running: guardvibe check ${targetPath}\n`);
203
+ await runFileCheck(targetPath, flags);
204
+ }
205
+ else {
206
+ await runDirectoryScan(targetPath, flags);
207
+ }
208
+ }
209
+ export async function handleDiffCommand(args) {
210
+ const { flags, positional } = parseArgs(args);
211
+ const base = positional[0] ?? "main";
212
+ await runDiffScan(base, flags);
213
+ }
214
+ export async function handleCheckCommand(args) {
215
+ const { flags, positional } = parseArgs(args);
216
+ const filePath = positional[0];
217
+ if (!filePath) {
218
+ console.error(" [ERR] Please specify a file: npx guardvibe check <file>");
219
+ process.exit(1);
220
+ }
221
+ await runFileCheck(filePath, flags);
222
+ }