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,1346 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ calculateScore,
4
+ createBaseline,
5
+ detectStack,
6
+ filterNewIssues,
7
+ filterResultsByFiles,
8
+ getBaselineStats,
9
+ getChangedFiles,
10
+ getDefaultConfig,
11
+ getPreset,
12
+ gitleaksRunner,
13
+ groupRulesByTool,
14
+ loadBaseline,
15
+ loadConfig,
16
+ osvScannerRunner,
17
+ resolveRules,
18
+ runBuiltinChecks,
19
+ saveBaseline,
20
+ semgrepRunner,
21
+ shouldFail,
22
+ updateBaseline
23
+ } from "../chunk-ZDEEHXE7.js";
24
+ import {
25
+ getRuleById
26
+ } from "../chunk-J22FFU7Z.js";
27
+
28
+ // src/cli/index.ts
29
+ import { Command } from "commander";
30
+ import chalk9 from "chalk";
31
+
32
+ // src/cli/commands/init.ts
33
+ import { writeFile, mkdir } from "fs/promises";
34
+ import { join } from "path";
35
+ import chalk from "chalk";
36
+ import ora from "ora";
37
+ async function initCommand(options) {
38
+ const cwd = process.cwd();
39
+ console.log(chalk.bold.cyan("\n\u{1F6E4}\uFE0F Devrail - Security & Quality Guardrails\n"));
40
+ const spinner = ora("Detecting your stack...").start();
41
+ try {
42
+ const detected = await detectStack(cwd);
43
+ const preset = options.preset ?? detected.preset;
44
+ const level = options.level;
45
+ spinner.succeed(chalk.green("Stack detected!"));
46
+ console.log("\n" + chalk.bold("\u{1F4E6} Project Analysis:"));
47
+ console.log(` Language: ${chalk.cyan(detected.language)}`);
48
+ if (detected.framework) {
49
+ console.log(` Framework: ${chalk.cyan(detected.framework)}`);
50
+ }
51
+ console.log(` Preset: ${chalk.cyan(preset)} ${detected.confidence < 80 ? chalk.dim("(suggested)") : ""}`);
52
+ console.log(` TypeScript: ${detected.hasTypeScript ? chalk.green("\u2713") : chalk.dim("\u2717")}`);
53
+ console.log(` Tests: ${detected.hasTests ? chalk.green("\u2713") : chalk.yellow("\u2717 missing")}`);
54
+ console.log(` CI/CD: ${detected.hasCi ? chalk.green("\u2713") : chalk.yellow("\u2717 missing")}`);
55
+ spinner.start("Running quick security scan...");
56
+ const quickResults = await runBuiltinChecks(cwd, [
57
+ "secrets.gitignore-required",
58
+ "secrets.no-env-commit",
59
+ "deps.lockfile.required",
60
+ "deps.no-unpinned",
61
+ "tests.unit.required",
62
+ "code.strict-mode",
63
+ "config.node-version"
64
+ ]);
65
+ spinner.stop();
66
+ const errors = quickResults.filter((r) => r.severity === "error");
67
+ const warnings = quickResults.filter((r) => r.severity === "warn");
68
+ if (quickResults.length > 0 || detected.issues.length > 0) {
69
+ console.log("\n" + chalk.bold.yellow("\u26A0\uFE0F Issues Found:"));
70
+ for (const issue of detected.issues) {
71
+ console.log(` ${chalk.red("\u25CF")} ${issue}`);
72
+ }
73
+ for (const result of errors) {
74
+ console.log(` ${chalk.red("\u25CF")} ${result.message}`);
75
+ }
76
+ for (const result of warnings.slice(0, 5)) {
77
+ console.log(` ${chalk.yellow("\u25CF")} ${result.message}`);
78
+ }
79
+ if (warnings.length > 5) {
80
+ console.log(chalk.dim(` ... and ${warnings.length - 5} more warnings`));
81
+ }
82
+ }
83
+ spinner.start("Creating configuration...");
84
+ await createConfigFile(cwd, preset, level);
85
+ if (options.hooks) {
86
+ spinner.text = "Setting up git hooks...";
87
+ await setupGitHooks(cwd);
88
+ }
89
+ if (options.ci && !detected.hasCi) {
90
+ spinner.text = "Creating CI templates...";
91
+ await createCITemplates(cwd, preset, level);
92
+ }
93
+ await updateGitignore(cwd);
94
+ spinner.succeed(chalk.green("Devrail initialized!"));
95
+ const presetConfig = getPreset(preset);
96
+ console.log("\n" + chalk.bold("\u{1F680} Next Actions:"));
97
+ if (errors.length > 0 || warnings.length > 0) {
98
+ console.log(` 1. ${chalk.cyan("npx devrail fix")} Fix ${errors.length + Math.min(warnings.length, 5)} auto-fixable issues`);
99
+ console.log(` 2. ${chalk.cyan("npx devrail baseline")} Accept existing issues, block new ones`);
100
+ console.log(` 3. ${chalk.cyan("npx devrail ci")} Run full CI check`);
101
+ } else {
102
+ console.log(` 1. ${chalk.cyan("npx devrail check")} Run security scan`);
103
+ console.log(` 2. ${chalk.cyan("npx devrail ci")} Run full CI check`);
104
+ }
105
+ console.log("\n" + chalk.dim(`Config: devrail.config.yaml | ${presetConfig.rules[level]?.length ?? 0} rules active
106
+ `));
107
+ } catch (error) {
108
+ spinner.fail(chalk.red("Initialization failed"));
109
+ console.error(error);
110
+ process.exit(1);
111
+ }
112
+ }
113
+ async function createConfigFile(cwd, preset, level) {
114
+ const config = `# Devrail Configuration
115
+ # Docs: https://devrail.dev/docs/config
116
+
117
+ devrail:
118
+ preset: "${preset}"
119
+ level: "${level}"
120
+
121
+ # Baseline: accept existing issues, block new ones
122
+ baseline: ".devrail/baseline.json"
123
+
124
+ # Override specific rules (optional)
125
+ # rules:
126
+ # secrets.no-plaintext:
127
+ # enabled: true
128
+ # severity: error
129
+ # deps.no-unpinned:
130
+ # enabled: false
131
+
132
+ # CI configuration
133
+ ci:
134
+ failOn: error
135
+ # Only fail on NEW issues (not baselined)
136
+ newOnly: true
137
+ reportFormat: console
138
+
139
+ # Tool toggles
140
+ tools:
141
+ eslint: true
142
+ semgrep: true
143
+ gitleaks: true
144
+ osvScanner: true
145
+ `;
146
+ await writeFile(join(cwd, "devrail.config.yaml"), config, "utf-8");
147
+ }
148
+ async function setupGitHooks(cwd) {
149
+ const hooksDir = join(cwd, ".husky");
150
+ try {
151
+ await mkdir(hooksDir, { recursive: true });
152
+ const preCommitHook = `#!/usr/bin/env sh
153
+ . "$(dirname -- "$0")/_/husky.sh"
154
+
155
+ npx devrail check --changed
156
+ `;
157
+ await writeFile(join(hooksDir, "pre-commit"), preCommitHook, { mode: 493 });
158
+ const prePushHook = `#!/usr/bin/env sh
159
+ . "$(dirname -- "$0")/_/husky.sh"
160
+
161
+ npx devrail ci
162
+ `;
163
+ await writeFile(join(hooksDir, "pre-push"), prePushHook, { mode: 493 });
164
+ } catch {
165
+ const gitHooksDir = join(cwd, ".git", "hooks");
166
+ try {
167
+ const preCommitHook = `#!/usr/bin/env sh
168
+ npx devrail check --changed
169
+ `;
170
+ await writeFile(join(gitHooksDir, "pre-commit"), preCommitHook, { mode: 493 });
171
+ } catch {
172
+ }
173
+ }
174
+ }
175
+ async function createCITemplates(cwd, preset, level) {
176
+ const githubDir = join(cwd, ".github", "workflows");
177
+ await mkdir(githubDir, { recursive: true });
178
+ const workflow = `name: VibeGuard Security & Quality
179
+
180
+ on:
181
+ push:
182
+ branches: [main, master]
183
+ pull_request:
184
+ branches: [main, master]
185
+
186
+ jobs:
187
+ vibeguard:
188
+ runs-on: ubuntu-latest
189
+ steps:
190
+ - uses: actions/checkout@v4
191
+
192
+ - name: Setup Node.js
193
+ uses: actions/setup-node@v4
194
+ with:
195
+ node-version: '20'
196
+ cache: 'npm'
197
+
198
+ - name: Install dependencies
199
+ run: npm ci
200
+
201
+ - name: Install security tools
202
+ run: |
203
+ # Install gitleaks
204
+ curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v8.18.0/gitleaks_8.18.0_linux_x64.tar.gz | tar -xz
205
+ sudo mv gitleaks /usr/local/bin/
206
+
207
+ # Install osv-scanner
208
+ curl -sSfL https://github.com/google/osv-scanner/releases/download/v1.7.0/osv-scanner_linux_amd64 -o osv-scanner
209
+ chmod +x osv-scanner
210
+ sudo mv osv-scanner /usr/local/bin/
211
+
212
+ # Install semgrep
213
+ pip install semgrep
214
+
215
+ - name: Run VibeGuard CI
216
+ run: npx vg ci --format sarif > vibeguard-results.sarif
217
+ continue-on-error: true
218
+
219
+ - name: Upload SARIF results
220
+ uses: github/codeql-action/upload-sarif@v3
221
+ with:
222
+ sarif_file: vibeguard-results.sarif
223
+ if: always()
224
+
225
+ - name: Run VibeGuard CI (blocking)
226
+ run: npx vg ci --fail-on error
227
+ `;
228
+ await writeFile(join(githubDir, "vibeguard.yml"), workflow, "utf-8");
229
+ }
230
+ async function updateGitignore(cwd) {
231
+ const gitignorePath = join(cwd, ".gitignore");
232
+ const entries = `
233
+ # VibeGuard
234
+ .vibeguard-cache/
235
+ vibeguard-report.json
236
+ vibeguard-report.sarif
237
+ `;
238
+ try {
239
+ const { readFile: readFile3 } = await import("fs/promises");
240
+ const existing = await readFile3(gitignorePath, "utf-8");
241
+ if (!existing.includes(".vibeguard-cache")) {
242
+ await writeFile(gitignorePath, existing + entries, "utf-8");
243
+ }
244
+ } catch {
245
+ await writeFile(gitignorePath, entries.trim() + "\n", "utf-8");
246
+ }
247
+ }
248
+
249
+ // src/cli/commands/check.ts
250
+ import chalk2 from "chalk";
251
+ import ora2 from "ora";
252
+ async function checkCommand(options) {
253
+ const cwd = process.cwd();
254
+ const startTime = Date.now();
255
+ const spinner = options.json ? null : ora2("Loading configuration...").start();
256
+ try {
257
+ const loaded = await loadConfig(cwd);
258
+ const config = loaded?.config ?? getDefaultConfig();
259
+ if (spinner) spinner.text = "Resolving rules...";
260
+ const resolvedRules = resolveRules(config);
261
+ const rulesByTool = groupRulesByTool(resolvedRules);
262
+ const allResults = [];
263
+ const toolStats = [];
264
+ if (spinner) spinner.text = "Running builtin checks...";
265
+ const builtinRules = rulesByTool.get("builtin") ?? [];
266
+ if (builtinRules.length > 0) {
267
+ const builtinStart = Date.now();
268
+ const builtinResults = await runBuiltinChecks(
269
+ cwd,
270
+ builtinRules.map((r) => r.definition.id)
271
+ );
272
+ allResults.push(...builtinResults);
273
+ toolStats.push({
274
+ name: "builtin",
275
+ duration: Date.now() - builtinStart,
276
+ results: builtinResults.length
277
+ });
278
+ }
279
+ if (config.tools?.gitleaks !== false) {
280
+ if (spinner) spinner.text = "Running gitleaks (secret scanning)...";
281
+ const isInstalled = await gitleaksRunner.isInstalled();
282
+ if (isInstalled) {
283
+ const toolStart = Date.now();
284
+ const results = await gitleaksRunner.run(cwd);
285
+ allResults.push(...results);
286
+ toolStats.push({
287
+ name: "gitleaks",
288
+ version: await gitleaksRunner.getVersion() ?? void 0,
289
+ duration: Date.now() - toolStart,
290
+ results: results.length
291
+ });
292
+ } else if (spinner) {
293
+ spinner.warn(chalk2.yellow("gitleaks not installed, skipping secret scanning"));
294
+ spinner.start();
295
+ }
296
+ }
297
+ if (config.tools?.osvScanner !== false) {
298
+ if (spinner) spinner.text = "Running osv-scanner (dependency vulnerabilities)...";
299
+ const isInstalled = await osvScannerRunner.isInstalled();
300
+ if (isInstalled) {
301
+ const toolStart = Date.now();
302
+ const results = await osvScannerRunner.run(cwd);
303
+ allResults.push(...results);
304
+ toolStats.push({
305
+ name: "osv-scanner",
306
+ version: await osvScannerRunner.getVersion() ?? void 0,
307
+ duration: Date.now() - toolStart,
308
+ results: results.length
309
+ });
310
+ } else if (spinner) {
311
+ spinner.warn(chalk2.yellow("osv-scanner not installed, skipping dependency scanning"));
312
+ spinner.start();
313
+ }
314
+ }
315
+ if (config.tools?.semgrep !== false) {
316
+ if (spinner) spinner.text = "Running semgrep (SAST)...";
317
+ const isInstalled = await semgrepRunner.isInstalled();
318
+ if (isInstalled) {
319
+ const toolStart = Date.now();
320
+ const results = await semgrepRunner.run(cwd);
321
+ allResults.push(...results);
322
+ toolStats.push({
323
+ name: "semgrep",
324
+ version: await semgrepRunner.getVersion() ?? void 0,
325
+ duration: Date.now() - toolStart,
326
+ results: results.length
327
+ });
328
+ } else if (spinner) {
329
+ spinner.warn(chalk2.yellow("semgrep not installed, skipping SAST"));
330
+ spinner.start();
331
+ }
332
+ }
333
+ const duration = Date.now() - startTime;
334
+ const score = calculateScore(allResults);
335
+ const report = {
336
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
337
+ duration,
338
+ preset: config.preset,
339
+ level: config.level ?? "standard",
340
+ summary: {
341
+ total: allResults.length,
342
+ errors: allResults.filter((r) => r.severity === "error").length,
343
+ warnings: allResults.filter((r) => r.severity === "warn").length,
344
+ infos: allResults.filter((r) => r.severity === "info").length,
345
+ fixed: 0
346
+ },
347
+ score,
348
+ results: allResults,
349
+ tools: toolStats
350
+ };
351
+ if (options.json) {
352
+ console.log(JSON.stringify(report, null, 2));
353
+ return;
354
+ }
355
+ if (spinner) spinner.stop();
356
+ printReport(report);
357
+ if (report.summary.errors > 0) {
358
+ process.exit(1);
359
+ }
360
+ } catch (error) {
361
+ if (spinner) spinner.fail(chalk2.red("Check failed"));
362
+ console.error(error);
363
+ process.exit(1);
364
+ }
365
+ }
366
+ function printReport(report) {
367
+ console.log("\n" + chalk2.bold("Devrail Check Results"));
368
+ console.log(chalk2.dim("\u2500".repeat(50)));
369
+ const scoreColor = report.score >= 80 ? chalk2.green : report.score >= 50 ? chalk2.yellow : chalk2.red;
370
+ console.log(`
371
+ Score: ${scoreColor.bold(report.score + "/100")}`);
372
+ console.log(`
373
+ Summary:`);
374
+ console.log(` ${chalk2.red("\u25CF")} Errors: ${report.summary.errors}`);
375
+ console.log(` ${chalk2.yellow("\u25CF")} Warnings: ${report.summary.warnings}`);
376
+ console.log(` ${chalk2.blue("\u25CF")} Info: ${report.summary.infos}`);
377
+ if (report.results.length > 0) {
378
+ console.log("\n" + chalk2.bold("Issues:"));
379
+ const grouped = /* @__PURE__ */ new Map();
380
+ for (const result of report.results) {
381
+ const key = result.file ?? "General";
382
+ const existing = grouped.get(key) ?? [];
383
+ existing.push(result);
384
+ grouped.set(key, existing);
385
+ }
386
+ for (const [file, results] of grouped) {
387
+ console.log(`
388
+ ${chalk2.cyan(file)}`);
389
+ for (const result of results) {
390
+ const severityIcon = result.severity === "error" ? chalk2.red("\u2716") : result.severity === "warn" ? chalk2.yellow("\u26A0") : chalk2.blue("\u2139");
391
+ const location = result.line ? `:${result.line}` : "";
392
+ console.log(` ${severityIcon} ${result.message}`);
393
+ console.log(` ${chalk2.dim(result.ruleId)}${chalk2.dim(location)}`);
394
+ }
395
+ }
396
+ } else {
397
+ console.log("\n" + chalk2.green("\u2713 All clear! No issues found."));
398
+ }
399
+ console.log("\n" + chalk2.dim(`Duration: ${report.duration}ms`));
400
+ console.log(chalk2.dim(`Tools: ${report.tools.map((t) => t.name).join(", ")}`));
401
+ console.log();
402
+ }
403
+
404
+ // src/cli/commands/explain.ts
405
+ import chalk3 from "chalk";
406
+ async function explainCommand(ruleId) {
407
+ const rule = getRuleById(ruleId);
408
+ if (!rule) {
409
+ console.error(chalk3.red(`Rule not found: ${ruleId}`));
410
+ console.log(chalk3.dim("\nRun `vg rules` to see all available rules."));
411
+ process.exit(1);
412
+ }
413
+ console.log("\n" + chalk3.bold.cyan(`Rule: ${rule.id}`));
414
+ console.log(chalk3.dim("\u2500".repeat(50)));
415
+ console.log(`
416
+ ${chalk3.bold("Name:")} ${rule.name}`);
417
+ console.log(`${chalk3.bold("Category:")} ${rule.category}`);
418
+ const severityColor = rule.severity === "error" ? chalk3.red : rule.severity === "warn" ? chalk3.yellow : chalk3.blue;
419
+ console.log(`${chalk3.bold("Severity:")} ${severityColor(rule.severity)}`);
420
+ console.log(`${chalk3.bold("Blocking:")} ${rule.blocking ? chalk3.red("Yes") : chalk3.green("No")}`);
421
+ console.log(`${chalk3.bold("Autofix:")} ${rule.autofix ? chalk3.green("Yes") : chalk3.dim("No")}`);
422
+ console.log(`${chalk3.bold("Tool:")} ${rule.tool}`);
423
+ console.log(`
424
+ ${chalk3.bold("Description:")}`);
425
+ console.log(` ${rule.description}`);
426
+ console.log(`
427
+ ${chalk3.bold("Why this matters:")}`);
428
+ console.log(` ${rule.rationale}`);
429
+ console.log(`
430
+ ${chalk3.bold("How to fix:")}`);
431
+ console.log(` ${rule.fix}`);
432
+ if (rule.examples) {
433
+ console.log(`
434
+ ${chalk3.bold("Examples:")}`);
435
+ if (rule.examples.bad) {
436
+ console.log(`
437
+ ${chalk3.red("\u2716 Bad:")}`);
438
+ console.log(chalk3.dim(" ```"));
439
+ console.log(` ${rule.examples.bad.split("\n").join("\n ")}`);
440
+ console.log(chalk3.dim(" ```"));
441
+ }
442
+ if (rule.examples.good) {
443
+ console.log(`
444
+ ${chalk3.green("\u2713 Good:")}`);
445
+ console.log(chalk3.dim(" ```"));
446
+ console.log(` ${rule.examples.good.split("\n").join("\n ")}`);
447
+ console.log(chalk3.dim(" ```"));
448
+ }
449
+ }
450
+ console.log(`
451
+ ${chalk3.bold("Active in levels:")}`);
452
+ const levels = [];
453
+ if (rule.levels.basic) levels.push(chalk3.green("basic"));
454
+ if (rule.levels.standard) levels.push(chalk3.yellow("standard"));
455
+ if (rule.levels.strict) levels.push(chalk3.red("strict"));
456
+ console.log(` ${levels.join(", ")}`);
457
+ console.log(`
458
+ ${chalk3.dim(`Docs: ${rule.docs}`)}
459
+ `);
460
+ }
461
+
462
+ // src/cli/commands/ci.ts
463
+ import chalk4 from "chalk";
464
+ async function ciCommand(options) {
465
+ const cwd = process.cwd();
466
+ const startTime = Date.now();
467
+ const failOn = options.failOn ?? "error";
468
+ const format = options.format ?? "console";
469
+ const useBaseline = options.newOnly ?? true;
470
+ const useDiffOnly = options.diffOnly ?? false;
471
+ try {
472
+ const loaded = await loadConfig(cwd);
473
+ const config = loaded?.config ?? getDefaultConfig();
474
+ const baseline = useBaseline ? await loadBaseline(cwd) : null;
475
+ const changedFiles = useDiffOnly ? await getChangedFiles(cwd) : [];
476
+ const resolvedRules = resolveRules(config);
477
+ const rulesByTool = groupRulesByTool(resolvedRules);
478
+ const allResults = [];
479
+ const toolStats = [];
480
+ const builtinRules = rulesByTool.get("builtin") ?? [];
481
+ if (builtinRules.length > 0) {
482
+ const builtinStart = Date.now();
483
+ const builtinResults = await runBuiltinChecks(
484
+ cwd,
485
+ builtinRules.map((r) => r.definition.id)
486
+ );
487
+ allResults.push(...builtinResults);
488
+ toolStats.push({
489
+ name: "builtin",
490
+ duration: Date.now() - builtinStart,
491
+ results: builtinResults.length
492
+ });
493
+ }
494
+ if (config.tools?.gitleaks !== false) {
495
+ const isInstalled = await gitleaksRunner.isInstalled();
496
+ if (isInstalled) {
497
+ const toolStart = Date.now();
498
+ const results = await gitleaksRunner.run(cwd);
499
+ allResults.push(...results);
500
+ toolStats.push({
501
+ name: "gitleaks",
502
+ version: await gitleaksRunner.getVersion() ?? void 0,
503
+ duration: Date.now() - toolStart,
504
+ results: results.length
505
+ });
506
+ }
507
+ }
508
+ if (config.tools?.osvScanner !== false) {
509
+ const isInstalled = await osvScannerRunner.isInstalled();
510
+ if (isInstalled) {
511
+ const toolStart = Date.now();
512
+ const results = await osvScannerRunner.run(cwd);
513
+ allResults.push(...results);
514
+ toolStats.push({
515
+ name: "osv-scanner",
516
+ version: await osvScannerRunner.getVersion() ?? void 0,
517
+ duration: Date.now() - toolStart,
518
+ results: results.length
519
+ });
520
+ }
521
+ }
522
+ if (config.tools?.semgrep !== false) {
523
+ const isInstalled = await semgrepRunner.isInstalled();
524
+ if (isInstalled) {
525
+ const toolStart = Date.now();
526
+ const results = await semgrepRunner.run(cwd);
527
+ allResults.push(...results);
528
+ toolStats.push({
529
+ name: "semgrep",
530
+ version: await semgrepRunner.getVersion() ?? void 0,
531
+ duration: Date.now() - toolStart,
532
+ results: results.length
533
+ });
534
+ }
535
+ }
536
+ const filteredResults = useDiffOnly && changedFiles.length > 0 ? filterResultsByFiles(allResults, changedFiles) : allResults;
537
+ const newIssues = filterNewIssues(filteredResults, baseline);
538
+ const baselineStats = getBaselineStats(allResults, baseline);
539
+ const duration = Date.now() - startTime;
540
+ const score = calculateScore(allResults);
541
+ const report = {
542
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
543
+ duration,
544
+ preset: config.preset,
545
+ level: config.level ?? "standard",
546
+ summary: {
547
+ total: allResults.length,
548
+ errors: allResults.filter((r) => r.severity === "error").length,
549
+ warnings: allResults.filter((r) => r.severity === "warn").length,
550
+ infos: allResults.filter((r) => r.severity === "info").length,
551
+ fixed: 0
552
+ },
553
+ score,
554
+ results: allResults,
555
+ tools: toolStats,
556
+ // PR Gate stats
557
+ baseline: baseline ? {
558
+ total: baselineStats.total,
559
+ new: baselineStats.new,
560
+ baselined: baselineStats.baselined,
561
+ fixed: baselineStats.fixed
562
+ } : void 0,
563
+ newIssues,
564
+ diffOnly: useDiffOnly,
565
+ changedFiles: changedFiles.length
566
+ };
567
+ if (format === "json") {
568
+ console.log(JSON.stringify(report, null, 2));
569
+ } else if (format === "sarif") {
570
+ console.log(JSON.stringify(toSarif(report), null, 2));
571
+ } else {
572
+ printCIReport(report, useBaseline);
573
+ }
574
+ const issuesToCheck = useBaseline ? newIssues : allResults;
575
+ if (shouldFail(issuesToCheck, failOn)) {
576
+ if (useBaseline && newIssues.length > 0) {
577
+ console.log(chalk4.red(`
578
+ \u274C CI failed: ${newIssues.length} NEW issue(s) detected
579
+ `));
580
+ }
581
+ process.exit(1);
582
+ }
583
+ if (useBaseline && baseline) {
584
+ console.log(chalk4.green(`
585
+ \u2713 CI passed (${baselineStats.baselined} baselined issues ignored)
586
+ `));
587
+ }
588
+ } catch (error) {
589
+ console.error(chalk4.red("CI check failed:"), error);
590
+ process.exit(1);
591
+ }
592
+ }
593
+ function printCIReport(report, useBaseline) {
594
+ console.log(chalk4.bold("\n\u{1F6E4}\uFE0F Devrail CI Report\n"));
595
+ const scoreColor = report.score >= 80 ? chalk4.green : report.score >= 50 ? chalk4.yellow : chalk4.red;
596
+ console.log(`Score: ${scoreColor.bold(report.score + "/100")}`);
597
+ if (useBaseline && report.baseline) {
598
+ console.log(chalk4.bold("\n\u{1F4CA} PR Gate Status:"));
599
+ console.log(` Total issues: ${report.baseline.total}`);
600
+ console.log(` ${chalk4.green("Fixed:")} ${report.baseline.fixed}`);
601
+ console.log(` ${chalk4.dim("Baselined:")} ${report.baseline.baselined}`);
602
+ console.log(` ${chalk4.red("NEW:")} ${report.baseline.new}`);
603
+ }
604
+ console.log(`
605
+ All Findings:`);
606
+ console.log(` ${chalk4.red("Errors:")} ${report.summary.errors}`);
607
+ console.log(` ${chalk4.yellow("Warnings:")} ${report.summary.warnings}`);
608
+ console.log(` ${chalk4.blue("Info:")} ${report.summary.infos}`);
609
+ if (report.newIssues.length > 0) {
610
+ console.log("\n" + chalk4.bold.red("\u{1F6A8} NEW Issues (will fail CI):"));
611
+ for (const result of report.newIssues) {
612
+ const icon = result.severity === "error" ? chalk4.red("\u2716") : result.severity === "warn" ? chalk4.yellow("\u26A0") : chalk4.blue("\u2139");
613
+ const location = result.file ? `${result.file}${result.line ? `:${result.line}` : ""}` : "";
614
+ console.log(` ${icon} [${result.ruleId}] ${result.message}`);
615
+ if (location) console.log(` ${chalk4.dim(location)}`);
616
+ }
617
+ } else if (useBaseline && report.baseline && report.baseline.baselined > 0) {
618
+ console.log("\n" + chalk4.green("\u2713 No NEW issues! Baselined issues are ignored."));
619
+ }
620
+ if (report.diffOnly) {
621
+ console.log(chalk4.dim(`
622
+ Mode: diff-only (${report.changedFiles} files changed)`));
623
+ }
624
+ console.log(`
625
+ ${chalk4.dim(`Duration: ${report.duration}ms | Tools: ${report.tools.map((t) => t.name).join(", ")}`)}`);
626
+ }
627
+ function toSarif(report) {
628
+ return {
629
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
630
+ version: "2.1.0",
631
+ runs: [
632
+ {
633
+ tool: {
634
+ driver: {
635
+ name: "VibeGuard",
636
+ version: "0.1.0",
637
+ informationUri: "https://vibeguard.dev",
638
+ rules: report.results.map((r) => ({
639
+ id: r.ruleId,
640
+ shortDescription: { text: r.message }
641
+ }))
642
+ }
643
+ },
644
+ results: report.results.map((r) => ({
645
+ ruleId: r.ruleId,
646
+ level: r.severity === "error" ? "error" : r.severity === "warn" ? "warning" : "note",
647
+ message: { text: r.message },
648
+ locations: r.file ? [
649
+ {
650
+ physicalLocation: {
651
+ artifactLocation: { uri: r.file },
652
+ region: r.line ? {
653
+ startLine: r.line,
654
+ startColumn: r.column ?? 1,
655
+ endLine: r.endLine ?? r.line,
656
+ endColumn: r.endColumn ?? r.column ?? 1
657
+ } : void 0
658
+ }
659
+ }
660
+ ] : []
661
+ }))
662
+ }
663
+ ]
664
+ };
665
+ }
666
+
667
+ // src/cli/commands/baseline.ts
668
+ import chalk5 from "chalk";
669
+ import ora3 from "ora";
670
+ async function baselineCommand(options) {
671
+ const cwd = process.cwd();
672
+ const spinner = ora3("Loading configuration...").start();
673
+ try {
674
+ const loaded = await loadConfig(cwd);
675
+ const config = loaded?.config ?? getDefaultConfig();
676
+ const existingBaseline = await loadBaseline(cwd);
677
+ if (options.show) {
678
+ spinner.stop();
679
+ await showBaselineStatus(cwd, existingBaseline);
680
+ return;
681
+ }
682
+ spinner.text = "Running full scan...";
683
+ const allResults = await runFullScan(cwd, config);
684
+ if (existingBaseline && !options.update) {
685
+ spinner.stop();
686
+ const stats = getBaselineStats(allResults, existingBaseline);
687
+ console.log(chalk5.bold("\n\u{1F4CA} Baseline Status:\n"));
688
+ console.log(` Total issues: ${chalk5.cyan(stats.total)}`);
689
+ console.log(` ${chalk5.green("Fixed since baseline:")} ${stats.fixed}`);
690
+ console.log(` ${chalk5.yellow("Baselined (ignored):")} ${stats.baselined}`);
691
+ console.log(` ${chalk5.red("New issues:")} ${stats.new}`);
692
+ if (stats.new > 0) {
693
+ console.log(chalk5.red(`
694
+ \u26A0\uFE0F ${stats.new} new issue(s) detected!`));
695
+ console.log(chalk5.dim("Run `devrail baseline --update` to add them to baseline."));
696
+ } else if (stats.fixed > 0) {
697
+ console.log(chalk5.green(`
698
+ \u2713 ${stats.fixed} issue(s) fixed since baseline!`));
699
+ console.log(chalk5.dim("Run `devrail baseline --update` to update the baseline."));
700
+ } else {
701
+ console.log(chalk5.green("\n\u2713 No changes since baseline."));
702
+ }
703
+ console.log(chalk5.dim(`
704
+ Baseline: .devrail/baseline.json (${existingBaseline.entries.length} entries)
705
+ `));
706
+ return;
707
+ }
708
+ let baseline;
709
+ if (existingBaseline && options.update) {
710
+ spinner.text = "Updating baseline...";
711
+ baseline = updateBaseline(existingBaseline, allResults);
712
+ } else {
713
+ spinner.text = "Creating baseline...";
714
+ baseline = createBaseline(allResults);
715
+ }
716
+ await saveBaseline(cwd, baseline);
717
+ spinner.succeed(chalk5.green("Baseline saved!"));
718
+ console.log(chalk5.bold("\n\u{1F4CB} Baseline Summary:\n"));
719
+ console.log(` Total issues baselined: ${chalk5.cyan(baseline.entries.length)}`);
720
+ console.log(` Created: ${chalk5.dim(baseline.createdAt)}`);
721
+ const byCategory = /* @__PURE__ */ new Map();
722
+ for (const entry of baseline.entries) {
723
+ const category = entry.ruleId.split(".")[0] ?? "other";
724
+ byCategory.set(category, (byCategory.get(category) ?? 0) + 1);
725
+ }
726
+ console.log("\n By category:");
727
+ for (const [category, count] of byCategory) {
728
+ console.log(` ${category}: ${count}`);
729
+ }
730
+ console.log(chalk5.bold("\n\u{1F680} What this means:\n"));
731
+ console.log(` \u2022 These issues are now "accepted" and won't fail CI`);
732
+ console.log(" \u2022 New issues in the same categories WILL fail CI");
733
+ console.log(" \u2022 Fix baselined issues over time to improve your score");
734
+ console.log(chalk5.dim("\nRun `devrail ci` to verify only new issues are blocked.\n"));
735
+ } catch (error) {
736
+ spinner.fail(chalk5.red("Baseline failed"));
737
+ console.error(error);
738
+ process.exit(1);
739
+ }
740
+ }
741
+ async function showBaselineStatus(cwd, baseline) {
742
+ if (!baseline) {
743
+ console.log(chalk5.yellow("\n\u26A0\uFE0F No baseline found."));
744
+ console.log(chalk5.dim("Run `devrail baseline` to create one.\n"));
745
+ return;
746
+ }
747
+ console.log(chalk5.bold("\n\u{1F4CB} Current Baseline:\n"));
748
+ console.log(` Entries: ${chalk5.cyan(baseline.entries.length)}`);
749
+ console.log(` Created: ${chalk5.dim(baseline.createdAt)}`);
750
+ console.log(` Updated: ${chalk5.dim(baseline.updatedAt)}`);
751
+ const byRule = /* @__PURE__ */ new Map();
752
+ for (const entry of baseline.entries) {
753
+ byRule.set(entry.ruleId, (byRule.get(entry.ruleId) ?? 0) + 1);
754
+ }
755
+ console.log("\n Top issues:");
756
+ const sorted = [...byRule.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
757
+ for (const [rule, count] of sorted) {
758
+ console.log(` ${chalk5.dim(rule)}: ${count}`);
759
+ }
760
+ console.log();
761
+ }
762
+ async function runFullScan(cwd, config) {
763
+ const allResults = [];
764
+ const builtinResults = await runBuiltinChecks(cwd, [
765
+ "secrets.gitignore-required",
766
+ "secrets.no-env-commit",
767
+ "deps.lockfile.required",
768
+ "deps.no-unpinned",
769
+ "deps.no-git-deps",
770
+ "tests.unit.required",
771
+ "code.strict-mode",
772
+ "config.node-version",
773
+ "config.editor-config"
774
+ ]);
775
+ allResults.push(...builtinResults);
776
+ if (config.tools?.gitleaks !== false && await gitleaksRunner.isInstalled()) {
777
+ const results = await gitleaksRunner.run(cwd);
778
+ allResults.push(...results);
779
+ }
780
+ if (config.tools?.osvScanner !== false && await osvScannerRunner.isInstalled()) {
781
+ const results = await osvScannerRunner.run(cwd);
782
+ allResults.push(...results);
783
+ }
784
+ if (config.tools?.semgrep !== false && await semgrepRunner.isInstalled()) {
785
+ const results = await semgrepRunner.run(cwd);
786
+ allResults.push(...results);
787
+ }
788
+ return allResults;
789
+ }
790
+
791
+ // src/cli/commands/fix.ts
792
+ import { writeFile as writeFile2, readFile } from "fs/promises";
793
+ import { join as join2 } from "path";
794
+ import chalk6 from "chalk";
795
+ import ora4 from "ora";
796
+ async function fixCommand(options) {
797
+ const cwd = process.cwd();
798
+ const spinner = ora4("Analyzing project for fixes...").start();
799
+ try {
800
+ const fixes = [];
801
+ spinner.text = "Checking .gitignore...";
802
+ fixes.push(...await getGitignoreFixes(cwd));
803
+ spinner.text = "Checking .editorconfig...";
804
+ fixes.push(...await getEditorConfigFixes(cwd));
805
+ spinner.text = "Checking tsconfig.json...";
806
+ fixes.push(...await getTsConfigFixes(cwd));
807
+ spinner.text = "Checking Node version...";
808
+ fixes.push(...await getNodeVersionFixes(cwd));
809
+ spinner.text = "Checking security headers...";
810
+ fixes.push(...await getSecurityFixes(cwd));
811
+ spinner.stop();
812
+ const applicableFixes = options.all ? fixes : fixes.filter((f) => f.safe);
813
+ if (applicableFixes.length === 0) {
814
+ console.log(chalk6.green("\n\u2713 No auto-fixable issues found!\n"));
815
+ return;
816
+ }
817
+ console.log(chalk6.bold(`
818
+ \u{1F527} Found ${applicableFixes.length} fix(es):
819
+ `));
820
+ for (const fix of applicableFixes) {
821
+ const safeLabel = fix.safe ? chalk6.green("[safe]") : chalk6.yellow("[review]");
822
+ console.log(` ${safeLabel} ${chalk6.cyan(fix.file)}`);
823
+ console.log(` ${chalk6.dim(fix.ruleId)}: ${fix.description}`);
824
+ }
825
+ if (options.dryRun) {
826
+ console.log(chalk6.yellow("\n[Dry run] No changes applied."));
827
+ console.log(chalk6.dim("Run without --dry-run to apply fixes.\n"));
828
+ return;
829
+ }
830
+ console.log();
831
+ const applySpinner = ora4("Applying fixes...").start();
832
+ let applied = 0;
833
+ for (const fix of applicableFixes) {
834
+ applySpinner.text = `Fixing ${fix.file}...`;
835
+ try {
836
+ await fix.action();
837
+ applied++;
838
+ } catch (error) {
839
+ console.error(chalk6.red(`Failed to fix ${fix.file}: ${error}`));
840
+ }
841
+ }
842
+ applySpinner.succeed(chalk6.green(`Applied ${applied}/${applicableFixes.length} fix(es)`));
843
+ console.log(chalk6.bold("\n\u{1F680} Next steps:\n"));
844
+ console.log(` 1. ${chalk6.cyan("git diff")} Review the changes`);
845
+ console.log(` 2. ${chalk6.cyan("devrail check")} Verify fixes worked`);
846
+ console.log(` 3. ${chalk6.cyan("git commit")} Commit the fixes
847
+ `);
848
+ } catch (error) {
849
+ spinner.fail(chalk6.red("Fix failed"));
850
+ console.error(error);
851
+ process.exit(1);
852
+ }
853
+ }
854
+ async function getGitignoreFixes(cwd) {
855
+ const fixes = [];
856
+ const gitignorePath = join2(cwd, ".gitignore");
857
+ const requiredPatterns = [
858
+ "# Environment",
859
+ ".env",
860
+ ".env.local",
861
+ ".env*.local",
862
+ "",
863
+ "# Dependencies",
864
+ "node_modules/",
865
+ "",
866
+ "# Build",
867
+ "dist/",
868
+ "build/",
869
+ ".next/",
870
+ "",
871
+ "# Devrail",
872
+ ".devrail/"
873
+ ];
874
+ try {
875
+ const content = await readFile(gitignorePath, "utf-8");
876
+ const missingPatterns = requiredPatterns.filter(
877
+ (p) => p && !content.includes(p.replace("/", ""))
878
+ );
879
+ if (missingPatterns.length > 3) {
880
+ fixes.push({
881
+ file: ".gitignore",
882
+ ruleId: "secrets.gitignore-required",
883
+ description: "Add missing patterns for env files and build artifacts",
884
+ safe: true,
885
+ action: async () => {
886
+ const newContent = content.trimEnd() + "\n\n" + missingPatterns.join("\n") + "\n";
887
+ await writeFile2(gitignorePath, newContent, "utf-8");
888
+ }
889
+ });
890
+ }
891
+ } catch {
892
+ fixes.push({
893
+ file: ".gitignore",
894
+ ruleId: "secrets.gitignore-required",
895
+ description: "Create .gitignore with security patterns",
896
+ safe: true,
897
+ action: async () => {
898
+ await writeFile2(gitignorePath, requiredPatterns.join("\n") + "\n", "utf-8");
899
+ }
900
+ });
901
+ }
902
+ return fixes;
903
+ }
904
+ async function getEditorConfigFixes(cwd) {
905
+ const fixes = [];
906
+ const editorConfigPath = join2(cwd, ".editorconfig");
907
+ const defaultConfig = `# EditorConfig - https://editorconfig.org
908
+ root = true
909
+
910
+ [*]
911
+ indent_style = space
912
+ indent_size = 2
913
+ end_of_line = lf
914
+ charset = utf-8
915
+ trim_trailing_whitespace = true
916
+ insert_final_newline = true
917
+
918
+ [*.md]
919
+ trim_trailing_whitespace = false
920
+ `;
921
+ try {
922
+ await readFile(editorConfigPath, "utf-8");
923
+ } catch {
924
+ fixes.push({
925
+ file: ".editorconfig",
926
+ ruleId: "config.editor-config",
927
+ description: "Create .editorconfig for consistent formatting",
928
+ safe: true,
929
+ action: async () => {
930
+ await writeFile2(editorConfigPath, defaultConfig, "utf-8");
931
+ }
932
+ });
933
+ }
934
+ return fixes;
935
+ }
936
+ async function getTsConfigFixes(cwd) {
937
+ const fixes = [];
938
+ const tsconfigPath = join2(cwd, "tsconfig.json");
939
+ try {
940
+ const content = await readFile(tsconfigPath, "utf-8");
941
+ const tsconfig = JSON.parse(content);
942
+ if (!tsconfig.compilerOptions?.strict) {
943
+ fixes.push({
944
+ file: "tsconfig.json",
945
+ ruleId: "code.strict-mode",
946
+ description: "Enable TypeScript strict mode",
947
+ safe: false,
948
+ // May break existing code
949
+ action: async () => {
950
+ tsconfig.compilerOptions = tsconfig.compilerOptions || {};
951
+ tsconfig.compilerOptions.strict = true;
952
+ await writeFile2(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n", "utf-8");
953
+ }
954
+ });
955
+ }
956
+ } catch {
957
+ }
958
+ return fixes;
959
+ }
960
+ async function getNodeVersionFixes(cwd) {
961
+ const fixes = [];
962
+ const nvmrcPath = join2(cwd, ".nvmrc");
963
+ try {
964
+ await readFile(nvmrcPath, "utf-8");
965
+ } catch {
966
+ try {
967
+ const pkgPath = join2(cwd, "package.json");
968
+ const pkgContent = await readFile(pkgPath, "utf-8");
969
+ const pkg = JSON.parse(pkgContent);
970
+ if (!pkg.engines?.node) {
971
+ fixes.push({
972
+ file: ".nvmrc",
973
+ ruleId: "config.node-version",
974
+ description: "Create .nvmrc with Node.js LTS version",
975
+ safe: true,
976
+ action: async () => {
977
+ await writeFile2(nvmrcPath, "20\n", "utf-8");
978
+ }
979
+ });
980
+ }
981
+ } catch {
982
+ }
983
+ }
984
+ return fixes;
985
+ }
986
+ async function getSecurityFixes(cwd) {
987
+ const fixes = [];
988
+ const nextConfigPath = join2(cwd, "next.config.js");
989
+ const nextConfigMjsPath = join2(cwd, "next.config.mjs");
990
+ try {
991
+ let configPath = nextConfigPath;
992
+ let content;
993
+ try {
994
+ content = await readFile(nextConfigPath, "utf-8");
995
+ } catch {
996
+ content = await readFile(nextConfigMjsPath, "utf-8");
997
+ configPath = nextConfigMjsPath;
998
+ }
999
+ if (!content.includes("headers")) {
1000
+ fixes.push({
1001
+ file: configPath.split("/").pop() ?? "next.config.js",
1002
+ ruleId: "security.headers.required",
1003
+ description: "Add security headers configuration (manual review needed)",
1004
+ safe: false,
1005
+ action: async () => {
1006
+ const headerComment = `
1007
+ // TODO: Add security headers
1008
+ // See: https://nextjs.org/docs/app/api-reference/next-config-js/headers
1009
+ // async headers() {
1010
+ // return [
1011
+ // {
1012
+ // source: '/:path*',
1013
+ // headers: [
1014
+ // { key: 'X-Frame-Options', value: 'DENY' },
1015
+ // { key: 'X-Content-Type-Options', value: 'nosniff' },
1016
+ // { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
1017
+ // ],
1018
+ // },
1019
+ // ];
1020
+ // },
1021
+ `;
1022
+ const newContent = content.replace(
1023
+ "module.exports",
1024
+ headerComment + "\nmodule.exports"
1025
+ );
1026
+ await writeFile2(configPath, newContent, "utf-8");
1027
+ }
1028
+ });
1029
+ }
1030
+ } catch {
1031
+ }
1032
+ return fixes;
1033
+ }
1034
+
1035
+ // src/cli/commands/watch.ts
1036
+ import { watch } from "fs";
1037
+ import chalk7 from "chalk";
1038
+ async function watchCommand(options) {
1039
+ const cwd = process.cwd();
1040
+ const debounceMs = options.debounce ?? 500;
1041
+ console.log(chalk7.bold.cyan("\n\u{1F6E4}\uFE0F Devrail Guard Mode\n"));
1042
+ console.log(chalk7.dim("Watching for changes... Press Ctrl+C to stop.\n"));
1043
+ let lastRun = 0;
1044
+ let timeout = null;
1045
+ const runCheck = async () => {
1046
+ const now = Date.now();
1047
+ if (now - lastRun < debounceMs) return;
1048
+ lastRun = now;
1049
+ console.clear();
1050
+ console.log(chalk7.bold.cyan("\u{1F6E4}\uFE0F Devrail Guard Mode"));
1051
+ console.log(chalk7.dim(`Last check: ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}
1052
+ `));
1053
+ try {
1054
+ const results = await runBuiltinChecks(cwd, [
1055
+ "secrets.gitignore-required",
1056
+ "secrets.no-env-commit",
1057
+ "deps.lockfile.required",
1058
+ "tests.unit.required",
1059
+ "code.strict-mode"
1060
+ ]);
1061
+ const errors = results.filter((r) => r.severity === "error");
1062
+ const warnings = results.filter((r) => r.severity === "warn");
1063
+ if (results.length === 0) {
1064
+ console.log(chalk7.green.bold("\u2713 All clear!\n"));
1065
+ } else {
1066
+ console.log(chalk7.bold("Issues:"));
1067
+ for (const result of errors) {
1068
+ console.log(` ${chalk7.red("\u2716")} ${result.message}`);
1069
+ console.log(` ${chalk7.dim(result.ruleId)}`);
1070
+ }
1071
+ for (const result of warnings.slice(0, 5)) {
1072
+ console.log(` ${chalk7.yellow("\u26A0")} ${result.message}`);
1073
+ console.log(` ${chalk7.dim(result.ruleId)}`);
1074
+ }
1075
+ if (warnings.length > 5) {
1076
+ console.log(chalk7.dim(`
1077
+ ... and ${warnings.length - 5} more warnings`));
1078
+ }
1079
+ console.log(`
1080
+ ${chalk7.red("Errors:")} ${errors.length} | ${chalk7.yellow("Warnings:")} ${warnings.length}`);
1081
+ }
1082
+ const score = Math.max(0, 100 - errors.length * 10 - warnings.length * 3);
1083
+ const scoreColor = score >= 80 ? chalk7.green : score >= 50 ? chalk7.yellow : chalk7.red;
1084
+ console.log(`
1085
+ Score: ${scoreColor.bold(score + "/100")}`);
1086
+ console.log(chalk7.dim("\nWatching for changes..."));
1087
+ } catch (error) {
1088
+ console.error(chalk7.red("Check failed:"), error);
1089
+ }
1090
+ };
1091
+ await runCheck();
1092
+ const watcher = watch(cwd, { recursive: true }, (eventType, filename) => {
1093
+ if (!filename) return;
1094
+ if (filename.includes("node_modules") || filename.includes("dist") || filename.includes(".git") || filename.includes(".devrail")) {
1095
+ return;
1096
+ }
1097
+ if (timeout) clearTimeout(timeout);
1098
+ timeout = setTimeout(runCheck, debounceMs);
1099
+ });
1100
+ process.on("SIGINT", () => {
1101
+ watcher.close();
1102
+ console.log(chalk7.dim("\n\nStopped watching.\n"));
1103
+ process.exit(0);
1104
+ });
1105
+ }
1106
+
1107
+ // src/cli/commands/report.ts
1108
+ import { writeFile as writeFile3 } from "fs/promises";
1109
+ import chalk8 from "chalk";
1110
+ import ora5 from "ora";
1111
+ async function reportCommand(options) {
1112
+ const cwd = process.cwd();
1113
+ const format = options.format ?? "console";
1114
+ const spinner = ora5("Generating security posture report...").start();
1115
+ try {
1116
+ const loaded = await loadConfig(cwd);
1117
+ const config = loaded?.config ?? getDefaultConfig();
1118
+ const allResults = [];
1119
+ spinner.text = "Running builtin checks...";
1120
+ const builtinResults = await runBuiltinChecks(cwd, [
1121
+ "secrets.gitignore-required",
1122
+ "secrets.no-env-commit",
1123
+ "deps.lockfile.required",
1124
+ "deps.no-unpinned",
1125
+ "deps.no-git-deps",
1126
+ "tests.unit.required",
1127
+ "code.strict-mode",
1128
+ "config.node-version",
1129
+ "config.editor-config"
1130
+ ]);
1131
+ allResults.push(...builtinResults);
1132
+ if (await gitleaksRunner.isInstalled()) {
1133
+ spinner.text = "Running secret scan...";
1134
+ const results = await gitleaksRunner.run(cwd);
1135
+ allResults.push(...results);
1136
+ }
1137
+ if (await osvScannerRunner.isInstalled()) {
1138
+ spinner.text = "Running dependency scan...";
1139
+ const results = await osvScannerRunner.run(cwd);
1140
+ allResults.push(...results);
1141
+ }
1142
+ const baseline = await loadBaseline(cwd);
1143
+ const baselineStats = getBaselineStats(allResults, baseline);
1144
+ const posture = calculatePosture(allResults, baseline);
1145
+ spinner.stop();
1146
+ if (format === "json") {
1147
+ const report = {
1148
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1149
+ posture,
1150
+ issues: allResults,
1151
+ baseline: baseline ? baselineStats : void 0
1152
+ };
1153
+ if (options.output) {
1154
+ await writeFile3(options.output, JSON.stringify(report, null, 2));
1155
+ console.log(chalk8.green(`Report saved to ${options.output}`));
1156
+ } else {
1157
+ console.log(JSON.stringify(report, null, 2));
1158
+ }
1159
+ } else if (format === "markdown") {
1160
+ const md = generateMarkdownReport(posture, allResults, baselineStats);
1161
+ if (options.output) {
1162
+ await writeFile3(options.output, md);
1163
+ console.log(chalk8.green(`Report saved to ${options.output}`));
1164
+ } else {
1165
+ console.log(md);
1166
+ }
1167
+ } else {
1168
+ printConsoleReport(posture, allResults, baselineStats);
1169
+ }
1170
+ } catch (error) {
1171
+ spinner.fail(chalk8.red("Report generation failed"));
1172
+ console.error(error);
1173
+ process.exit(1);
1174
+ }
1175
+ }
1176
+ function calculatePosture(results, baseline) {
1177
+ const categories = {
1178
+ secrets: { score: 100, issues: 0 },
1179
+ deps: { score: 100, issues: 0 },
1180
+ security: { score: 100, issues: 0 },
1181
+ tests: { score: 100, issues: 0 },
1182
+ config: { score: 100, issues: 0 }
1183
+ };
1184
+ for (const result of results) {
1185
+ const category = result.ruleId.split(".")[0];
1186
+ if (categories[category]) {
1187
+ categories[category].issues++;
1188
+ const penalty = result.severity === "error" ? 15 : result.severity === "warn" ? 5 : 2;
1189
+ categories[category].score = Math.max(0, categories[category].score - penalty);
1190
+ }
1191
+ }
1192
+ const overall = Math.round(
1193
+ categories.secrets.score * 0.3 + categories.deps.score * 0.25 + categories.security.score * 0.2 + categories.tests.score * 0.15 + categories.config.score * 0.1
1194
+ );
1195
+ let trend;
1196
+ let previousScore;
1197
+ if (baseline) {
1198
+ previousScore = baseline.entries.length > 0 ? Math.max(0, 100 - baseline.entries.length * 3) : 100;
1199
+ if (overall > previousScore + 5) trend = "improving";
1200
+ else if (overall < previousScore - 5) trend = "declining";
1201
+ else trend = "stable";
1202
+ }
1203
+ return { overall, categories, trend, previousScore };
1204
+ }
1205
+ function printConsoleReport(posture, results, baselineStats) {
1206
+ console.log(chalk8.bold.cyan("\n\u{1F4CA} Devrail Security Posture Report\n"));
1207
+ console.log(chalk8.dim("\u2500".repeat(50)));
1208
+ const scoreColor = posture.overall >= 80 ? chalk8.green : posture.overall >= 50 ? chalk8.yellow : chalk8.red;
1209
+ const filledBars = Math.round(posture.overall / 5);
1210
+ const emptyBars = 20 - filledBars;
1211
+ const bar = scoreColor("\u2588".repeat(filledBars)) + chalk8.dim("\u2591".repeat(emptyBars));
1212
+ console.log(`
1213
+ ${chalk8.bold("Overall Score:")} ${scoreColor.bold(posture.overall + "/100")}`);
1214
+ console.log(` ${bar}`);
1215
+ if (posture.trend) {
1216
+ const trendIcon = posture.trend === "improving" ? chalk8.green("\u2191") : posture.trend === "declining" ? chalk8.red("\u2193") : chalk8.dim("\u2192");
1217
+ console.log(` ${trendIcon} ${posture.trend} (was ${posture.previousScore})`);
1218
+ }
1219
+ console.log(chalk8.bold("\n\u{1F4C8} Category Breakdown:\n"));
1220
+ const cats = [
1221
+ { name: "Secrets Hygiene", key: "secrets", weight: "30%" },
1222
+ { name: "Dependencies", key: "deps", weight: "25%" },
1223
+ { name: "Security Patterns", key: "security", weight: "20%" },
1224
+ { name: "Test Coverage", key: "tests", weight: "15%" },
1225
+ { name: "Configuration", key: "config", weight: "10%" }
1226
+ ];
1227
+ for (const cat of cats) {
1228
+ const data = posture.categories[cat.key];
1229
+ const color = data.score >= 80 ? chalk8.green : data.score >= 50 ? chalk8.yellow : chalk8.red;
1230
+ const miniBar = color("\u2588".repeat(Math.round(data.score / 10))) + chalk8.dim("\u2591".repeat(10 - Math.round(data.score / 10)));
1231
+ console.log(` ${miniBar} ${color(data.score.toString().padStart(3))} ${cat.name} (${cat.weight})`);
1232
+ if (data.issues > 0) {
1233
+ console.log(` ${chalk8.dim(`${data.issues} issue(s)`)}`);
1234
+ }
1235
+ }
1236
+ if (baselineStats && baselineStats.baselined > 0) {
1237
+ console.log(chalk8.bold("\n\u{1F4CB} Technical Debt:\n"));
1238
+ console.log(` Baselined issues: ${chalk8.yellow(baselineStats.baselined)}`);
1239
+ console.log(` Fixed since baseline: ${chalk8.green(baselineStats.fixed)}`);
1240
+ console.log(` New issues: ${chalk8.red(baselineStats.new)}`);
1241
+ }
1242
+ console.log(chalk8.bold("\n\u{1F3AF} Top Actions:\n"));
1243
+ const actions = [];
1244
+ if (posture.categories.secrets.issues > 0) {
1245
+ actions.push("Fix secret/credential issues");
1246
+ }
1247
+ if (posture.categories.deps.issues > 0) {
1248
+ actions.push("Update vulnerable dependencies");
1249
+ }
1250
+ if (posture.categories.tests.issues > 0) {
1251
+ actions.push("Add unit tests");
1252
+ }
1253
+ if (posture.categories.security.issues > 0) {
1254
+ actions.push("Address security patterns");
1255
+ }
1256
+ if (posture.categories.config.issues > 0) {
1257
+ actions.push("Improve project configuration");
1258
+ }
1259
+ for (let i = 0; i < Math.min(3, actions.length); i++) {
1260
+ console.log(` ${i + 1}. ${actions[i]}`);
1261
+ }
1262
+ if (actions.length === 0) {
1263
+ console.log(chalk8.green(" \u2713 No critical actions needed!"));
1264
+ }
1265
+ console.log();
1266
+ }
1267
+ function generateMarkdownReport(posture, results, baselineStats) {
1268
+ const lines = [];
1269
+ lines.push("# \u{1F4CA} Devrail Security Posture Report");
1270
+ lines.push("");
1271
+ lines.push(`**Date:** ${(/* @__PURE__ */ new Date()).toLocaleDateString()}`);
1272
+ lines.push(`**Overall Score:** ${posture.overall}/100`);
1273
+ if (posture.trend) {
1274
+ const emoji = posture.trend === "improving" ? "\u{1F4C8}" : posture.trend === "declining" ? "\u{1F4C9}" : "\u27A1\uFE0F";
1275
+ lines.push(`**Trend:** ${emoji} ${posture.trend}`);
1276
+ }
1277
+ lines.push("");
1278
+ lines.push("## Category Breakdown");
1279
+ lines.push("");
1280
+ lines.push("| Category | Score | Issues |");
1281
+ lines.push("|----------|-------|--------|");
1282
+ const cats = [
1283
+ { name: "Secrets Hygiene", key: "secrets" },
1284
+ { name: "Dependencies", key: "deps" },
1285
+ { name: "Security Patterns", key: "security" },
1286
+ { name: "Test Coverage", key: "tests" },
1287
+ { name: "Configuration", key: "config" }
1288
+ ];
1289
+ for (const cat of cats) {
1290
+ const data = posture.categories[cat.key];
1291
+ const emoji = data.score >= 80 ? "\u2705" : data.score >= 50 ? "\u26A0\uFE0F" : "\u274C";
1292
+ lines.push(`| ${cat.name} | ${emoji} ${data.score}/100 | ${data.issues} |`);
1293
+ }
1294
+ if (baselineStats && baselineStats.baselined > 0) {
1295
+ lines.push("");
1296
+ lines.push("## Technical Debt");
1297
+ lines.push("");
1298
+ lines.push(`- **Baselined:** ${baselineStats.baselined} issues`);
1299
+ lines.push(`- **Fixed:** ${baselineStats.fixed} issues`);
1300
+ lines.push(`- **New:** ${baselineStats.new} issues`);
1301
+ }
1302
+ lines.push("");
1303
+ lines.push("---");
1304
+ lines.push("*Generated by [Devrail](https://devrail.dev)*");
1305
+ return lines.join("\n");
1306
+ }
1307
+
1308
+ // src/cli/index.ts
1309
+ var program = new Command();
1310
+ program.name("dr").description("Devrail - Security & Quality Guardrails").version("0.1.0");
1311
+ program.command("init").description("Initialize Devrail in your project").option("-p, --preset <preset>", "Preset to use (auto-detected if not specified)").option("-l, --level <level>", "Level (basic, standard, strict)", "standard").option("--no-hooks", "Skip git hooks setup").option("--no-ci", "Skip CI template generation").action(initCommand);
1312
+ program.command("check").description("Run local checks (fast)").option("-f, --fix", "Apply safe autofixes").option("--changed", "Only check changed files").option("--json", "Output as JSON").action(checkCommand);
1313
+ program.command("ci").description("Run full CI checks (blocking)").option("--fail-on <severity>", "Fail on severity level (info, warn, error)", "error").option("--format <format>", "Output format (console, json, sarif)", "console").option("--new-only", "Only fail on NEW issues (not baselined)", true).option("--no-new-only", "Fail on ALL issues (ignore baseline)").option("--diff-only", "Only check changed files").action(ciCommand);
1314
+ program.command("explain <ruleId>").description("Explain a rule and how to fix it").action(explainCommand);
1315
+ program.command("fix").description("Apply safe auto-fixes").option("--dry-run", "Show what would be fixed without applying").option("--all", "Include fixes that need review").action(fixCommand);
1316
+ program.command("baseline").description("Accept existing issues, block only new ones").option("--update", "Update existing baseline with current issues").option("--show", "Show current baseline status").action(baselineCommand);
1317
+ program.command("guard").alias("watch").description("Watch mode - continuous feedback while coding").option("--debounce <ms>", "Debounce interval in milliseconds", "500").action(watchCommand);
1318
+ program.command("report").description("Generate security posture report").option("--format <format>", "Output format (console, json, markdown)", "console").option("-o, --output <file>", "Output file path").action(reportCommand);
1319
+ program.command("rules").description("List all available rules").option("-c, --category <category>", "Filter by category").option("-l, --level <level>", "Filter by level").action(async (options) => {
1320
+ const { rules } = await import("../rules-XIWD4KI4.js");
1321
+ let filtered = rules;
1322
+ if (options.category) {
1323
+ filtered = filtered.filter((r) => r.category === options.category);
1324
+ }
1325
+ if (options.level) {
1326
+ filtered = filtered.filter((r) => r.levels[options.level]);
1327
+ }
1328
+ console.log(chalk9.bold("\nDevrail Rules\n"));
1329
+ const categories = [...new Set(filtered.map((r) => r.category))];
1330
+ for (const category of categories) {
1331
+ console.log(chalk9.cyan.bold(`
1332
+ ${category.toUpperCase()}`));
1333
+ const categoryRules = filtered.filter((r) => r.category === category);
1334
+ for (const rule of categoryRules) {
1335
+ const severityColor = rule.severity === "error" ? chalk9.red : rule.severity === "warn" ? chalk9.yellow : chalk9.blue;
1336
+ const autofix = rule.autofix ? chalk9.green(" [autofix]") : "";
1337
+ console.log(` ${severityColor("\u25CF")} ${chalk9.bold(rule.id)}${autofix}`);
1338
+ console.log(` ${chalk9.dim(rule.description)}`);
1339
+ }
1340
+ }
1341
+ console.log(`
1342
+ ${chalk9.dim(`Total: ${filtered.length} rules`)}
1343
+ `);
1344
+ });
1345
+ program.parse();
1346
+ //# sourceMappingURL=index.js.map