devrail 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1298 @@
1
+ import {
2
+ getRuleById,
3
+ rules
4
+ } from "./chunk-J22FFU7Z.js";
5
+
6
+ // src/types/config.ts
7
+ import { z } from "zod";
8
+ var RuleSeveritySchema = z.enum(["info", "warn", "error"]);
9
+ var RuleScopeSchema = z.enum(["changed", "all"]);
10
+ var LevelSchema = z.enum(["basic", "standard", "strict"]);
11
+ var PresetSchema = z.enum([
12
+ "node-api",
13
+ "nextjs-app",
14
+ "python-api",
15
+ "cli-tool",
16
+ "library",
17
+ "monorepo"
18
+ ]);
19
+ var RuleConfigSchema = z.object({
20
+ enabled: z.boolean().default(true),
21
+ severity: RuleSeveritySchema.optional(),
22
+ autofix: z.boolean().optional(),
23
+ blocking: z.boolean().optional(),
24
+ scope: RuleScopeSchema.optional(),
25
+ options: z.record(z.unknown()).optional()
26
+ });
27
+ var VibeGuardConfigSchema = z.object({
28
+ preset: PresetSchema.optional(),
29
+ level: LevelSchema.default("standard"),
30
+ rules: z.record(z.union([z.boolean(), RuleConfigSchema])).optional(),
31
+ include: z.array(z.string()).optional(),
32
+ exclude: z.array(z.string()).optional(),
33
+ ci: z.object({
34
+ failOn: RuleSeveritySchema.default("error"),
35
+ reportFormat: z.enum(["console", "json", "sarif"]).default("console")
36
+ }).optional(),
37
+ tools: z.object({
38
+ eslint: z.boolean().default(true),
39
+ semgrep: z.boolean().default(true),
40
+ gitleaks: z.boolean().default(true),
41
+ osvScanner: z.boolean().default(true)
42
+ }).optional()
43
+ });
44
+
45
+ // src/presets/node-api.ts
46
+ var nodeApiPreset = {
47
+ name: "node-api",
48
+ description: "Node.js API backend (Express, Fastify, Koa, etc.)",
49
+ rules: {
50
+ basic: [
51
+ "secrets.no-plaintext",
52
+ "secrets.no-env-commit",
53
+ "secrets.gitignore-required",
54
+ "deps.lockfile.required",
55
+ "deps.no-vulnerable",
56
+ "deps.no-typosquatting",
57
+ "security.cors.safe-default",
58
+ "security.no-eval",
59
+ "security.no-prototype-pollution",
60
+ "auth.no-weak-jwt",
61
+ "auth.no-hardcoded-credentials",
62
+ "api.no-sensitive-logging",
63
+ "sql.no-string-concat",
64
+ "tests.no-skipped",
65
+ "code.no-unused-vars"
66
+ ],
67
+ standard: [
68
+ "secrets.no-plaintext",
69
+ "secrets.no-env-commit",
70
+ "secrets.gitignore-required",
71
+ "deps.lockfile.required",
72
+ "deps.no-unpinned",
73
+ "deps.no-git-deps",
74
+ "deps.no-vulnerable",
75
+ "deps.no-typosquatting",
76
+ "security.headers.required",
77
+ "security.cors.safe-default",
78
+ "security.no-eval",
79
+ "security.no-unsafe-regex",
80
+ "security.no-prototype-pollution",
81
+ "auth.no-weak-jwt",
82
+ "auth.no-hardcoded-credentials",
83
+ "auth.session-secure",
84
+ "api.validation.required",
85
+ "api.no-sensitive-logging",
86
+ "sql.no-string-concat",
87
+ "tests.unit.required",
88
+ "tests.coverage.min-50",
89
+ "tests.no-skipped",
90
+ "logging.no-pii",
91
+ "logging.no-console",
92
+ "code.no-any",
93
+ "code.strict-mode",
94
+ "code.no-unused-vars",
95
+ "config.node-version",
96
+ "config.editor-config"
97
+ ],
98
+ strict: [
99
+ "secrets.no-plaintext",
100
+ "secrets.no-env-commit",
101
+ "secrets.gitignore-required",
102
+ "deps.lockfile.required",
103
+ "deps.no-unpinned",
104
+ "deps.no-git-deps",
105
+ "deps.no-vulnerable",
106
+ "deps.no-typosquatting",
107
+ "deps.license-check",
108
+ "security.headers.required",
109
+ "security.cors.safe-default",
110
+ "security.no-eval",
111
+ "security.no-unsafe-regex",
112
+ "security.no-prototype-pollution",
113
+ "auth.no-weak-jwt",
114
+ "auth.no-hardcoded-credentials",
115
+ "auth.session-secure",
116
+ "api.validation.required",
117
+ "api.rate-limiting",
118
+ "api.no-sensitive-logging",
119
+ "sql.no-string-concat",
120
+ "sql.no-raw-queries",
121
+ "tests.unit.required",
122
+ "tests.coverage.min-80",
123
+ "tests.no-skipped",
124
+ "logging.no-pii",
125
+ "logging.no-console",
126
+ "code.no-any",
127
+ "code.strict-mode",
128
+ "code.no-unused-vars",
129
+ "config.node-version",
130
+ "config.editor-config"
131
+ ]
132
+ },
133
+ tools: ["eslint", "semgrep", "gitleaks", "osv-scanner", "vitest"],
134
+ eslintExtends: [
135
+ "eslint:recommended",
136
+ "plugin:@typescript-eslint/recommended",
137
+ "plugin:security/recommended-legacy"
138
+ ],
139
+ semgrepRules: [
140
+ "p/javascript",
141
+ "p/typescript",
142
+ "p/nodejs",
143
+ "p/jwt",
144
+ "p/sql-injection",
145
+ "p/secrets"
146
+ ]
147
+ };
148
+
149
+ // src/presets/nextjs-app.ts
150
+ var nextjsAppPreset = {
151
+ name: "nextjs-app",
152
+ description: "Next.js full-stack application",
153
+ rules: {
154
+ basic: [
155
+ "secrets.no-plaintext",
156
+ "secrets.no-env-commit",
157
+ "secrets.gitignore-required",
158
+ "deps.lockfile.required",
159
+ "deps.no-vulnerable",
160
+ "deps.no-typosquatting",
161
+ "security.cors.safe-default",
162
+ "security.no-eval",
163
+ "security.no-prototype-pollution",
164
+ "auth.no-weak-jwt",
165
+ "auth.no-hardcoded-credentials",
166
+ "api.no-sensitive-logging",
167
+ "sql.no-string-concat",
168
+ "tests.no-skipped",
169
+ "code.no-unused-vars"
170
+ ],
171
+ standard: [
172
+ "secrets.no-plaintext",
173
+ "secrets.no-env-commit",
174
+ "secrets.gitignore-required",
175
+ "deps.lockfile.required",
176
+ "deps.no-unpinned",
177
+ "deps.no-git-deps",
178
+ "deps.no-vulnerable",
179
+ "deps.no-typosquatting",
180
+ "security.headers.required",
181
+ "security.cors.safe-default",
182
+ "security.no-eval",
183
+ "security.no-unsafe-regex",
184
+ "security.no-prototype-pollution",
185
+ "auth.no-weak-jwt",
186
+ "auth.no-hardcoded-credentials",
187
+ "auth.session-secure",
188
+ "api.validation.required",
189
+ "api.no-sensitive-logging",
190
+ "sql.no-string-concat",
191
+ "tests.unit.required",
192
+ "tests.coverage.min-50",
193
+ "tests.no-skipped",
194
+ "logging.no-pii",
195
+ "logging.no-console",
196
+ "code.no-any",
197
+ "code.strict-mode",
198
+ "code.no-unused-vars",
199
+ "config.node-version",
200
+ "config.editor-config"
201
+ ],
202
+ strict: [
203
+ "secrets.no-plaintext",
204
+ "secrets.no-env-commit",
205
+ "secrets.gitignore-required",
206
+ "deps.lockfile.required",
207
+ "deps.no-unpinned",
208
+ "deps.no-git-deps",
209
+ "deps.no-vulnerable",
210
+ "deps.no-typosquatting",
211
+ "deps.license-check",
212
+ "security.headers.required",
213
+ "security.cors.safe-default",
214
+ "security.no-eval",
215
+ "security.no-unsafe-regex",
216
+ "security.no-prototype-pollution",
217
+ "auth.no-weak-jwt",
218
+ "auth.no-hardcoded-credentials",
219
+ "auth.session-secure",
220
+ "api.validation.required",
221
+ "api.rate-limiting",
222
+ "api.no-sensitive-logging",
223
+ "sql.no-string-concat",
224
+ "sql.no-raw-queries",
225
+ "tests.unit.required",
226
+ "tests.coverage.min-80",
227
+ "tests.no-skipped",
228
+ "logging.no-pii",
229
+ "logging.no-console",
230
+ "code.no-any",
231
+ "code.strict-mode",
232
+ "code.no-unused-vars",
233
+ "config.node-version",
234
+ "config.editor-config"
235
+ ]
236
+ },
237
+ tools: ["eslint", "semgrep", "gitleaks", "osv-scanner", "vitest"],
238
+ eslintExtends: [
239
+ "eslint:recommended",
240
+ "plugin:@typescript-eslint/recommended",
241
+ "plugin:security/recommended-legacy",
242
+ "next/core-web-vitals"
243
+ ],
244
+ semgrepRules: [
245
+ "p/javascript",
246
+ "p/typescript",
247
+ "p/nodejs",
248
+ "p/react",
249
+ "p/nextjs",
250
+ "p/jwt",
251
+ "p/sql-injection",
252
+ "p/secrets"
253
+ ]
254
+ };
255
+
256
+ // src/presets/index.ts
257
+ var presets = {
258
+ "node-api": nodeApiPreset,
259
+ "nextjs-app": nextjsAppPreset,
260
+ "python-api": nodeApiPreset,
261
+ // TODO: implement
262
+ "cli-tool": nodeApiPreset,
263
+ // TODO: implement
264
+ library: nodeApiPreset,
265
+ // TODO: implement
266
+ monorepo: nodeApiPreset
267
+ // TODO: implement
268
+ };
269
+ function getPreset(name) {
270
+ const preset = presets[name];
271
+ if (!preset) {
272
+ throw new Error(`Unknown preset: ${name}`);
273
+ }
274
+ return preset;
275
+ }
276
+ function listPresets() {
277
+ return Object.keys(presets);
278
+ }
279
+
280
+ // src/engine/config-loader.ts
281
+ import { cosmiconfig } from "cosmiconfig";
282
+ import { z as z2 } from "zod";
283
+ var explorer = cosmiconfig("vibeguard", {
284
+ searchPlaces: [
285
+ "vibeguard.config.js",
286
+ "vibeguard.config.mjs",
287
+ "vibeguard.config.cjs",
288
+ "vibeguard.config.json",
289
+ "vibeguard.config.yaml",
290
+ "vibeguard.config.yml",
291
+ ".vibeguardrc",
292
+ ".vibeguardrc.json",
293
+ ".vibeguardrc.yaml",
294
+ ".vibeguardrc.yml",
295
+ "package.json"
296
+ ]
297
+ });
298
+ async function loadConfig(cwd = process.cwd()) {
299
+ try {
300
+ const result = await explorer.search(cwd);
301
+ if (!result || result.isEmpty) {
302
+ return null;
303
+ }
304
+ const validated = VibeGuardConfigSchema.parse(result.config);
305
+ return {
306
+ config: validated,
307
+ filepath: result.filepath,
308
+ isEmpty: false
309
+ };
310
+ } catch (error) {
311
+ if (error instanceof z2.ZodError) {
312
+ const issues = error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
313
+ throw new Error(`Invalid vibeguard config:
314
+ ${issues}`);
315
+ }
316
+ throw error;
317
+ }
318
+ }
319
+ async function loadConfigFromFile(filepath) {
320
+ const result = await explorer.load(filepath);
321
+ if (!result || result.isEmpty) {
322
+ throw new Error(`Config file is empty: ${filepath}`);
323
+ }
324
+ const validated = VibeGuardConfigSchema.parse(result.config);
325
+ return {
326
+ config: validated,
327
+ filepath: result.filepath,
328
+ isEmpty: false
329
+ };
330
+ }
331
+ function getDefaultConfig() {
332
+ return {
333
+ level: "standard",
334
+ ci: {
335
+ failOn: "error",
336
+ reportFormat: "console"
337
+ },
338
+ tools: {
339
+ eslint: true,
340
+ semgrep: true,
341
+ gitleaks: true,
342
+ osvScanner: true
343
+ }
344
+ };
345
+ }
346
+
347
+ // src/engine/rule-engine.ts
348
+ function resolveRules(config) {
349
+ const level = config.level ?? "standard";
350
+ let activeRuleIds = [];
351
+ if (config.preset) {
352
+ const preset = getPreset(config.preset);
353
+ activeRuleIds = preset.rules[level] ?? [];
354
+ } else {
355
+ activeRuleIds = rules.filter((r) => r.levels[level]).map((r) => r.id);
356
+ }
357
+ const resolvedRules = [];
358
+ for (const ruleId of activeRuleIds) {
359
+ const definition = getRuleById(ruleId);
360
+ if (!definition) continue;
361
+ const ruleConfig = config.rules?.[ruleId];
362
+ let enabled = true;
363
+ let severity = definition.severity;
364
+ let blocking = definition.blocking;
365
+ let autofix = definition.autofix;
366
+ if (typeof ruleConfig === "boolean") {
367
+ enabled = ruleConfig;
368
+ } else if (ruleConfig) {
369
+ enabled = ruleConfig.enabled ?? true;
370
+ severity = ruleConfig.severity ?? severity;
371
+ blocking = ruleConfig.blocking ?? blocking;
372
+ autofix = ruleConfig.autofix ?? autofix;
373
+ }
374
+ if (enabled) {
375
+ resolvedRules.push({
376
+ definition,
377
+ severity,
378
+ enabled,
379
+ blocking,
380
+ autofix
381
+ });
382
+ }
383
+ }
384
+ if (config.rules) {
385
+ for (const [ruleId, ruleConfig] of Object.entries(config.rules)) {
386
+ if (activeRuleIds.includes(ruleId)) continue;
387
+ const definition = getRuleById(ruleId);
388
+ if (!definition) continue;
389
+ let enabled = true;
390
+ if (typeof ruleConfig === "boolean") {
391
+ enabled = ruleConfig;
392
+ } else {
393
+ enabled = ruleConfig.enabled ?? true;
394
+ }
395
+ if (enabled) {
396
+ resolvedRules.push({
397
+ definition,
398
+ severity: typeof ruleConfig === "object" ? ruleConfig.severity ?? definition.severity : definition.severity,
399
+ enabled,
400
+ blocking: typeof ruleConfig === "object" ? ruleConfig.blocking ?? definition.blocking : definition.blocking,
401
+ autofix: typeof ruleConfig === "object" ? ruleConfig.autofix ?? definition.autofix : definition.autofix
402
+ });
403
+ }
404
+ }
405
+ }
406
+ return resolvedRules;
407
+ }
408
+ function groupRulesByTool(resolvedRules) {
409
+ const grouped = /* @__PURE__ */ new Map();
410
+ for (const rule of resolvedRules) {
411
+ const tool = rule.definition.tool;
412
+ const existing = grouped.get(tool) ?? [];
413
+ existing.push(rule);
414
+ grouped.set(tool, existing);
415
+ }
416
+ return grouped;
417
+ }
418
+ function calculateScore(results) {
419
+ if (results.length === 0) return 100;
420
+ const weights = { error: 10, warn: 3, info: 1 };
421
+ let totalPenalty = 0;
422
+ for (const result of results) {
423
+ totalPenalty += weights[result.severity] ?? 0;
424
+ }
425
+ const score = Math.max(0, 100 - totalPenalty);
426
+ return Math.round(score);
427
+ }
428
+ function shouldFail(results, failOn) {
429
+ const severityOrder = ["info", "warn", "error"];
430
+ const failIndex = severityOrder.indexOf(failOn);
431
+ return results.some((r) => severityOrder.indexOf(r.severity) >= failIndex);
432
+ }
433
+
434
+ // src/engine/stack-detector.ts
435
+ import { readFile, readdir } from "fs/promises";
436
+ import { join } from "path";
437
+ async function detectStack(cwd) {
438
+ const result = {
439
+ preset: "node-api",
440
+ confidence: 0,
441
+ language: "unknown",
442
+ hasTests: false,
443
+ hasTypeScript: false,
444
+ hasCi: false,
445
+ issues: []
446
+ };
447
+ const files = await safeReadDir(cwd);
448
+ if (files.includes("package-lock.json")) {
449
+ result.packageManager = "npm";
450
+ } else if (files.includes("yarn.lock")) {
451
+ result.packageManager = "yarn";
452
+ } else if (files.includes("pnpm-lock.yaml")) {
453
+ result.packageManager = "pnpm";
454
+ } else if (files.includes("bun.lockb")) {
455
+ result.packageManager = "bun";
456
+ }
457
+ if (files.includes("tsconfig.json")) {
458
+ result.hasTypeScript = true;
459
+ result.language = "typescript";
460
+ }
461
+ if (files.includes(".github")) {
462
+ const githubFiles = await safeReadDir(join(cwd, ".github"));
463
+ if (githubFiles.includes("workflows")) {
464
+ result.hasCi = true;
465
+ }
466
+ }
467
+ if (files.includes(".gitlab-ci.yml")) {
468
+ result.hasCi = true;
469
+ }
470
+ const testIndicators = ["test", "tests", "__tests__", "spec", "specs", "vitest.config.ts", "jest.config.js"];
471
+ result.hasTests = testIndicators.some((t) => files.includes(t));
472
+ if (files.includes("package.json")) {
473
+ result.language = result.hasTypeScript ? "typescript" : "javascript";
474
+ const pkg = await readPackageJson(cwd);
475
+ if (pkg) {
476
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
477
+ if (allDeps["next"]) {
478
+ result.preset = "nextjs-app";
479
+ result.framework = "next.js";
480
+ result.confidence = 95;
481
+ } else if (allDeps["express"] || allDeps["fastify"] || allDeps["koa"] || allDeps["hono"]) {
482
+ result.preset = "node-api";
483
+ result.framework = allDeps["express"] ? "express" : allDeps["fastify"] ? "fastify" : allDeps["koa"] ? "koa" : "hono";
484
+ result.confidence = 90;
485
+ } else if (pkg.main || pkg.exports) {
486
+ result.preset = "library";
487
+ result.confidence = 70;
488
+ } else {
489
+ result.preset = "node-api";
490
+ result.confidence = 50;
491
+ }
492
+ if (pkg.workspaces || files.includes("lerna.json") || files.includes("pnpm-workspace.yaml")) {
493
+ result.preset = "monorepo";
494
+ result.confidence = 85;
495
+ }
496
+ }
497
+ }
498
+ if (files.includes("requirements.txt") || files.includes("pyproject.toml") || files.includes("setup.py")) {
499
+ result.language = "python";
500
+ result.preset = "python-api";
501
+ result.confidence = 80;
502
+ if (files.includes("pyproject.toml")) {
503
+ const content = await safeReadFile(join(cwd, "pyproject.toml"));
504
+ if (content?.includes("fastapi")) {
505
+ result.framework = "fastapi";
506
+ result.confidence = 95;
507
+ } else if (content?.includes("django")) {
508
+ result.framework = "django";
509
+ result.confidence = 95;
510
+ } else if (content?.includes("flask")) {
511
+ result.framework = "flask";
512
+ result.confidence = 95;
513
+ }
514
+ }
515
+ }
516
+ await collectIssues(cwd, files, result);
517
+ return result;
518
+ }
519
+ async function collectIssues(cwd, files, result) {
520
+ if (!files.includes(".gitignore")) {
521
+ result.issues.push("Missing .gitignore file");
522
+ } else {
523
+ const gitignore = await safeReadFile(join(cwd, ".gitignore"));
524
+ if (gitignore && !gitignore.includes(".env")) {
525
+ result.issues.push(".gitignore missing .env pattern");
526
+ }
527
+ }
528
+ const envFiles = files.filter((f) => f.startsWith(".env") && !f.endsWith(".example"));
529
+ if (envFiles.length > 0) {
530
+ result.issues.push(`Potential secrets: ${envFiles.join(", ")}`);
531
+ }
532
+ if (result.language !== "python" && !result.packageManager) {
533
+ result.issues.push("No lockfile found (npm/yarn/pnpm)");
534
+ }
535
+ if (!result.hasTests) {
536
+ result.issues.push("No test files detected");
537
+ }
538
+ if (result.hasTypeScript) {
539
+ const tsconfig = await safeReadFile(join(cwd, "tsconfig.json"));
540
+ if (tsconfig && !tsconfig.includes('"strict": true')) {
541
+ result.issues.push("TypeScript strict mode not enabled");
542
+ }
543
+ }
544
+ if (!result.hasCi) {
545
+ result.issues.push("No CI/CD configuration found");
546
+ }
547
+ }
548
+ async function safeReadDir(path) {
549
+ try {
550
+ return await readdir(path);
551
+ } catch {
552
+ return [];
553
+ }
554
+ }
555
+ async function safeReadFile(path) {
556
+ try {
557
+ return await readFile(path, "utf-8");
558
+ } catch {
559
+ return null;
560
+ }
561
+ }
562
+ async function readPackageJson(cwd) {
563
+ try {
564
+ const content = await readFile(join(cwd, "package.json"), "utf-8");
565
+ return JSON.parse(content);
566
+ } catch {
567
+ return null;
568
+ }
569
+ }
570
+
571
+ // src/engine/baseline.ts
572
+ import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
573
+ import { join as join2, dirname } from "path";
574
+ import { createHash } from "crypto";
575
+ var BASELINE_VERSION = "1.0";
576
+ var DEFAULT_BASELINE_PATH = ".devrail/baseline.json";
577
+ function createFingerprint(result) {
578
+ const content = [result.ruleId, result.file ?? "", result.message].join("|");
579
+ return createHash("sha256").update(content).digest("hex").slice(0, 16);
580
+ }
581
+ async function loadBaseline(cwd, path) {
582
+ const baselinePath = join2(cwd, path ?? DEFAULT_BASELINE_PATH);
583
+ try {
584
+ const content = await readFile2(baselinePath, "utf-8");
585
+ return JSON.parse(content);
586
+ } catch {
587
+ return null;
588
+ }
589
+ }
590
+ async function saveBaseline(cwd, baseline, path) {
591
+ const baselinePath = join2(cwd, path ?? DEFAULT_BASELINE_PATH);
592
+ await mkdir(dirname(baselinePath), { recursive: true });
593
+ await writeFile(baselinePath, JSON.stringify(baseline, null, 2), "utf-8");
594
+ }
595
+ function createBaseline(results) {
596
+ const now = (/* @__PURE__ */ new Date()).toISOString();
597
+ return {
598
+ version: BASELINE_VERSION,
599
+ createdAt: now,
600
+ updatedAt: now,
601
+ entries: results.map((r) => ({
602
+ ruleId: r.ruleId,
603
+ file: r.file,
604
+ line: r.line,
605
+ fingerprint: createFingerprint(r),
606
+ message: r.message,
607
+ createdAt: now
608
+ }))
609
+ };
610
+ }
611
+ function filterNewIssues(results, baseline) {
612
+ if (!baseline) {
613
+ return results;
614
+ }
615
+ const baselineFingerprints = new Set(baseline.entries.map((e) => e.fingerprint));
616
+ return results.filter((result) => {
617
+ const fingerprint = createFingerprint(result);
618
+ return !baselineFingerprints.has(fingerprint);
619
+ });
620
+ }
621
+ function getBaselinedIssues(results, baseline) {
622
+ if (!baseline) {
623
+ return [];
624
+ }
625
+ const baselineFingerprints = new Set(baseline.entries.map((e) => e.fingerprint));
626
+ return results.filter((result) => {
627
+ const fingerprint = createFingerprint(result);
628
+ return baselineFingerprints.has(fingerprint);
629
+ });
630
+ }
631
+ function getBaselineStats(results, baseline) {
632
+ if (!baseline) {
633
+ return {
634
+ total: results.length,
635
+ new: results.length,
636
+ baselined: 0,
637
+ fixed: 0
638
+ };
639
+ }
640
+ const currentFingerprints = new Set(results.map((r) => createFingerprint(r)));
641
+ const baselineFingerprints = new Set(baseline.entries.map((e) => e.fingerprint));
642
+ const newIssues = results.filter((r) => !baselineFingerprints.has(createFingerprint(r)));
643
+ const baselinedIssues = results.filter((r) => baselineFingerprints.has(createFingerprint(r)));
644
+ const fixedIssues = baseline.entries.filter((e) => !currentFingerprints.has(e.fingerprint));
645
+ return {
646
+ total: results.length,
647
+ new: newIssues.length,
648
+ baselined: baselinedIssues.length,
649
+ fixed: fixedIssues.length
650
+ };
651
+ }
652
+ function updateBaseline(baseline, results) {
653
+ const now = (/* @__PURE__ */ new Date()).toISOString();
654
+ const currentFingerprints = new Set(results.map((r) => createFingerprint(r)));
655
+ const existingEntries = baseline.entries.filter((e) => currentFingerprints.has(e.fingerprint));
656
+ const existingFingerprints = new Set(existingEntries.map((e) => e.fingerprint));
657
+ const newEntries = results.filter((r) => !existingFingerprints.has(createFingerprint(r))).map((r) => ({
658
+ ruleId: r.ruleId,
659
+ file: r.file,
660
+ line: r.line,
661
+ fingerprint: createFingerprint(r),
662
+ message: r.message,
663
+ createdAt: now
664
+ }));
665
+ return {
666
+ ...baseline,
667
+ updatedAt: now,
668
+ entries: [...existingEntries, ...newEntries]
669
+ };
670
+ }
671
+
672
+ // src/engine/diff-mode.ts
673
+ import { execa } from "execa";
674
+ async function getChangedFiles(cwd, base) {
675
+ try {
676
+ const baseRef = base ?? await getDefaultBranch(cwd);
677
+ const mergeBase = await getMergeBase(cwd, baseRef);
678
+ const result = await execa("git", ["diff", "--name-status", mergeBase], {
679
+ cwd,
680
+ reject: false
681
+ });
682
+ if (result.exitCode !== 0) {
683
+ return await getUncommittedChanges(cwd);
684
+ }
685
+ return parseGitDiff(result.stdout);
686
+ } catch {
687
+ return [];
688
+ }
689
+ }
690
+ async function getUncommittedChanges(cwd) {
691
+ try {
692
+ const result = await execa("git", ["status", "--porcelain"], {
693
+ cwd,
694
+ reject: false
695
+ });
696
+ if (result.exitCode !== 0) {
697
+ return [];
698
+ }
699
+ return parseGitStatus(result.stdout);
700
+ } catch {
701
+ return [];
702
+ }
703
+ }
704
+ async function getStagedFiles(cwd) {
705
+ try {
706
+ const result = await execa("git", ["diff", "--name-status", "--cached"], {
707
+ cwd,
708
+ reject: false
709
+ });
710
+ if (result.exitCode !== 0) {
711
+ return [];
712
+ }
713
+ return parseGitDiff(result.stdout);
714
+ } catch {
715
+ return [];
716
+ }
717
+ }
718
+ async function getDefaultBranch(cwd) {
719
+ try {
720
+ const result = await execa("git", ["remote", "show", "origin"], {
721
+ cwd,
722
+ reject: false
723
+ });
724
+ const match = result.stdout.match(/HEAD branch: (.+)/);
725
+ if (match) {
726
+ return `origin/${match[1]}`;
727
+ }
728
+ } catch {
729
+ }
730
+ try {
731
+ await execa("git", ["rev-parse", "--verify", "origin/main"], { cwd });
732
+ return "origin/main";
733
+ } catch {
734
+ try {
735
+ await execa("git", ["rev-parse", "--verify", "origin/master"], { cwd });
736
+ return "origin/master";
737
+ } catch {
738
+ return "HEAD~1";
739
+ }
740
+ }
741
+ }
742
+ async function getMergeBase(cwd, base) {
743
+ try {
744
+ const result = await execa("git", ["merge-base", "HEAD", base], {
745
+ cwd,
746
+ reject: false
747
+ });
748
+ if (result.exitCode === 0 && result.stdout.trim()) {
749
+ return result.stdout.trim();
750
+ }
751
+ } catch {
752
+ }
753
+ return base;
754
+ }
755
+ function parseGitDiff(output) {
756
+ const files = [];
757
+ for (const line of output.split("\n")) {
758
+ if (!line.trim()) continue;
759
+ const [status, ...pathParts] = line.split(" ");
760
+ const path = pathParts.join(" ");
761
+ if (!path) continue;
762
+ let fileStatus;
763
+ switch (status?.[0]) {
764
+ case "A":
765
+ fileStatus = "added";
766
+ break;
767
+ case "M":
768
+ fileStatus = "modified";
769
+ break;
770
+ case "D":
771
+ fileStatus = "deleted";
772
+ break;
773
+ case "R":
774
+ fileStatus = "renamed";
775
+ break;
776
+ default:
777
+ fileStatus = "modified";
778
+ }
779
+ files.push({ path, status: fileStatus });
780
+ }
781
+ return files;
782
+ }
783
+ function parseGitStatus(output) {
784
+ const files = [];
785
+ for (const line of output.split("\n")) {
786
+ if (!line.trim()) continue;
787
+ const status = line.slice(0, 2);
788
+ const path = line.slice(3);
789
+ if (!path) continue;
790
+ let fileStatus;
791
+ if (status.includes("A") || status === "??") {
792
+ fileStatus = "added";
793
+ } else if (status.includes("D")) {
794
+ fileStatus = "deleted";
795
+ } else if (status.includes("R")) {
796
+ fileStatus = "renamed";
797
+ } else {
798
+ fileStatus = "modified";
799
+ }
800
+ files.push({ path, status: fileStatus });
801
+ }
802
+ return files;
803
+ }
804
+ function filterResultsByFiles(results, changedFiles) {
805
+ const changedPaths = new Set(changedFiles.map((f) => f.path));
806
+ return results.filter((r) => {
807
+ if (!r.file) return true;
808
+ return changedPaths.has(r.file);
809
+ });
810
+ }
811
+
812
+ // src/tools/base.ts
813
+ import { execa as execa2 } from "execa";
814
+ async function checkCommand(command) {
815
+ try {
816
+ await execa2("which", [command]);
817
+ return true;
818
+ } catch {
819
+ return false;
820
+ }
821
+ }
822
+ async function runCommand(command, args, cwd) {
823
+ try {
824
+ const result = await execa2(command, args, { cwd, reject: false });
825
+ return {
826
+ stdout: result.stdout,
827
+ stderr: result.stderr,
828
+ exitCode: result.exitCode ?? 0
829
+ };
830
+ } catch (error) {
831
+ const execaError = error;
832
+ return {
833
+ stdout: String(execaError.stdout ?? ""),
834
+ stderr: String(execaError.stderr ?? ""),
835
+ exitCode: typeof execaError.exitCode === "number" ? execaError.exitCode : 1
836
+ };
837
+ }
838
+ }
839
+ function parseSeverity(level) {
840
+ const normalized = level.toLowerCase();
841
+ if (normalized === "error" || normalized === "high" || normalized === "critical") {
842
+ return "error";
843
+ }
844
+ if (normalized === "warning" || normalized === "warn" || normalized === "medium") {
845
+ return "warn";
846
+ }
847
+ return "info";
848
+ }
849
+
850
+ // src/tools/gitleaks.ts
851
+ var gitleaksRunner = {
852
+ name: "gitleaks",
853
+ async isInstalled() {
854
+ return checkCommand("gitleaks");
855
+ },
856
+ async getVersion() {
857
+ const result = await runCommand("gitleaks", ["version"], process.cwd());
858
+ if (result.exitCode === 0) {
859
+ return result.stdout.trim();
860
+ }
861
+ return null;
862
+ },
863
+ async run(cwd, _files) {
864
+ const args = ["detect", "--source", cwd, "--report-format", "json", "--report-path", "-", "--no-git"];
865
+ const result = await runCommand("gitleaks", args, cwd);
866
+ if (result.exitCode === 0) {
867
+ return [];
868
+ }
869
+ try {
870
+ const findings = JSON.parse(result.stdout || "[]");
871
+ return findings.map((finding) => ({
872
+ ruleId: `secrets.${finding.RuleID}`,
873
+ severity: "error",
874
+ message: `Secret detected: ${finding.Description}`,
875
+ file: finding.File,
876
+ line: finding.StartLine,
877
+ column: finding.StartColumn,
878
+ endLine: finding.EndLine,
879
+ endColumn: finding.EndColumn
880
+ }));
881
+ } catch {
882
+ if (result.stdout.includes("no leaks found")) {
883
+ return [];
884
+ }
885
+ return [
886
+ {
887
+ ruleId: "secrets.no-plaintext",
888
+ severity: "error",
889
+ message: `Gitleaks error: ${result.stderr || "Unknown error"}`
890
+ }
891
+ ];
892
+ }
893
+ }
894
+ };
895
+
896
+ // src/tools/semgrep.ts
897
+ var semgrepRunner = {
898
+ name: "semgrep",
899
+ async isInstalled() {
900
+ return checkCommand("semgrep");
901
+ },
902
+ async getVersion() {
903
+ const result = await runCommand("semgrep", ["--version"], process.cwd());
904
+ if (result.exitCode === 0) {
905
+ return result.stdout.trim();
906
+ }
907
+ return null;
908
+ },
909
+ async run(cwd, files) {
910
+ const args = [
911
+ "scan",
912
+ "--json",
913
+ "--config",
914
+ "auto",
915
+ "--metrics",
916
+ "off"
917
+ ];
918
+ if (files && files.length > 0) {
919
+ args.push(...files);
920
+ } else {
921
+ args.push(cwd);
922
+ }
923
+ const result = await runCommand("semgrep", args, cwd);
924
+ try {
925
+ const output = JSON.parse(result.stdout);
926
+ const results = [];
927
+ for (const finding of output.results) {
928
+ results.push({
929
+ ruleId: finding.check_id,
930
+ severity: parseSeverity(finding.extra.severity),
931
+ message: finding.extra.message,
932
+ file: finding.path,
933
+ line: finding.start.line,
934
+ column: finding.start.col,
935
+ endLine: finding.end.line,
936
+ endColumn: finding.end.col,
937
+ fix: finding.extra.fix ? { description: "Apply suggested fix", replacement: finding.extra.fix } : void 0
938
+ });
939
+ }
940
+ return results;
941
+ } catch {
942
+ if (result.exitCode === 0) {
943
+ return [];
944
+ }
945
+ return [
946
+ {
947
+ ruleId: "semgrep.error",
948
+ severity: "error",
949
+ message: `Semgrep error: ${result.stderr || "Unknown error"}`
950
+ }
951
+ ];
952
+ }
953
+ }
954
+ };
955
+
956
+ // src/tools/osv-scanner.ts
957
+ var osvScannerRunner = {
958
+ name: "osv-scanner",
959
+ async isInstalled() {
960
+ return checkCommand("osv-scanner");
961
+ },
962
+ async getVersion() {
963
+ const result = await runCommand("osv-scanner", ["--version"], process.cwd());
964
+ if (result.exitCode === 0) {
965
+ return result.stdout.trim();
966
+ }
967
+ return null;
968
+ },
969
+ async run(cwd, _files) {
970
+ const args = ["--format", "json", "--recursive", cwd];
971
+ const result = await runCommand("osv-scanner", args, cwd);
972
+ if (result.exitCode === 0 && !result.stdout.trim()) {
973
+ return [];
974
+ }
975
+ try {
976
+ const output = JSON.parse(result.stdout);
977
+ const results = [];
978
+ for (const source of output.results || []) {
979
+ for (const pkg of source.packages || []) {
980
+ for (const vuln of pkg.vulnerabilities || []) {
981
+ const severity = vuln.severity?.[0]?.score;
982
+ let severityLevel = "warn";
983
+ if (severity) {
984
+ const score = parseFloat(severity);
985
+ if (score >= 7) severityLevel = "error";
986
+ else if (score >= 4) severityLevel = "warn";
987
+ else severityLevel = "info";
988
+ }
989
+ results.push({
990
+ ruleId: `deps.vulnerability.${vuln.id}`,
991
+ severity: severityLevel,
992
+ message: `${pkg.package.name}@${pkg.package.version}: ${vuln.summary || vuln.id}`,
993
+ file: source.source.path
994
+ });
995
+ }
996
+ }
997
+ }
998
+ return results;
999
+ } catch {
1000
+ if (result.exitCode === 0) {
1001
+ return [];
1002
+ }
1003
+ return [
1004
+ {
1005
+ ruleId: "deps.no-vulnerable",
1006
+ severity: "error",
1007
+ message: `OSV Scanner error: ${result.stderr || "Unknown error"}`
1008
+ }
1009
+ ];
1010
+ }
1011
+ }
1012
+ };
1013
+
1014
+ // src/tools/builtin.ts
1015
+ import { readFile as readFile3, access as access2, readdir as readdir2 } from "fs/promises";
1016
+ import { join as join3 } from "path";
1017
+ async function checkGitignore(cwd) {
1018
+ const results = [];
1019
+ const gitignorePath = join3(cwd, ".gitignore");
1020
+ try {
1021
+ await access2(gitignorePath);
1022
+ const content = await readFile3(gitignorePath, "utf-8");
1023
+ const requiredPatterns = [".env", "node_modules", ".env.local", ".env*.local"];
1024
+ const missingPatterns = requiredPatterns.filter(
1025
+ (pattern) => !content.includes(pattern)
1026
+ );
1027
+ if (missingPatterns.length > 0) {
1028
+ results.push({
1029
+ ruleId: "secrets.gitignore-required",
1030
+ severity: "warn",
1031
+ message: `.gitignore is missing patterns: ${missingPatterns.join(", ")}`,
1032
+ file: ".gitignore",
1033
+ fix: {
1034
+ description: `Add missing patterns to .gitignore`,
1035
+ replacement: missingPatterns.join("\n")
1036
+ }
1037
+ });
1038
+ }
1039
+ } catch {
1040
+ results.push({
1041
+ ruleId: "secrets.gitignore-required",
1042
+ severity: "warn",
1043
+ message: ".gitignore file not found",
1044
+ fix: {
1045
+ description: "Create .gitignore with common patterns"
1046
+ }
1047
+ });
1048
+ }
1049
+ return results;
1050
+ }
1051
+ async function checkEnvFiles(cwd) {
1052
+ const results = [];
1053
+ try {
1054
+ const files = await readdir2(cwd);
1055
+ const envFiles = files.filter(
1056
+ (f) => f.startsWith(".env") && !f.endsWith(".example") && !f.endsWith(".template")
1057
+ );
1058
+ const gitignorePath = join3(cwd, ".gitignore");
1059
+ let gitignoreContent = "";
1060
+ try {
1061
+ gitignoreContent = await readFile3(gitignorePath, "utf-8");
1062
+ } catch {
1063
+ }
1064
+ for (const envFile of envFiles) {
1065
+ const isIgnored = gitignoreContent.includes(envFile) || gitignoreContent.includes(".env");
1066
+ if (!isIgnored) {
1067
+ results.push({
1068
+ ruleId: "secrets.no-env-commit",
1069
+ severity: "error",
1070
+ message: `${envFile} may be committed to git. Add it to .gitignore.`,
1071
+ file: envFile,
1072
+ fix: {
1073
+ description: `Add ${envFile} to .gitignore`
1074
+ }
1075
+ });
1076
+ }
1077
+ }
1078
+ } catch {
1079
+ }
1080
+ return results;
1081
+ }
1082
+ async function checkLockfile(cwd) {
1083
+ const results = [];
1084
+ const lockfiles = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb"];
1085
+ try {
1086
+ const files = await readdir2(cwd);
1087
+ const hasLockfile = lockfiles.some((lf) => files.includes(lf));
1088
+ const hasPackageJson = files.includes("package.json");
1089
+ if (hasPackageJson && !hasLockfile) {
1090
+ results.push({
1091
+ ruleId: "deps.lockfile.required",
1092
+ severity: "error",
1093
+ message: "No lockfile found. Run npm install, yarn, or pnpm install.",
1094
+ fix: {
1095
+ description: "Generate lockfile by running package manager install"
1096
+ }
1097
+ });
1098
+ }
1099
+ } catch {
1100
+ }
1101
+ return results;
1102
+ }
1103
+ async function checkUnpinnedDeps(cwd) {
1104
+ const results = [];
1105
+ const packageJsonPath = join3(cwd, "package.json");
1106
+ try {
1107
+ const content = await readFile3(packageJsonPath, "utf-8");
1108
+ const pkg = JSON.parse(content);
1109
+ const checkDeps = (deps, type) => {
1110
+ if (!deps) return;
1111
+ for (const [name, version] of Object.entries(deps)) {
1112
+ if (version.startsWith("^") || version.startsWith("~") || version === "*" || version === "latest") {
1113
+ results.push({
1114
+ ruleId: "deps.no-unpinned",
1115
+ severity: "warn",
1116
+ message: `${type} "${name}" has unpinned version: ${version}`,
1117
+ file: "package.json"
1118
+ });
1119
+ }
1120
+ }
1121
+ };
1122
+ checkDeps(pkg.dependencies, "Dependency");
1123
+ checkDeps(pkg.devDependencies, "DevDependency");
1124
+ } catch {
1125
+ }
1126
+ return results;
1127
+ }
1128
+ async function checkGitDeps(cwd) {
1129
+ const results = [];
1130
+ const packageJsonPath = join3(cwd, "package.json");
1131
+ try {
1132
+ const content = await readFile3(packageJsonPath, "utf-8");
1133
+ const pkg = JSON.parse(content);
1134
+ const checkDeps = (deps) => {
1135
+ if (!deps) return;
1136
+ for (const [name, version] of Object.entries(deps)) {
1137
+ if (version.startsWith("git://") || version.startsWith("git+") || version.startsWith("github:") || version.includes("github.com")) {
1138
+ results.push({
1139
+ ruleId: "deps.no-git-deps",
1140
+ severity: "warn",
1141
+ message: `Dependency "${name}" uses git URL: ${version}`,
1142
+ file: "package.json"
1143
+ });
1144
+ }
1145
+ }
1146
+ };
1147
+ checkDeps(pkg.dependencies);
1148
+ checkDeps(pkg.devDependencies);
1149
+ } catch {
1150
+ }
1151
+ return results;
1152
+ }
1153
+ async function checkTestsExist(cwd) {
1154
+ const results = [];
1155
+ try {
1156
+ const testPatterns = ["test", "tests", "__tests__", "spec", "specs"];
1157
+ const files = await readdir2(cwd);
1158
+ const hasTestDir = testPatterns.some((p) => files.includes(p));
1159
+ const hasTestFiles = files.some(
1160
+ (f) => f.endsWith(".test.ts") || f.endsWith(".test.js") || f.endsWith(".spec.ts") || f.endsWith(".spec.js")
1161
+ );
1162
+ if (!hasTestDir && !hasTestFiles) {
1163
+ results.push({
1164
+ ruleId: "tests.unit.required",
1165
+ severity: "warn",
1166
+ message: "No test files or test directory found"
1167
+ });
1168
+ }
1169
+ } catch {
1170
+ }
1171
+ return results;
1172
+ }
1173
+ async function checkTsConfig(cwd) {
1174
+ const results = [];
1175
+ const tsconfigPath = join3(cwd, "tsconfig.json");
1176
+ try {
1177
+ const content = await readFile3(tsconfigPath, "utf-8");
1178
+ const tsconfig = JSON.parse(content);
1179
+ if (!tsconfig.compilerOptions?.strict) {
1180
+ results.push({
1181
+ ruleId: "code.strict-mode",
1182
+ severity: "warn",
1183
+ message: "TypeScript strict mode is not enabled",
1184
+ file: "tsconfig.json",
1185
+ fix: {
1186
+ description: 'Add "strict": true to compilerOptions'
1187
+ }
1188
+ });
1189
+ }
1190
+ } catch {
1191
+ }
1192
+ return results;
1193
+ }
1194
+ async function checkNodeVersion(cwd) {
1195
+ const results = [];
1196
+ try {
1197
+ const files = await readdir2(cwd);
1198
+ const hasNvmrc = files.includes(".nvmrc");
1199
+ const hasNodeVersion = files.includes(".node-version");
1200
+ let hasEngines = false;
1201
+ try {
1202
+ const pkgContent = await readFile3(join3(cwd, "package.json"), "utf-8");
1203
+ const pkg = JSON.parse(pkgContent);
1204
+ hasEngines = !!pkg.engines?.node;
1205
+ } catch {
1206
+ }
1207
+ if (!hasNvmrc && !hasNodeVersion && !hasEngines) {
1208
+ results.push({
1209
+ ruleId: "config.node-version",
1210
+ severity: "info",
1211
+ message: "Node.js version not specified (.nvmrc, .node-version, or engines.node)",
1212
+ fix: {
1213
+ description: "Create .nvmrc or add engines.node to package.json"
1214
+ }
1215
+ });
1216
+ }
1217
+ } catch {
1218
+ }
1219
+ return results;
1220
+ }
1221
+ async function checkEditorConfig(cwd) {
1222
+ const results = [];
1223
+ try {
1224
+ await access2(join3(cwd, ".editorconfig"));
1225
+ } catch {
1226
+ results.push({
1227
+ ruleId: "config.editor-config",
1228
+ severity: "info",
1229
+ message: ".editorconfig not found",
1230
+ fix: {
1231
+ description: "Create .editorconfig for consistent formatting"
1232
+ }
1233
+ });
1234
+ }
1235
+ return results;
1236
+ }
1237
+ async function runBuiltinChecks(cwd, ruleIds) {
1238
+ const results = [];
1239
+ const checks = {
1240
+ "secrets.gitignore-required": () => checkGitignore(cwd),
1241
+ "secrets.no-env-commit": () => checkEnvFiles(cwd),
1242
+ "deps.lockfile.required": () => checkLockfile(cwd),
1243
+ "deps.no-unpinned": () => checkUnpinnedDeps(cwd),
1244
+ "deps.no-git-deps": () => checkGitDeps(cwd),
1245
+ "tests.unit.required": () => checkTestsExist(cwd),
1246
+ "code.strict-mode": () => checkTsConfig(cwd),
1247
+ "config.node-version": () => checkNodeVersion(cwd),
1248
+ "config.editor-config": () => checkEditorConfig(cwd)
1249
+ };
1250
+ for (const ruleId of ruleIds) {
1251
+ const check = checks[ruleId];
1252
+ if (check) {
1253
+ const checkResults = await check();
1254
+ results.push(...checkResults);
1255
+ }
1256
+ }
1257
+ return results;
1258
+ }
1259
+
1260
+ export {
1261
+ RuleSeveritySchema,
1262
+ RuleScopeSchema,
1263
+ LevelSchema,
1264
+ PresetSchema,
1265
+ RuleConfigSchema,
1266
+ VibeGuardConfigSchema,
1267
+ presets,
1268
+ getPreset,
1269
+ listPresets,
1270
+ loadConfig,
1271
+ loadConfigFromFile,
1272
+ getDefaultConfig,
1273
+ resolveRules,
1274
+ groupRulesByTool,
1275
+ calculateScore,
1276
+ shouldFail,
1277
+ detectStack,
1278
+ createFingerprint,
1279
+ loadBaseline,
1280
+ saveBaseline,
1281
+ createBaseline,
1282
+ filterNewIssues,
1283
+ getBaselinedIssues,
1284
+ getBaselineStats,
1285
+ updateBaseline,
1286
+ getChangedFiles,
1287
+ getUncommittedChanges,
1288
+ getStagedFiles,
1289
+ filterResultsByFiles,
1290
+ checkCommand,
1291
+ runCommand,
1292
+ parseSeverity,
1293
+ gitleaksRunner,
1294
+ semgrepRunner,
1295
+ osvScannerRunner,
1296
+ runBuiltinChecks
1297
+ };
1298
+ //# sourceMappingURL=chunk-ZDEEHXE7.js.map