qodfy 0.2.9 → 0.2.10

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.
Files changed (3) hide show
  1. package/README.md +20 -3
  2. package/dist/index.js +537 -14
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -18,9 +18,24 @@ Scan a specific folder:
18
18
  npx qodfy scan --path apps/web
19
19
  ```
20
20
 
21
+ Print machine-readable JSON:
22
+
23
+ ```bash
24
+ npx qodfy scan --json
25
+ ```
26
+
27
+ Write JSON or Markdown reports:
28
+
29
+ ```bash
30
+ npx qodfy scan --json --output qodfy-report.json
31
+ npx qodfy scan --report qodfy-report.md
32
+ ```
33
+
34
+ The Markdown report is the **Qodfy Launch Report**: a senior-engineer-style review with a launch status, executive summary, top priorities, what looks good, and per-issue context (what Qodfy found, why it matters, evidence, suggested fix, and an AI fix prompt).
35
+
21
36
  ## What Qodfy Checks Today
22
37
 
23
- Qodfy v0.1 scans locally and looks for common launch-readiness risks:
38
+ Qodfy scans locally and looks for common launch-readiness risks:
24
39
 
25
40
  - Next.js project detection
26
41
  - missing `.env.example`
@@ -67,6 +82,9 @@ Fix critical issues first, then warnings, then cleanup items.
67
82
  ```bash
68
83
  qodfy scan
69
84
  qodfy scan --path <project-path>
85
+ qodfy scan --json
86
+ qodfy scan --json --output qodfy-report.json
87
+ qodfy scan --report qodfy-report.md
70
88
  qodfy --help
71
89
  qodfy --version
72
90
  ```
@@ -79,13 +97,12 @@ Qodfy starts at `100`.
79
97
  - Warning: `-8`
80
98
  - Info: no major score penalty
81
99
 
82
- The score is intentionally simple in v0.1 and will become more precise as the rule set improves.
100
+ The score is intentionally simple and will become more precise as the rule set improves.
83
101
 
84
102
  ## Roadmap
85
103
 
86
104
  Near-term priorities:
87
105
 
88
- - JSON and Markdown output
89
106
  - `.env.example` coverage for `process.env.*`
90
107
  - exposed secret detection
91
108
  - Stripe webhook signature checks
package/dist/index.js CHANGED
@@ -11,34 +11,59 @@ import {
11
11
  scanProject,
12
12
  validScanChecks
13
13
  } from "@qodfy/core";
14
- var CLI_VERSION = "0.2.8";
14
+ var CLI_VERSION = "0.2.10";
15
15
  var DEFAULT_MAX_ISSUES = 5;
16
16
  var program = new Command();
17
17
  program.name("qodfy").description("Launch readiness scanner for AI-built apps.").version(CLI_VERSION);
18
- program.command("scan").description("Scan a project for launch readiness issues.").option("-p, --path <path>", "Project path to scan", process.cwd()).option("--max-issues <number>", "Maximum number of issues to display", String(DEFAULT_MAX_ISSUES)).option("--prompts", "Show safe copy-paste fix prompts for displayed issues").option("--prompt <issue-id>", "Show the safe AI fix prompt for one issue").option("--checks <checks>", "Comma-separated checks to run").option("--all", "Run all checks without prompting").option("--no-interactive", "Skip interactive prompts and run the recommended scan").action(async (options) => {
18
+ program.command("scan").description("Scan a project for launch readiness issues.").option("-p, --path <path>", "Project path to scan", process.cwd()).option("--max-issues <number>", "Maximum number of issues to display", String(DEFAULT_MAX_ISSUES)).option("--prompts", "Show safe copy-paste fix prompts for displayed issues").option("--prompt <issue-id>", "Show the safe AI fix prompt for one issue").option("--checks <checks>", "Comma-separated checks to run").option("--all", "Run all checks without prompting").option("--no-interactive", "Skip interactive prompts and run the recommended scan").option("--json", "Print a machine-readable JSON report").option("--output <file>", "Write the JSON report to a file").option("--report <file>", "Write a human-readable Markdown report to a file").action(async (options) => {
19
+ const outputOptionsResult = validateScanOutputOptions(options);
20
+ if (!outputOptionsResult.ok) {
21
+ printScanError(outputOptionsResult.reason, !isOutputMode(options));
22
+ process.exitCode = 1;
23
+ return;
24
+ }
19
25
  const pathResult = await resolveProjectPath(options.path);
20
26
  if (!pathResult.ok) {
21
- printScanError(pathResult.reason);
27
+ printScanError(pathResult.reason, !isOutputMode(options));
22
28
  process.exitCode = 1;
23
29
  return;
24
30
  }
25
31
  try {
26
32
  const scanModeResult = await resolveScanMode(options);
27
33
  if (!scanModeResult.ok) {
28
- printScanError(scanModeResult.reason);
34
+ printScanError(scanModeResult.reason, !isOutputMode(options));
29
35
  process.exitCode = 1;
30
36
  return;
31
37
  }
32
- if (scanModeResult.notice) {
38
+ if (scanModeResult.notice && !isOutputMode(options)) {
33
39
  console.log(pc.dim(scanModeResult.notice));
34
40
  console.log("");
35
41
  }
36
- console.log(pc.cyan("Qodfy is scanning your project...\n"));
42
+ if (!isOutputMode(options)) {
43
+ console.log(pc.cyan("Qodfy is scanning your project...\n"));
44
+ }
37
45
  const report = await scanProject({
38
46
  projectPath: pathResult.projectPath,
39
47
  checks: scanModeResult.checks,
40
48
  includeLowConfidence: Boolean(scanModeResult.includeLowConfidence)
41
49
  });
50
+ const outputReport = createOutputReport(report, scanModeResult);
51
+ if (options.json) {
52
+ const jsonReport = `${JSON.stringify(outputReport, null, 2)}
53
+ `;
54
+ if (options.output) {
55
+ await writeReportFile(options.output, jsonReport);
56
+ console.log(`Qodfy JSON report saved to ${options.output}`);
57
+ } else {
58
+ process.stdout.write(jsonReport);
59
+ }
60
+ return;
61
+ }
62
+ if (options.report) {
63
+ await writeReportFile(options.report, renderMarkdownReport(outputReport));
64
+ console.log(`Qodfy report saved to ${options.report}`);
65
+ return;
66
+ }
42
67
  if (options.prompt) {
43
68
  printPromptFromReport(report, options.prompt);
44
69
  return;
@@ -55,7 +80,7 @@ program.command("scan").description("Scan a project for launch readiness issues.
55
80
  if (isPromptCancelError(error)) {
56
81
  console.log("Scan cancelled.");
57
82
  } else {
58
- printScanError(getErrorMessage(error));
83
+ printScanError(getErrorMessage(error), !isOutputMode(options));
59
84
  }
60
85
  process.exitCode = 1;
61
86
  }
@@ -113,6 +138,30 @@ var categoryLabels = {
113
138
  project: "Project"
114
139
  };
115
140
  await program.parseAsync();
141
+ function validateScanOutputOptions(options) {
142
+ if (options.output && !options.json) {
143
+ return {
144
+ ok: false,
145
+ reason: "Use --output together with --json, for example: qodfy scan --json --output qodfy-report.json."
146
+ };
147
+ }
148
+ if (options.json && options.report) {
149
+ return {
150
+ ok: false,
151
+ reason: "Use either --json or --report for one scan command, not both."
152
+ };
153
+ }
154
+ if ((options.json || options.report) && options.prompt) {
155
+ return {
156
+ ok: false,
157
+ reason: "Use qodfy prompt <issue-id> for fix prompts, or run qodfy scan --json/--report for reports."
158
+ };
159
+ }
160
+ return { ok: true };
161
+ }
162
+ function isOutputMode(options) {
163
+ return Boolean(options.json || options.output || options.report);
164
+ }
116
165
  async function resolveScanMode(options) {
117
166
  if (options.checks) {
118
167
  const parsedChecks = parseChecks(options.checks);
@@ -141,6 +190,13 @@ async function resolveScanMode(options) {
141
190
  includeLowConfidence: true
142
191
  };
143
192
  }
193
+ if (isOutputMode(options)) {
194
+ return {
195
+ ok: true,
196
+ checks: [...recommendedScanChecks],
197
+ label: "Recommended launch scan"
198
+ };
199
+ }
144
200
  if (options.interactive === false) {
145
201
  return {
146
202
  ok: true,
@@ -380,6 +436,464 @@ async function resolveProjectPath(projectPath) {
380
436
  };
381
437
  }
382
438
  }
439
+ function createOutputReport(report, scanMode) {
440
+ return {
441
+ qodfyVersion: CLI_VERSION,
442
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
443
+ projectPath: report.projectPath,
444
+ scanMode: scanMode.label,
445
+ checks: [...scanMode.checks],
446
+ score: report.score,
447
+ stats: { ...report.stats },
448
+ issues: report.issues
449
+ };
450
+ }
451
+ async function writeReportFile(outputPath, content) {
452
+ const resolvedOutputPath = path.resolve(outputPath);
453
+ await fs.mkdir(path.dirname(resolvedOutputPath), { recursive: true });
454
+ await fs.writeFile(resolvedOutputPath, content, "utf8");
455
+ }
456
+ function renderMarkdownReport(report) {
457
+ const projectName = path.basename(report.projectPath) || report.projectPath;
458
+ const statusLabel = getStatusLabel(report.score);
459
+ const criticalCount = countIssuesBySeverity(report.issues, "critical");
460
+ const warningCount = countIssuesBySeverity(report.issues, "warning");
461
+ const infoCount = countIssuesBySeverity(report.issues, "info");
462
+ const lines = [
463
+ "# Qodfy Launch Readiness Report",
464
+ "",
465
+ `Generated: ${report.generatedAt}`,
466
+ `Project: ${projectName}`,
467
+ `Scan mode: ${report.scanMode}`,
468
+ `Score: ${report.score}/100`,
469
+ `Launch status: ${statusLabel}`,
470
+ "",
471
+ "## Executive Summary",
472
+ "",
473
+ getExecutiveSummary(report),
474
+ "",
475
+ "## Score Breakdown",
476
+ "",
477
+ `- Critical issues: ${criticalCount}`,
478
+ `- Warnings: ${warningCount}`,
479
+ `- Info: ${infoCount}`,
480
+ `- Files scanned: ${report.stats.totalFiles}`,
481
+ `- API routes scanned: ${report.stats.apiRoutes}`,
482
+ `- Scan duration: ${formatDuration(report.stats.durationMs)}`,
483
+ "",
484
+ "## Top Priorities",
485
+ ""
486
+ ];
487
+ const priorities = getTopPriorities(report.issues);
488
+ if (priorities.length === 0) {
489
+ lines.push("No urgent priorities found. Review warnings below before launch.");
490
+ } else {
491
+ for (const [index, priority] of priorities.entries()) {
492
+ lines.push(`${index + 1}. ${priority}`);
493
+ }
494
+ }
495
+ lines.push("", "## What Looks Good", "");
496
+ const observations = getWhatLooksGood(report);
497
+ if (observations.length === 0) {
498
+ lines.push("No positive observations to highlight from this scan.");
499
+ } else {
500
+ for (const observation of observations) {
501
+ lines.push(`- ${observation}`);
502
+ }
503
+ }
504
+ lines.push("", "## Issues by Priority", "");
505
+ if (report.issues.length === 0) {
506
+ lines.push("No issues found.");
507
+ appendMarkdownFooter(lines);
508
+ return `${lines.join("\n").trimEnd()}
509
+ `;
510
+ }
511
+ const sortedIssues = getSortedDisplayIssues(report.issues);
512
+ const severityOrder = ["critical", "warning", "info"];
513
+ for (const severity of severityOrder) {
514
+ const severityIssues = sortedIssues.filter((issue) => issue.severity === severity);
515
+ if (severityIssues.length === 0) {
516
+ continue;
517
+ }
518
+ lines.push(`**${getSeverityHeading(severity, severityIssues.length)}**`, "");
519
+ for (const category of categoryOrder) {
520
+ const categoryIssues = severityIssues.filter((issue) => issue.category === category);
521
+ if (categoryIssues.length === 0) {
522
+ continue;
523
+ }
524
+ lines.push(`_${categoryLabels[category]}_`, "");
525
+ for (const issue of categoryIssues) {
526
+ appendMarkdownIssue(lines, issue);
527
+ }
528
+ }
529
+ }
530
+ appendMarkdownFooter(lines);
531
+ return `${lines.join("\n").trimEnd()}
532
+ `;
533
+ }
534
+ function appendMarkdownFooter(lines) {
535
+ lines.push("", "## Recommended Next Steps", "");
536
+ lines.push("- Fix critical issues first.");
537
+ lines.push("- Review warnings before launch.");
538
+ lines.push("- Re-run Qodfy after changes.");
539
+ lines.push("- Use `qodfy prompt <issue-id>` for focused AI repair prompts.");
540
+ lines.push("", "## Generated by Qodfy", "");
541
+ lines.push("Qodfy scans locally and does not print secret values in reports.");
542
+ }
543
+ function appendMarkdownIssue(lines, issue) {
544
+ lines.push(`### ${issue.title}`);
545
+ lines.push("");
546
+ lines.push(`- ID: \`${issue.id}\``);
547
+ lines.push(`- Severity: ${issue.severity}`);
548
+ lines.push(`- Confidence: ${issue.confidence}`);
549
+ lines.push(`- Category: ${categoryLabels[issue.category]}`);
550
+ if (issue.file) {
551
+ lines.push(`- File: \`${issue.file}\``);
552
+ }
553
+ lines.push("");
554
+ lines.push("#### What Qodfy found");
555
+ lines.push("");
556
+ lines.push(issue.message);
557
+ lines.push("");
558
+ lines.push("#### Why it matters");
559
+ lines.push("");
560
+ lines.push(getWhyItMatters(issue));
561
+ lines.push("");
562
+ lines.push("#### Evidence");
563
+ lines.push("");
564
+ appendMarkdownEvidence(lines, issue.evidence);
565
+ lines.push("");
566
+ lines.push("#### Context");
567
+ lines.push("");
568
+ appendMarkdownEvidence(lines, issue.context);
569
+ lines.push("");
570
+ lines.push("#### Suggested fix");
571
+ lines.push("");
572
+ lines.push(issue.suggestion ?? "No specific suggestion provided for this rule yet.");
573
+ lines.push("");
574
+ lines.push("#### AI Fix Prompt");
575
+ lines.push("");
576
+ lines.push("```txt");
577
+ lines.push(issue.fixPrompt ?? "No AI fix prompt available for this rule yet.");
578
+ lines.push("```");
579
+ lines.push("");
580
+ lines.push("#### After fixing, test this");
581
+ lines.push("");
582
+ for (const test of getAfterFixTests(issue)) {
583
+ lines.push(`- ${test}`);
584
+ }
585
+ lines.push("");
586
+ }
587
+ function appendMarkdownEvidence(lines, items) {
588
+ if (!items || items.length === 0) {
589
+ lines.push("- None");
590
+ return;
591
+ }
592
+ for (const item of items) {
593
+ const detail = item.detail ? `: ${item.detail}` : "";
594
+ lines.push(`- ${item.label}${detail}`);
595
+ }
596
+ }
597
+ function getStatusLabel(score) {
598
+ if (score >= 90) {
599
+ return "Ready with minor review";
600
+ }
601
+ if (score >= 75) {
602
+ return "Almost ready";
603
+ }
604
+ if (score >= 50) {
605
+ return "Needs fixes before launch";
606
+ }
607
+ return "Not ready to launch";
608
+ }
609
+ function getSeverityHeading(severity, count) {
610
+ const noun = severity === "critical" ? "Critical" : severity === "warning" ? "Warnings" : "Info";
611
+ return `${noun} (${count})`;
612
+ }
613
+ function getExecutiveSummary(report) {
614
+ const issues = report.issues;
615
+ const criticalCount = countIssuesBySeverity(issues, "critical");
616
+ const warningCount = countIssuesBySeverity(issues, "warning");
617
+ const topRiskCategory = getTopRiskCategory(issues);
618
+ const sentences = [];
619
+ if (criticalCount > 0) {
620
+ const issueWord = criticalCount === 1 ? "critical issue" : "critical issues";
621
+ sentences.push(
622
+ `Qodfy found ${criticalCount} ${issueWord} that should be fixed before launch.`
623
+ );
624
+ } else if (warningCount > 0) {
625
+ sentences.push(
626
+ "Qodfy found no critical blockers. Review the warnings below before launch."
627
+ );
628
+ } else if (issues.length > 0) {
629
+ sentences.push(
630
+ "Qodfy found no critical or warning blockers. Review the info notes below before launch."
631
+ );
632
+ } else {
633
+ sentences.push(
634
+ "Qodfy did not flag any issues. Do a final manual review and you should be good to launch."
635
+ );
636
+ }
637
+ if (topRiskCategory) {
638
+ sentences.push(`The main area to review is ${topRiskCategory}.`);
639
+ }
640
+ sentences.push(
641
+ `Score: ${report.score}/100. Launch status: ${getStatusLabel(report.score)}.`
642
+ );
643
+ return sentences.join(" ");
644
+ }
645
+ function getTopRiskCategory(issues) {
646
+ if (issues.length === 0) {
647
+ return null;
648
+ }
649
+ const weights = /* @__PURE__ */ new Map();
650
+ for (const issue of issues) {
651
+ const weight = issue.severity === "critical" ? 1e3 : issue.severity === "warning" ? 10 : 1;
652
+ weights.set(issue.category, (weights.get(issue.category) ?? 0) + weight);
653
+ }
654
+ let topCategory = null;
655
+ let topWeight = 0;
656
+ let topOrder = Number.MAX_SAFE_INTEGER;
657
+ for (const [category, weight] of weights) {
658
+ const order = categoryOrder.indexOf(category);
659
+ if (weight > topWeight || weight === topWeight && order < topOrder) {
660
+ topWeight = weight;
661
+ topCategory = category;
662
+ topOrder = order;
663
+ }
664
+ }
665
+ if (!topCategory) {
666
+ return null;
667
+ }
668
+ return getCategoryShortName(topCategory);
669
+ }
670
+ function getCategoryShortName(category) {
671
+ if (category === "security") {
672
+ return "security";
673
+ }
674
+ if (category === "api") {
675
+ return "API routes";
676
+ }
677
+ if (category === "webhook") {
678
+ return "webhooks";
679
+ }
680
+ if (category === "ai") {
681
+ return "AI routes";
682
+ }
683
+ if (category === "environment") {
684
+ return "environment variables";
685
+ }
686
+ if (category === "maintainability") {
687
+ return "maintainability";
688
+ }
689
+ return "project setup";
690
+ }
691
+ function getWhatLooksGood(report) {
692
+ const observations = [];
693
+ const issues = report.issues;
694
+ const checks = new Set(report.checks);
695
+ const criticalCount = countIssuesBySeverity(issues, "critical");
696
+ if (criticalCount === 0) {
697
+ observations.push("No critical issues found.");
698
+ }
699
+ observations.push("Local source scan completed successfully.");
700
+ if (checks.has("api") && report.stats.apiRoutes > 0) {
701
+ observations.push("API routes were analyzed method by method.");
702
+ }
703
+ const hasPublicReadRouteNote = issues.some(
704
+ (issue) => issue.ruleId === "api-public-read-route"
705
+ );
706
+ if (hasPublicReadRouteNote) {
707
+ observations.push("Public read routes were separated from protected routes.");
708
+ }
709
+ if (checks.has("environment")) {
710
+ const hasEnvIssue = issues.some((issue) => issue.category === "environment");
711
+ if (!hasEnvIssue) {
712
+ observations.push("Environment variable documentation looks complete.");
713
+ }
714
+ }
715
+ if (checks.has("webhook")) {
716
+ const hasWebhookSignatureIssue = issues.some(
717
+ (issue) => issue.ruleId === "webhook-missing-signature-verification"
718
+ );
719
+ if (!hasWebhookSignatureIssue) {
720
+ observations.push("No webhook routes were flagged for missing signature checks.");
721
+ }
722
+ }
723
+ if (checks.has("ai")) {
724
+ const hasAiRateIssue = issues.some(
725
+ (issue) => issue.ruleId === "ai-route-missing-rate-limit"
726
+ );
727
+ if (!hasAiRateIssue && report.stats.aiFiles > 0) {
728
+ observations.push("AI routes were not flagged as missing rate limiting.");
729
+ }
730
+ }
731
+ if (checks.has("security")) {
732
+ const hasSecretIssue = issues.some(
733
+ (issue) => issue.ruleId === "security-hardcoded-secret" || issue.ruleId === "security-client-side-secret"
734
+ );
735
+ if (!hasSecretIssue) {
736
+ observations.push("No hardcoded or client-side secrets were detected.");
737
+ }
738
+ }
739
+ observations.push("Qodfy did not print any secret values in this report.");
740
+ return observations;
741
+ }
742
+ function getWhyItMatters(issue) {
743
+ const ruleExplanation = getWhyItMattersByRuleId(issue.ruleId);
744
+ if (ruleExplanation) {
745
+ return ruleExplanation;
746
+ }
747
+ return getWhyItMattersByCategory(issue.category);
748
+ }
749
+ function getWhyItMattersByRuleId(ruleId) {
750
+ switch (ruleId) {
751
+ case "admin-route-missing-authorization":
752
+ return "This route is authenticated, but admin/debug/private routes often need an extra role or permission check. A normal logged-in user should not be able to access admin-only data or tools.";
753
+ case "public-form-missing-abuse-protection":
754
+ return "Public forms can be abused by bots or repeated submissions. Validation helps data quality, but rate limiting or spam protection helps prevent abuse.";
755
+ case "webhook-missing-signature-verification":
756
+ return "Webhook routes receive external events. Without signature verification, attackers may be able to fake events and trick your app into running real actions.";
757
+ case "environment-variable-missing-from-example":
758
+ return "Missing env documentation makes the project harder to deploy or maintain. Future developers (and future you) will not know which variables are required.";
759
+ case "environment-missing-env-example":
760
+ return "Without a .env.example, anyone setting up this project has to guess which environment variables are required. That guesswork leads to broken deploys and runtime errors.";
761
+ case "ai-route-missing-rate-limit":
762
+ return "AI routes can create real API costs. Rate limiting helps control abuse and unexpected spend, especially if a route is exposed to anonymous users.";
763
+ case "security-hardcoded-secret":
764
+ return "Hardcoded secrets can leak through git history, public deploys, or AI tools that read your code. Rotate any real values and load them from environment variables instead.";
765
+ case "security-client-side-secret":
766
+ return "Secrets used in client-side code are visible to anyone who opens the browser devtools. Move sensitive values and logic to server-only code.";
767
+ case "sensitive-api-route-missing-auth":
768
+ return "API routes that handle sensitive data should verify the caller is authenticated before reading or writing. Without that check, anonymous users may access protected information.";
769
+ case "api-mutation-route-review-auth":
770
+ return "Routes that mutate data without an obvious auth check can let anyone create, update, or delete records. Review the auth path before launch.";
771
+ case "internal-route-missing-protection":
772
+ return "Internal or operational routes (debug, admin, jobs) are often forgotten before launch. Without protection they can expose admin-only behavior to the public.";
773
+ case "api-public-read-route":
774
+ return "This is a public read route. Confirm the data exposed here is safe to share with anonymous visitors and that no sensitive fields are leaking.";
775
+ case "maintainability-large-file":
776
+ return "Large files are harder to review, refactor, and test. Splitting them now reduces future cleanup cost and makes AI tools more reliable on this codebase.";
777
+ case "maintainability-large-file-skipped":
778
+ return "Some files were too large for Qodfy to read fully. Review them manually before launch in case they hide other issues.";
779
+ case "maintainability-file-unreadable":
780
+ return "Files Qodfy could not read may indicate encoding, permission, or generated-output issues that hide real problems from this scan.";
781
+ case "project-missing-package-json":
782
+ return "Without a package.json, Qodfy cannot fully reason about your project. Confirm Qodfy is pointed at the correct project root.";
783
+ case "project-invalid-package-json":
784
+ return "An invalid package.json blocks tooling, installs, and deploys. Fix the JSON before running other launch checks.";
785
+ case "project-next-not-detected":
786
+ return "Qodfy is currently optimized for Next.js apps. If this is a Next.js project, double-check dependencies and folder structure before launch.";
787
+ case "project-missing-readme":
788
+ return "A short README helps anyone (including future you) understand what this project does and how to run it.";
789
+ case "project-source-files-unreadable":
790
+ return "Some source files could not be read. Review them manually so the scan does not silently skip risky areas of the code.";
791
+ default:
792
+ return null;
793
+ }
794
+ }
795
+ function getWhyItMattersByCategory(category) {
796
+ switch (category) {
797
+ case "security":
798
+ return "Security risks can leak data, expose secrets, or grant unwanted access to your app. These should be the first issues you review before launch.";
799
+ case "api":
800
+ return "API routes shape what your app exposes to the internet. Confirm that authentication, input validation, and abuse protection match what each route does.";
801
+ case "webhook":
802
+ return "Webhooks receive events from external systems. Verifying signatures prevents fake or replayed events from being trusted by your app.";
803
+ case "ai":
804
+ return "AI routes have unique cost and abuse risks. Rate limits and usage caps protect your spend and keep the service usable for real users.";
805
+ case "environment":
806
+ return "Environment variables control how your app connects to external services. Missing or undocumented variables cause deploy failures and runtime errors.";
807
+ case "maintainability":
808
+ return "Maintainability signals do not block launch but slow future work. Cleaning them up early reduces friction later.";
809
+ case "project":
810
+ return "Project setup signals affect tooling, deploys, and onboarding. Fixing them early prevents surprises later in the launch.";
811
+ }
812
+ }
813
+ function getAfterFixTests(issue) {
814
+ const ruleTests = getAfterFixTestsByRuleId(issue.ruleId);
815
+ if (ruleTests) {
816
+ return ruleTests;
817
+ }
818
+ return getDefaultAfterFixTests();
819
+ }
820
+ function getAfterFixTestsByRuleId(ruleId) {
821
+ switch (ruleId) {
822
+ case "admin-route-missing-authorization":
823
+ return [
824
+ "Test as an unauthenticated user.",
825
+ "Test as a normal logged-in user.",
826
+ "Test as an admin/staff user.",
827
+ "Confirm non-admin users receive 403."
828
+ ];
829
+ case "public-form-missing-abuse-protection":
830
+ return [
831
+ "Submit a valid form and confirm it still works.",
832
+ "Submit invalid input and confirm validation returns 400.",
833
+ "Send repeated requests and confirm rate limiting or spam protection behavior.",
834
+ "Confirm no sensitive provider/API error details are exposed."
835
+ ];
836
+ case "webhook-missing-signature-verification":
837
+ return [
838
+ "Test a valid signed webhook.",
839
+ "Test an invalid signature.",
840
+ "Confirm invalid requests are rejected before processing.",
841
+ "Confirm the event is not processed twice."
842
+ ];
843
+ case "environment-missing-env-example":
844
+ case "environment-variable-missing-from-example":
845
+ return [
846
+ "Add the variable name to .env.example without a real value.",
847
+ "Run the app locally with required env values.",
848
+ "Confirm deployment docs or hosting env vars are updated."
849
+ ];
850
+ case "ai-route-missing-rate-limit":
851
+ return [
852
+ "Send a normal AI request.",
853
+ "Send repeated requests and confirm rate limiting.",
854
+ "Confirm rejected requests do not call the AI provider.",
855
+ "Check logs/cost controls after testing."
856
+ ];
857
+ case "sensitive-api-route-missing-auth":
858
+ case "api-mutation-route-review-auth":
859
+ return [
860
+ "Test unauthenticated access.",
861
+ "Test authenticated access.",
862
+ "Confirm unauthorized users receive 401 or 403.",
863
+ "Confirm existing response formats still work."
864
+ ];
865
+ case "internal-route-missing-protection":
866
+ return [
867
+ "Test without the secret/auth guard.",
868
+ "Test with a valid secret/auth guard.",
869
+ "Confirm invalid requests are rejected before internal work runs."
870
+ ];
871
+ case "security-hardcoded-secret":
872
+ case "security-client-side-secret":
873
+ return [
874
+ "Rotate any real secret values that were exposed.",
875
+ "Confirm the secret is loaded from environment variables, not source code.",
876
+ "Re-run Qodfy and confirm the issue no longer appears.",
877
+ "Check git history and remove any committed real values if needed."
878
+ ];
879
+ case "api-public-read-route":
880
+ return [
881
+ "Confirm the data exposed here is safe to share with anonymous users.",
882
+ "Test the route as an anonymous visitor.",
883
+ "Confirm no sensitive fields leak in the response."
884
+ ];
885
+ default:
886
+ return null;
887
+ }
888
+ }
889
+ function getDefaultAfterFixTests() {
890
+ return [
891
+ "Re-run Qodfy after the fix.",
892
+ "Test the affected route or file manually.",
893
+ "Confirm existing behavior still works.",
894
+ "Check logs for unexpected errors."
895
+ ];
896
+ }
383
897
  function printReport(report, maxIssues, showPrompts, scanModeLabel, showDetails, showAllIssues) {
384
898
  console.log(pc.bold("Qodfy Report"));
385
899
  console.log("");
@@ -539,14 +1053,23 @@ Run qodfy scan --all to see the current issue IDs.`
539
1053
  }
540
1054
  printFixPrompt(issue);
541
1055
  }
1056
+ function getSeverityText(severity) {
1057
+ if (severity === "critical") {
1058
+ return "CRITICAL";
1059
+ }
1060
+ if (severity === "warning") {
1061
+ return "WARNING";
1062
+ }
1063
+ return "INFO";
1064
+ }
542
1065
  function getSeverityLabel(severity) {
543
1066
  if (severity === "critical") {
544
- return pc.red("CRITICAL");
1067
+ return pc.red(getSeverityText(severity));
545
1068
  }
546
1069
  if (severity === "warning") {
547
- return pc.yellow("WARNING");
1070
+ return pc.yellow(getSeverityText(severity));
548
1071
  }
549
- return pc.blue("INFO");
1072
+ return pc.blue(getSeverityText(severity));
550
1073
  }
551
1074
  function countIssuesBySeverity(issues, severity) {
552
1075
  return issues.filter((issue) => issue.severity === severity).length;
@@ -663,13 +1186,13 @@ function shellQuote(value) {
663
1186
  }
664
1187
  return `'${value.replaceAll("'", "'\\''")}'`;
665
1188
  }
666
- function printScanError(reason) {
667
- console.error(pc.red("Qodfy could not scan this project."));
1189
+ function printScanError(reason, useColor = true) {
1190
+ console.error(useColor ? pc.red("Qodfy could not scan this project.") : "Qodfy could not scan this project.");
668
1191
  console.error("");
669
- console.error(pc.bold("Reason:"));
1192
+ console.error(useColor ? pc.bold("Reason:") : "Reason:");
670
1193
  console.error(reason);
671
1194
  console.error("");
672
- console.error(pc.bold("Try:"));
1195
+ console.error(useColor ? pc.bold("Try:") : "Try:");
673
1196
  console.error("qodfy scan --path ./my-next-app");
674
1197
  }
675
1198
  function printPromptError(reason) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qodfy",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "description": "Open-source launch readiness scanner for AI-built apps.",
5
5
  "keywords": [
6
6
  "qodfy",