qodfy 0.1.6 → 0.2.1

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 (2) hide show
  1. package/dist/index.js +407 -20
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -3,12 +3,18 @@
3
3
  // src/index.ts
4
4
  import fs from "fs/promises";
5
5
  import path from "path";
6
+ import { checkbox, select } from "@inquirer/prompts";
6
7
  import { Command } from "commander";
7
8
  import pc from "picocolors";
8
- import { scanProject } from "@qodfy/core";
9
+ import {
10
+ recommendedScanChecks,
11
+ scanProject,
12
+ validScanChecks
13
+ } from "@qodfy/core";
14
+ var DEFAULT_MAX_ISSUES = 20;
9
15
  var program = new Command();
10
- program.name("qodfy").description("Launch readiness scanner for AI-built apps.").version("0.1.6");
11
- 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", "50").action(async (options) => {
16
+ program.name("qodfy").description("Launch readiness scanner for AI-built apps.").version("0.2.1");
17
+ 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("--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) => {
12
18
  const pathResult = await resolveProjectPath(options.path);
13
19
  if (!pathResult.ok) {
14
20
  printScanError(pathResult.reason);
@@ -16,15 +22,260 @@ program.command("scan").description("Scan a project for launch readiness issues.
16
22
  return;
17
23
  }
18
24
  try {
25
+ const scanModeResult = await resolveScanMode(options);
26
+ if (!scanModeResult.ok) {
27
+ printScanError(scanModeResult.reason);
28
+ process.exitCode = 1;
29
+ return;
30
+ }
31
+ if (scanModeResult.notice) {
32
+ console.log(pc.dim(scanModeResult.notice));
33
+ console.log("");
34
+ }
19
35
  console.log(pc.cyan("Qodfy is scanning your project...\n"));
20
- const report = await scanProject(pathResult.projectPath);
21
- printReport(report, parseMaxIssues(options.maxIssues));
36
+ const report = await scanProject({
37
+ projectPath: pathResult.projectPath,
38
+ checks: scanModeResult.checks
39
+ });
40
+ printReport(
41
+ report,
42
+ parseMaxIssues(options.maxIssues),
43
+ Boolean(options.prompts),
44
+ scanModeResult.label
45
+ );
22
46
  } catch (error) {
23
- printScanError(getErrorMessage(error));
47
+ if (isPromptCancelError(error)) {
48
+ console.log("Scan cancelled.");
49
+ } else {
50
+ printScanError(getErrorMessage(error));
51
+ }
24
52
  process.exitCode = 1;
25
53
  }
26
54
  });
55
+ var categoryOrder = [
56
+ "security",
57
+ "webhook",
58
+ "ai",
59
+ "api",
60
+ "environment",
61
+ "maintainability",
62
+ "project"
63
+ ];
64
+ var categoryLabels = {
65
+ security: "Security",
66
+ api: "API",
67
+ webhook: "Webhooks",
68
+ ai: "AI",
69
+ environment: "Environment",
70
+ maintainability: "Maintainability",
71
+ project: "Project"
72
+ };
27
73
  await program.parseAsync();
74
+ async function resolveScanMode(options) {
75
+ if (options.checks) {
76
+ const parsedChecks = parseChecks(options.checks);
77
+ if (!parsedChecks.ok) {
78
+ return parsedChecks;
79
+ }
80
+ return {
81
+ ok: true,
82
+ checks: parsedChecks.checks,
83
+ label: getScanModeLabel(parsedChecks.checks)
84
+ };
85
+ }
86
+ if (options.all) {
87
+ return {
88
+ ok: true,
89
+ checks: [...validScanChecks],
90
+ label: "All checks"
91
+ };
92
+ }
93
+ if (options.interactive === false) {
94
+ return {
95
+ ok: true,
96
+ checks: [...recommendedScanChecks],
97
+ label: "Recommended launch scan"
98
+ };
99
+ }
100
+ if (isNonInteractiveTerminal()) {
101
+ return {
102
+ ok: true,
103
+ checks: [...recommendedScanChecks],
104
+ label: "Recommended launch scan",
105
+ notice: "Running recommended scan in non-interactive mode."
106
+ };
107
+ }
108
+ return promptForScanMode();
109
+ }
110
+ async function promptForScanMode() {
111
+ console.log(pc.bold("Qodfy Scan"));
112
+ console.log("");
113
+ const mode = await select({
114
+ message: "Choose scan mode:",
115
+ choices: [
116
+ {
117
+ name: "Recommended launch scan",
118
+ value: "recommended",
119
+ description: "Project setup, API routes, environment, AI, webhooks, and maintainability"
120
+ },
121
+ {
122
+ name: "Security & API routes",
123
+ value: "security-api",
124
+ description: "API authentication, client-side secrets, hardcoded secrets, and webhooks"
125
+ },
126
+ {
127
+ name: "Environment variables",
128
+ value: "environment",
129
+ description: ".env.example and process.env documentation"
130
+ },
131
+ {
132
+ name: "AI route cost risks",
133
+ value: "ai",
134
+ description: "AI-related routes that may need rate limits or usage limits"
135
+ },
136
+ {
137
+ name: "Webhooks",
138
+ value: "webhook",
139
+ description: "Webhook signature verification"
140
+ },
141
+ {
142
+ name: "Maintainability",
143
+ value: "maintainability",
144
+ description: "Large files and maintainability signals"
145
+ },
146
+ {
147
+ name: "Custom selection",
148
+ value: "custom",
149
+ description: "Choose exactly which checks to run"
150
+ }
151
+ ]
152
+ });
153
+ if (mode === "custom") {
154
+ const checks = await checkbox({
155
+ message: "Select checks to run:",
156
+ required: true,
157
+ choices: [
158
+ { name: "Project setup", value: "project" },
159
+ { name: "API route authentication", value: "api", checked: true },
160
+ { name: "Environment variables", value: "environment", checked: true },
161
+ { name: "AI route cost risks", value: "ai" },
162
+ { name: "Webhooks", value: "webhook" },
163
+ { name: "Maintainability / large files", value: "maintainability" }
164
+ ]
165
+ });
166
+ return {
167
+ ok: true,
168
+ checks,
169
+ label: `Custom selection: ${checks.join(", ")}`
170
+ };
171
+ }
172
+ return {
173
+ ok: true,
174
+ checks: getChecksForMode(mode),
175
+ label: getScanModeName(mode)
176
+ };
177
+ }
178
+ function getChecksForMode(mode) {
179
+ if (mode === "recommended") {
180
+ return [...recommendedScanChecks];
181
+ }
182
+ if (mode === "security-api") {
183
+ return ["api", "security", "webhook"];
184
+ }
185
+ if (mode === "environment") {
186
+ return ["environment"];
187
+ }
188
+ if (mode === "ai") {
189
+ return ["ai"];
190
+ }
191
+ if (mode === "webhook") {
192
+ return ["webhook"];
193
+ }
194
+ return ["maintainability"];
195
+ }
196
+ function getScanModeName(mode) {
197
+ if (mode === "recommended") {
198
+ return "Recommended launch scan";
199
+ }
200
+ if (mode === "security-api") {
201
+ return "Security & API routes";
202
+ }
203
+ if (mode === "environment") {
204
+ return "Environment variables";
205
+ }
206
+ if (mode === "ai") {
207
+ return "AI route cost risks";
208
+ }
209
+ if (mode === "webhook") {
210
+ return "Webhooks";
211
+ }
212
+ return "Maintainability";
213
+ }
214
+ function parseChecks(checks) {
215
+ const selectedChecks = [...new Set(
216
+ checks.split(",").map((check) => check.trim().toLowerCase()).filter(Boolean)
217
+ )];
218
+ if (selectedChecks.length === 0) {
219
+ return {
220
+ ok: false,
221
+ reason: `No checks were provided. Valid checks: ${validScanChecks.join(", ")}.`
222
+ };
223
+ }
224
+ const invalidChecks = selectedChecks.filter((check) => !isScanCheck(check));
225
+ if (invalidChecks.length > 0) {
226
+ return {
227
+ ok: false,
228
+ reason: `Invalid check${invalidChecks.length === 1 ? "" : "s"}: ${invalidChecks.join(", ")}.
229
+ Valid checks: ${validScanChecks.join(", ")}.`
230
+ };
231
+ }
232
+ return {
233
+ ok: true,
234
+ checks: selectedChecks.filter(isScanCheck)
235
+ };
236
+ }
237
+ function isScanCheck(check) {
238
+ return validScanChecks.includes(check);
239
+ }
240
+ function getScanModeLabel(checks) {
241
+ if (hasSameChecks(checks, recommendedScanChecks)) {
242
+ return "Recommended launch scan";
243
+ }
244
+ if (hasSameChecks(checks, validScanChecks)) {
245
+ return "All checks";
246
+ }
247
+ if (checks.length === 1) {
248
+ const check = checks[0];
249
+ if (check === "environment") {
250
+ return "Environment variables";
251
+ }
252
+ if (check === "api") {
253
+ return "API route authentication";
254
+ }
255
+ if (check === "ai") {
256
+ return "AI route cost risks";
257
+ }
258
+ if (check === "webhook") {
259
+ return "Webhooks";
260
+ }
261
+ if (check === "maintainability") {
262
+ return "Maintainability";
263
+ }
264
+ if (check === "project") {
265
+ return "Project setup";
266
+ }
267
+ return "Security";
268
+ }
269
+ return `Custom selection: ${checks.join(", ")}`;
270
+ }
271
+ function hasSameChecks(leftChecks, rightChecks) {
272
+ const leftSet = new Set(leftChecks);
273
+ const rightSet = new Set(rightChecks);
274
+ return leftSet.size === rightSet.size && [...leftSet].every((check) => rightSet.has(check));
275
+ }
276
+ function isNonInteractiveTerminal() {
277
+ return Boolean(process.env.CI) || !process.stdin.isTTY || !process.stdout.isTTY;
278
+ }
28
279
  async function resolveProjectPath(projectPath) {
29
280
  const inputPath = projectPath.trim() || process.cwd();
30
281
  const resolvedPath = path.resolve(inputPath);
@@ -60,11 +311,12 @@ async function resolveProjectPath(projectPath) {
60
311
  };
61
312
  }
62
313
  }
63
- function printReport(report, maxIssues) {
314
+ function printReport(report, maxIssues, showPrompts, scanModeLabel) {
64
315
  console.log(pc.bold("Qodfy Report"));
65
316
  console.log("");
66
317
  const scoreColor = report.score >= 80 ? pc.green : report.score >= 60 ? pc.yellow : pc.red;
67
318
  console.log(`Launch Readiness: ${scoreColor(`${report.score}/100`)}`);
319
+ console.log(`Scan mode: ${scanModeLabel}`);
68
320
  console.log("");
69
321
  console.log(pc.bold("Stats"));
70
322
  console.log(`Files scanned: ${report.stats.totalFiles}`);
@@ -73,31 +325,163 @@ function printReport(report, maxIssues) {
73
325
  console.log(`Large files: ${report.stats.largeFiles}`);
74
326
  console.log(`Scan duration: ${formatDuration(report.stats.durationMs)}`);
75
327
  console.log("");
328
+ printSummary(report.issues);
76
329
  if (report.issues.length === 0) {
77
330
  console.log(pc.green("No issues found. Your project looks clean."));
78
331
  return;
79
332
  }
80
333
  console.log(pc.bold("Issues"));
81
- const issuesToShow = report.issues.slice(0, maxIssues);
334
+ const displayIssues = getSortedDisplayIssues(report.issues);
335
+ const issuesToShow = displayIssues.slice(0, maxIssues);
82
336
  if (report.issues.length > maxIssues) {
83
337
  console.log(`Showing ${maxIssues} of ${report.issues.length} issues.`);
84
338
  console.log(`Use --max-issues <number> to show more.`);
85
339
  }
86
- for (const issue of issuesToShow) {
87
- const label = issue.severity === "critical" ? pc.red("CRITICAL") : issue.severity === "warning" ? pc.yellow("WARNING") : pc.blue("INFO");
88
- console.log(`
89
- ${label} ${pc.bold(issue.title)}`);
90
- console.log(issue.message);
91
- if (issue.file) {
92
- console.log(pc.dim(`File: ${issue.file}`));
340
+ printGroupedIssues(issuesToShow, showPrompts);
341
+ console.log("");
342
+ console.log(pc.bold("Recommended next step:"));
343
+ console.log("Fix critical issues first, then warnings, then cleanup items.");
344
+ console.log("");
345
+ console.log(pc.bold("Next commands:"));
346
+ console.log("qodfy scan --checks api,environment");
347
+ console.log("qodfy scan --prompts --max-issues 5");
348
+ }
349
+ function printSummary(issues) {
350
+ const criticalCount = countIssuesBySeverity(issues, "critical");
351
+ const warningCount = countIssuesBySeverity(issues, "warning");
352
+ const infoCount = countIssuesBySeverity(issues, "info");
353
+ console.log(pc.bold("Summary"));
354
+ console.log(`Critical: ${criticalCount}`);
355
+ console.log(`Warnings: ${warningCount}`);
356
+ console.log(`Info: ${infoCount}`);
357
+ console.log("");
358
+ if (issues.length === 0) {
359
+ return;
360
+ }
361
+ console.log(pc.bold("Categories"));
362
+ for (const category of categoryOrder) {
363
+ const count = issues.filter((issue) => issue.category === category).length;
364
+ if (count > 0) {
365
+ console.log(`${categoryLabels[category]}: ${count}`);
366
+ }
367
+ }
368
+ const priorities = getTopPriorities(issues);
369
+ if (priorities.length > 0) {
370
+ console.log("");
371
+ console.log(pc.bold("Top priorities"));
372
+ for (const [index, priority] of priorities.entries()) {
373
+ console.log(`${index + 1}. ${priority}`);
374
+ }
375
+ }
376
+ console.log("");
377
+ }
378
+ function printGroupedIssues(issues, showPrompts) {
379
+ for (const category of categoryOrder) {
380
+ const categoryIssues = issues.filter((issue) => issue.category === category);
381
+ if (categoryIssues.length === 0) {
382
+ continue;
93
383
  }
94
- if (issue.suggestion) {
95
- console.log(pc.dim(`Suggestion: ${issue.suggestion}`));
384
+ console.log("");
385
+ console.log(pc.bold(categoryLabels[category]));
386
+ for (const issue of categoryIssues) {
387
+ printIssue(issue, showPrompts);
96
388
  }
97
389
  }
390
+ }
391
+ function printIssue(issue, showPrompts) {
98
392
  console.log("");
99
- console.log(pc.bold("Recommended next step:"));
100
- console.log("Fix critical issues first, then warnings, then cleanup items.");
393
+ console.log(`${pc.dim(`[${issue.id}]`)} ${getSeverityLabel(issue.severity)} ${pc.bold(issue.title)}`);
394
+ console.log(issue.message);
395
+ if (issue.file) {
396
+ console.log(pc.dim(`File: ${issue.file}`));
397
+ }
398
+ if (issue.suggestion) {
399
+ console.log(pc.dim(`Suggestion: ${issue.suggestion}`));
400
+ }
401
+ if (showPrompts && issue.fixPrompt) {
402
+ console.log("");
403
+ console.log(pc.bold("Fix Prompt:"));
404
+ console.log(issue.fixPrompt);
405
+ }
406
+ }
407
+ function getSeverityLabel(severity) {
408
+ if (severity === "critical") {
409
+ return pc.red("CRITICAL");
410
+ }
411
+ if (severity === "warning") {
412
+ return pc.yellow("WARNING");
413
+ }
414
+ return pc.blue("INFO");
415
+ }
416
+ function countIssuesBySeverity(issues, severity) {
417
+ return issues.filter((issue) => issue.severity === severity).length;
418
+ }
419
+ function getSortedDisplayIssues(issues) {
420
+ return [...issues].sort((leftIssue, rightIssue) => {
421
+ return getSeverityRank(leftIssue.severity) - getSeverityRank(rightIssue.severity) || categoryOrder.indexOf(leftIssue.category) - categoryOrder.indexOf(rightIssue.category) || leftIssue.ruleId.localeCompare(rightIssue.ruleId) || getIssueNumber(leftIssue.id) - getIssueNumber(rightIssue.id) || (leftIssue.file ?? "").localeCompare(rightIssue.file ?? "") || leftIssue.id.localeCompare(rightIssue.id);
422
+ });
423
+ }
424
+ function getIssueNumber(issueId) {
425
+ const match = issueId.match(/-(\d+)$/);
426
+ return match ? Number.parseInt(match[1], 10) : 0;
427
+ }
428
+ function getSeverityRank(severity) {
429
+ if (severity === "critical") {
430
+ return 0;
431
+ }
432
+ if (severity === "warning") {
433
+ return 1;
434
+ }
435
+ return 2;
436
+ }
437
+ function getTopPriorities(issues) {
438
+ const priorities = [
439
+ {
440
+ ruleIds: ["security-hardcoded-secret"],
441
+ message: "Remove possible hardcoded secrets and rotate any real exposed values."
442
+ },
443
+ {
444
+ ruleIds: ["webhook-missing-signature-verification"],
445
+ message: "Verify webhook signatures before handling external events."
446
+ },
447
+ {
448
+ ruleIds: ["ai-route-missing-rate-limit"],
449
+ message: "Add cost and abuse protection to AI-related API routes."
450
+ },
451
+ {
452
+ ruleIds: ["security-client-side-secret"],
453
+ message: "Move possible server-only secrets out of client-side code."
454
+ },
455
+ {
456
+ ruleIds: ["api-route-missing-auth"],
457
+ message: "Review API routes that may be missing authentication."
458
+ },
459
+ {
460
+ ruleIds: [
461
+ "environment-missing-env-example",
462
+ "environment-variable-missing-from-example"
463
+ ],
464
+ message: "Add missing environment variables to .env.example."
465
+ },
466
+ {
467
+ ruleIds: [
468
+ "maintainability-large-file",
469
+ "maintainability-large-file-skipped"
470
+ ],
471
+ message: "Review large files for maintainability before launch."
472
+ },
473
+ {
474
+ ruleIds: [
475
+ "project-missing-package-json",
476
+ "project-invalid-package-json",
477
+ "project-next-not-detected"
478
+ ],
479
+ message: "Confirm Qodfy is scanning the correct project root."
480
+ }
481
+ ];
482
+ return priorities.filter(
483
+ (priority) => issues.some((issue) => priority.ruleIds.includes(issue.ruleId))
484
+ ).slice(0, 3).map((priority) => priority.message);
101
485
  }
102
486
  function printScanError(reason) {
103
487
  console.error(pc.red("Qodfy could not scan this project."));
@@ -114,10 +498,13 @@ function getErrorMessage(error) {
114
498
  }
115
499
  return "An unexpected error occurred while scanning the project.";
116
500
  }
501
+ function isPromptCancelError(error) {
502
+ return error instanceof Error && (error.name === "ExitPromptError" || error.message.includes("User force closed the prompt"));
503
+ }
117
504
  function parseMaxIssues(maxIssues) {
118
505
  const parsedMaxIssues = Number.parseInt(maxIssues, 10);
119
506
  if (!Number.isFinite(parsedMaxIssues) || parsedMaxIssues <= 0) {
120
- return 50;
507
+ return DEFAULT_MAX_ISSUES;
121
508
  }
122
509
  return parsedMaxIssues;
123
510
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qodfy",
3
- "version": "0.1.6",
3
+ "version": "0.2.1",
4
4
  "description": "Open-source launch readiness scanner for AI-built apps.",
5
5
  "keywords": [
6
6
  "qodfy",
@@ -49,9 +49,10 @@
49
49
  "access": "public"
50
50
  },
51
51
  "dependencies": {
52
+ "@inquirer/prompts": "^8.4.3",
52
53
  "commander": "^14.0.3",
53
54
  "picocolors": "^1.1.1",
54
- "@qodfy/core": "^0.1.6"
55
+ "@qodfy/core": "^0.2.1"
55
56
  },
56
57
  "devDependencies": {
57
58
  "@types/node": "^25.7.0",