qodfy 0.2.0 → 0.2.2

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 +369 -14
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -3,14 +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
9
  import {
9
- scanProject
10
+ recommendedScanChecks,
11
+ scanProject,
12
+ validScanChecks
10
13
  } from "@qodfy/core";
14
+ var DEFAULT_MAX_ISSUES = 20;
11
15
  var program = new Command();
12
- program.name("qodfy").description("Launch readiness scanner for AI-built apps.").version("0.2.0");
13
- 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").option("--prompts", "Show safe copy-paste fix prompts for displayed issues").action(async (options) => {
16
+ program.name("qodfy").description("Launch readiness scanner for AI-built apps.").version("0.2.2");
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) => {
14
18
  const pathResult = await resolveProjectPath(options.path);
15
19
  if (!pathResult.ok) {
16
20
  printScanError(pathResult.reason);
@@ -18,19 +22,74 @@ program.command("scan").description("Scan a project for launch readiness issues.
18
22
  return;
19
23
  }
20
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
+ }
21
35
  console.log(pc.cyan("Qodfy is scanning your project...\n"));
22
- const report = await scanProject(pathResult.projectPath);
23
- printReport(report, parseMaxIssues(options.maxIssues), Boolean(options.prompts));
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
+ );
24
46
  } catch (error) {
25
- printScanError(getErrorMessage(error));
47
+ if (isPromptCancelError(error)) {
48
+ console.log("Scan cancelled.");
49
+ } else {
50
+ printScanError(getErrorMessage(error));
51
+ }
26
52
  process.exitCode = 1;
27
53
  }
28
54
  });
55
+ program.command("prompt <issue-id>").description("Print the safe AI fix prompt for a specific issue.").option("-p, --path <path>", "Project path to scan", process.cwd()).option("--checks <checks>", "Comma-separated checks to search").option("--all", "Search all checks", true).action(async (issueId, options) => {
56
+ const pathResult = await resolveProjectPath(options.path);
57
+ if (!pathResult.ok) {
58
+ printScanError(pathResult.reason);
59
+ process.exitCode = 1;
60
+ return;
61
+ }
62
+ const checksResult = resolvePromptChecks(options);
63
+ if (!checksResult.ok) {
64
+ printPromptError(checksResult.reason);
65
+ process.exitCode = 1;
66
+ return;
67
+ }
68
+ const report = await scanProject({
69
+ projectPath: pathResult.projectPath,
70
+ checks: checksResult.checks
71
+ });
72
+ const issue = report.issues.find((scanIssue) => scanIssue.id === issueId);
73
+ if (!issue) {
74
+ printPromptError(
75
+ `Issue "${issueId}" was not found in this project scan.
76
+ Run qodfy scan --all to see the current issue IDs.`
77
+ );
78
+ process.exitCode = 1;
79
+ return;
80
+ }
81
+ if (!issue.fixPrompt) {
82
+ printPromptError(`Issue "${issueId}" does not have an AI fix prompt yet.`);
83
+ process.exitCode = 1;
84
+ return;
85
+ }
86
+ printFixPrompt(issue);
87
+ });
29
88
  var categoryOrder = [
30
89
  "security",
31
- "api",
32
90
  "webhook",
33
91
  "ai",
92
+ "api",
34
93
  "environment",
35
94
  "maintainability",
36
95
  "project"
@@ -45,6 +104,229 @@ var categoryLabels = {
45
104
  project: "Project"
46
105
  };
47
106
  await program.parseAsync();
107
+ async function resolveScanMode(options) {
108
+ if (options.checks) {
109
+ const parsedChecks = parseChecks(options.checks);
110
+ if (!parsedChecks.ok) {
111
+ return parsedChecks;
112
+ }
113
+ return {
114
+ ok: true,
115
+ checks: parsedChecks.checks,
116
+ label: getScanModeLabel(parsedChecks.checks)
117
+ };
118
+ }
119
+ if (options.all) {
120
+ return {
121
+ ok: true,
122
+ checks: [...validScanChecks],
123
+ label: "All checks"
124
+ };
125
+ }
126
+ if (options.interactive === false) {
127
+ return {
128
+ ok: true,
129
+ checks: [...recommendedScanChecks],
130
+ label: "Recommended launch scan"
131
+ };
132
+ }
133
+ if (isNonInteractiveTerminal()) {
134
+ return {
135
+ ok: true,
136
+ checks: [...recommendedScanChecks],
137
+ label: "Recommended launch scan",
138
+ notice: "Running recommended scan in non-interactive mode."
139
+ };
140
+ }
141
+ return promptForScanMode();
142
+ }
143
+ async function promptForScanMode() {
144
+ console.log(pc.bold("Qodfy Scan"));
145
+ console.log("");
146
+ const mode = await select({
147
+ message: "Choose scan mode:",
148
+ choices: [
149
+ {
150
+ name: "Recommended launch scan",
151
+ value: "recommended",
152
+ description: "Project setup, API routes, environment, AI, webhooks, and maintainability"
153
+ },
154
+ {
155
+ name: "Security & API routes",
156
+ value: "security-api",
157
+ description: "API authentication, client-side secrets, hardcoded secrets, and webhooks"
158
+ },
159
+ {
160
+ name: "Environment variables",
161
+ value: "environment",
162
+ description: ".env.example and process.env documentation"
163
+ },
164
+ {
165
+ name: "AI route cost risks",
166
+ value: "ai",
167
+ description: "AI-related routes that may need rate limits or usage limits"
168
+ },
169
+ {
170
+ name: "Webhooks",
171
+ value: "webhook",
172
+ description: "Webhook signature verification"
173
+ },
174
+ {
175
+ name: "Maintainability",
176
+ value: "maintainability",
177
+ description: "Large files and maintainability signals"
178
+ },
179
+ {
180
+ name: "Custom selection",
181
+ value: "custom",
182
+ description: "Choose exactly which checks to run"
183
+ }
184
+ ]
185
+ });
186
+ if (mode === "custom") {
187
+ const checks = await checkbox({
188
+ message: "Select checks to run:",
189
+ required: true,
190
+ choices: [
191
+ { name: "Project setup", value: "project" },
192
+ { name: "API route authentication", value: "api", checked: true },
193
+ { name: "Environment variables", value: "environment", checked: true },
194
+ { name: "AI route cost risks", value: "ai" },
195
+ { name: "Webhooks", value: "webhook" },
196
+ { name: "Maintainability / large files", value: "maintainability" }
197
+ ]
198
+ });
199
+ return {
200
+ ok: true,
201
+ checks,
202
+ label: `Custom selection: ${checks.join(", ")}`
203
+ };
204
+ }
205
+ return {
206
+ ok: true,
207
+ checks: getChecksForMode(mode),
208
+ label: getScanModeName(mode)
209
+ };
210
+ }
211
+ function getChecksForMode(mode) {
212
+ if (mode === "recommended") {
213
+ return [...recommendedScanChecks];
214
+ }
215
+ if (mode === "security-api") {
216
+ return ["api", "security", "webhook"];
217
+ }
218
+ if (mode === "environment") {
219
+ return ["environment"];
220
+ }
221
+ if (mode === "ai") {
222
+ return ["ai"];
223
+ }
224
+ if (mode === "webhook") {
225
+ return ["webhook"];
226
+ }
227
+ return ["maintainability"];
228
+ }
229
+ function getScanModeName(mode) {
230
+ if (mode === "recommended") {
231
+ return "Recommended launch scan";
232
+ }
233
+ if (mode === "security-api") {
234
+ return "Security & API routes";
235
+ }
236
+ if (mode === "environment") {
237
+ return "Environment variables";
238
+ }
239
+ if (mode === "ai") {
240
+ return "AI route cost risks";
241
+ }
242
+ if (mode === "webhook") {
243
+ return "Webhooks";
244
+ }
245
+ return "Maintainability";
246
+ }
247
+ function parseChecks(checks) {
248
+ const selectedChecks = [...new Set(
249
+ checks.split(",").map((check) => check.trim().toLowerCase()).filter(Boolean)
250
+ )];
251
+ if (selectedChecks.length === 0) {
252
+ return {
253
+ ok: false,
254
+ reason: `No checks were provided. Valid checks: ${validScanChecks.join(", ")}.`
255
+ };
256
+ }
257
+ const invalidChecks = selectedChecks.filter((check) => !isScanCheck(check));
258
+ if (invalidChecks.length > 0) {
259
+ return {
260
+ ok: false,
261
+ reason: `Invalid check${invalidChecks.length === 1 ? "" : "s"}: ${invalidChecks.join(", ")}.
262
+ Valid checks: ${validScanChecks.join(", ")}.`
263
+ };
264
+ }
265
+ return {
266
+ ok: true,
267
+ checks: selectedChecks.filter(isScanCheck)
268
+ };
269
+ }
270
+ function isScanCheck(check) {
271
+ return validScanChecks.includes(check);
272
+ }
273
+ function getScanModeLabel(checks) {
274
+ if (hasSameChecks(checks, recommendedScanChecks)) {
275
+ return "Recommended launch scan";
276
+ }
277
+ if (hasSameChecks(checks, validScanChecks)) {
278
+ return "All checks";
279
+ }
280
+ if (checks.length === 1) {
281
+ const check = checks[0];
282
+ if (check === "environment") {
283
+ return "Environment variables";
284
+ }
285
+ if (check === "api") {
286
+ return "API route authentication";
287
+ }
288
+ if (check === "ai") {
289
+ return "AI route cost risks";
290
+ }
291
+ if (check === "webhook") {
292
+ return "Webhooks";
293
+ }
294
+ if (check === "maintainability") {
295
+ return "Maintainability";
296
+ }
297
+ if (check === "project") {
298
+ return "Project setup";
299
+ }
300
+ return "Security";
301
+ }
302
+ return `Custom selection: ${checks.join(", ")}`;
303
+ }
304
+ function hasSameChecks(leftChecks, rightChecks) {
305
+ const leftSet = new Set(leftChecks);
306
+ const rightSet = new Set(rightChecks);
307
+ return leftSet.size === rightSet.size && [...leftSet].every((check) => rightSet.has(check));
308
+ }
309
+ function isNonInteractiveTerminal() {
310
+ return Boolean(process.env.CI) || !process.stdin.isTTY || !process.stdout.isTTY;
311
+ }
312
+ function resolvePromptChecks(options) {
313
+ if (options.checks) {
314
+ const parsedChecks = parseChecks(options.checks);
315
+ if (!parsedChecks.ok) {
316
+ return parsedChecks;
317
+ }
318
+ return {
319
+ ok: true,
320
+ checks: parsedChecks.checks,
321
+ label: getScanModeLabel(parsedChecks.checks)
322
+ };
323
+ }
324
+ return {
325
+ ok: true,
326
+ checks: [...validScanChecks],
327
+ label: "All checks"
328
+ };
329
+ }
48
330
  async function resolveProjectPath(projectPath) {
49
331
  const inputPath = projectPath.trim() || process.cwd();
50
332
  const resolvedPath = path.resolve(inputPath);
@@ -80,11 +362,12 @@ async function resolveProjectPath(projectPath) {
80
362
  };
81
363
  }
82
364
  }
83
- function printReport(report, maxIssues, showPrompts) {
365
+ function printReport(report, maxIssues, showPrompts, scanModeLabel) {
84
366
  console.log(pc.bold("Qodfy Report"));
85
367
  console.log("");
86
368
  const scoreColor = report.score >= 80 ? pc.green : report.score >= 60 ? pc.yellow : pc.red;
87
369
  console.log(`Launch Readiness: ${scoreColor(`${report.score}/100`)}`);
370
+ console.log(`Scan mode: ${scanModeLabel}`);
88
371
  console.log("");
89
372
  console.log(pc.bold("Stats"));
90
373
  console.log(`Files scanned: ${report.stats.totalFiles}`);
@@ -99,15 +382,24 @@ function printReport(report, maxIssues, showPrompts) {
99
382
  return;
100
383
  }
101
384
  console.log(pc.bold("Issues"));
102
- const issuesToShow = report.issues.slice(0, maxIssues);
385
+ const displayIssues = getSortedDisplayIssues(report.issues);
386
+ const issuesToShow = displayIssues.slice(0, maxIssues);
103
387
  if (report.issues.length > maxIssues) {
104
388
  console.log(`Showing ${maxIssues} of ${report.issues.length} issues.`);
105
389
  console.log(`Use --max-issues <number> to show more.`);
106
390
  }
107
- printGroupedIssues(issuesToShow, showPrompts);
391
+ printGroupedIssues(issuesToShow, showPrompts, report.projectPath);
108
392
  console.log("");
109
393
  console.log(pc.bold("Recommended next step:"));
110
394
  console.log("Fix critical issues first, then warnings, then cleanup items.");
395
+ console.log("");
396
+ console.log(pc.bold("Next commands:"));
397
+ const firstPromptIssue = issuesToShow.find((issue) => issue.fixPrompt);
398
+ if (firstPromptIssue) {
399
+ console.log(getPromptCommand(firstPromptIssue.id, report.projectPath));
400
+ }
401
+ console.log("qodfy scan --checks api,environment");
402
+ console.log("qodfy scan --max-issues 50");
111
403
  }
112
404
  function printSummary(issues) {
113
405
  const criticalCount = countIssuesBySeverity(issues, "critical");
@@ -138,7 +430,7 @@ function printSummary(issues) {
138
430
  }
139
431
  console.log("");
140
432
  }
141
- function printGroupedIssues(issues, showPrompts) {
433
+ function printGroupedIssues(issues, showPrompts, projectPath) {
142
434
  for (const category of categoryOrder) {
143
435
  const categoryIssues = issues.filter((issue) => issue.category === category);
144
436
  if (categoryIssues.length === 0) {
@@ -147,11 +439,11 @@ function printGroupedIssues(issues, showPrompts) {
147
439
  console.log("");
148
440
  console.log(pc.bold(categoryLabels[category]));
149
441
  for (const issue of categoryIssues) {
150
- printIssue(issue, showPrompts);
442
+ printIssue(issue, showPrompts, projectPath);
151
443
  }
152
444
  }
153
445
  }
154
- function printIssue(issue, showPrompts) {
446
+ function printIssue(issue, showPrompts, projectPath) {
155
447
  console.log("");
156
448
  console.log(`${pc.dim(`[${issue.id}]`)} ${getSeverityLabel(issue.severity)} ${pc.bold(issue.title)}`);
157
449
  console.log(issue.message);
@@ -165,7 +457,19 @@ function printIssue(issue, showPrompts) {
165
457
  console.log("");
166
458
  console.log(pc.bold("Fix Prompt:"));
167
459
  console.log(issue.fixPrompt);
460
+ } else if (issue.fixPrompt) {
461
+ console.log(pc.dim(`AI fix prompt: ${getPromptCommand(issue.id, projectPath)}`));
462
+ }
463
+ }
464
+ function printFixPrompt(issue) {
465
+ console.log(pc.bold("Qodfy Fix Prompt"));
466
+ console.log("");
467
+ console.log(`${pc.dim(`[${issue.id}]`)} ${getSeverityLabel(issue.severity)} ${pc.bold(issue.title)}`);
468
+ if (issue.file) {
469
+ console.log(pc.dim(`File: ${issue.file}`));
168
470
  }
471
+ console.log("");
472
+ console.log(issue.fixPrompt);
169
473
  }
170
474
  function getSeverityLabel(severity) {
171
475
  if (severity === "critical") {
@@ -179,6 +483,24 @@ function getSeverityLabel(severity) {
179
483
  function countIssuesBySeverity(issues, severity) {
180
484
  return issues.filter((issue) => issue.severity === severity).length;
181
485
  }
486
+ function getSortedDisplayIssues(issues) {
487
+ return [...issues].sort((leftIssue, rightIssue) => {
488
+ 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);
489
+ });
490
+ }
491
+ function getIssueNumber(issueId) {
492
+ const match = issueId.match(/-(\d+)$/);
493
+ return match ? Number.parseInt(match[1], 10) : 0;
494
+ }
495
+ function getSeverityRank(severity) {
496
+ if (severity === "critical") {
497
+ return 0;
498
+ }
499
+ if (severity === "warning") {
500
+ return 1;
501
+ }
502
+ return 2;
503
+ }
182
504
  function getTopPriorities(issues) {
183
505
  const priorities = [
184
506
  {
@@ -228,6 +550,27 @@ function getTopPriorities(issues) {
228
550
  (priority) => issues.some((issue) => priority.ruleIds.includes(issue.ruleId))
229
551
  ).slice(0, 3).map((priority) => priority.message);
230
552
  }
553
+ function getPromptCommand(issueId, projectPath) {
554
+ const relativeProjectPath = path.relative(process.cwd(), projectPath);
555
+ const promptPath = getPromptPath(projectPath, relativeProjectPath);
556
+ const pathOption = promptPath ? ` --path ${shellQuote(promptPath)}` : "";
557
+ return `qodfy prompt ${issueId}${pathOption}`;
558
+ }
559
+ function getPromptPath(projectPath, relativeProjectPath) {
560
+ if (!relativeProjectPath) {
561
+ return "";
562
+ }
563
+ if (!relativeProjectPath.startsWith("..") && !path.isAbsolute(relativeProjectPath)) {
564
+ return relativeProjectPath;
565
+ }
566
+ return projectPath;
567
+ }
568
+ function shellQuote(value) {
569
+ if (/^[A-Za-z0-9_./:-]+$/.test(value)) {
570
+ return value;
571
+ }
572
+ return `'${value.replaceAll("'", "'\\''")}'`;
573
+ }
231
574
  function printScanError(reason) {
232
575
  console.error(pc.red("Qodfy could not scan this project."));
233
576
  console.error("");
@@ -237,16 +580,28 @@ function printScanError(reason) {
237
580
  console.error(pc.bold("Try:"));
238
581
  console.error("qodfy scan --path ./my-next-app");
239
582
  }
583
+ function printPromptError(reason) {
584
+ console.error(pc.red("Qodfy could not create this fix prompt."));
585
+ console.error("");
586
+ console.error(pc.bold("Reason:"));
587
+ console.error(reason);
588
+ console.error("");
589
+ console.error(pc.bold("Try:"));
590
+ console.error("qodfy scan --all");
591
+ }
240
592
  function getErrorMessage(error) {
241
593
  if (error instanceof Error && error.message) {
242
594
  return error.message;
243
595
  }
244
596
  return "An unexpected error occurred while scanning the project.";
245
597
  }
598
+ function isPromptCancelError(error) {
599
+ return error instanceof Error && (error.name === "ExitPromptError" || error.message.includes("User force closed the prompt"));
600
+ }
246
601
  function parseMaxIssues(maxIssues) {
247
602
  const parsedMaxIssues = Number.parseInt(maxIssues, 10);
248
603
  if (!Number.isFinite(parsedMaxIssues) || parsedMaxIssues <= 0) {
249
- return 50;
604
+ return DEFAULT_MAX_ISSUES;
250
605
  }
251
606
  return parsedMaxIssues;
252
607
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qodfy",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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.2.0"
55
+ "@qodfy/core": "^0.2.2"
55
56
  },
56
57
  "devDependencies": {
57
58
  "@types/node": "^25.7.0",