qodfy 0.2.0 → 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 +268 -10
  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.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) => {
14
18
  const pathResult = await resolveProjectPath(options.path);
15
19
  if (!pathResult.ok) {
16
20
  printScanError(pathResult.reason);
@@ -18,19 +22,41 @@ 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
  });
29
55
  var categoryOrder = [
30
56
  "security",
31
- "api",
32
57
  "webhook",
33
58
  "ai",
59
+ "api",
34
60
  "environment",
35
61
  "maintainability",
36
62
  "project"
@@ -45,6 +71,211 @@ var categoryLabels = {
45
71
  project: "Project"
46
72
  };
47
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
+ }
48
279
  async function resolveProjectPath(projectPath) {
49
280
  const inputPath = projectPath.trim() || process.cwd();
50
281
  const resolvedPath = path.resolve(inputPath);
@@ -80,11 +311,12 @@ async function resolveProjectPath(projectPath) {
80
311
  };
81
312
  }
82
313
  }
83
- function printReport(report, maxIssues, showPrompts) {
314
+ function printReport(report, maxIssues, showPrompts, scanModeLabel) {
84
315
  console.log(pc.bold("Qodfy Report"));
85
316
  console.log("");
86
317
  const scoreColor = report.score >= 80 ? pc.green : report.score >= 60 ? pc.yellow : pc.red;
87
318
  console.log(`Launch Readiness: ${scoreColor(`${report.score}/100`)}`);
319
+ console.log(`Scan mode: ${scanModeLabel}`);
88
320
  console.log("");
89
321
  console.log(pc.bold("Stats"));
90
322
  console.log(`Files scanned: ${report.stats.totalFiles}`);
@@ -99,7 +331,8 @@ function printReport(report, maxIssues, showPrompts) {
99
331
  return;
100
332
  }
101
333
  console.log(pc.bold("Issues"));
102
- const issuesToShow = report.issues.slice(0, maxIssues);
334
+ const displayIssues = getSortedDisplayIssues(report.issues);
335
+ const issuesToShow = displayIssues.slice(0, maxIssues);
103
336
  if (report.issues.length > maxIssues) {
104
337
  console.log(`Showing ${maxIssues} of ${report.issues.length} issues.`);
105
338
  console.log(`Use --max-issues <number> to show more.`);
@@ -108,6 +341,10 @@ function printReport(report, maxIssues, showPrompts) {
108
341
  console.log("");
109
342
  console.log(pc.bold("Recommended next step:"));
110
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");
111
348
  }
112
349
  function printSummary(issues) {
113
350
  const criticalCount = countIssuesBySeverity(issues, "critical");
@@ -179,6 +416,24 @@ function getSeverityLabel(severity) {
179
416
  function countIssuesBySeverity(issues, severity) {
180
417
  return issues.filter((issue) => issue.severity === severity).length;
181
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
+ }
182
437
  function getTopPriorities(issues) {
183
438
  const priorities = [
184
439
  {
@@ -243,10 +498,13 @@ function getErrorMessage(error) {
243
498
  }
244
499
  return "An unexpected error occurred while scanning the project.";
245
500
  }
501
+ function isPromptCancelError(error) {
502
+ return error instanceof Error && (error.name === "ExitPromptError" || error.message.includes("User force closed the prompt"));
503
+ }
246
504
  function parseMaxIssues(maxIssues) {
247
505
  const parsedMaxIssues = Number.parseInt(maxIssues, 10);
248
506
  if (!Number.isFinite(parsedMaxIssues) || parsedMaxIssues <= 0) {
249
- return 50;
507
+ return DEFAULT_MAX_ISSUES;
250
508
  }
251
509
  return parsedMaxIssues;
252
510
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qodfy",
3
- "version": "0.2.0",
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.2.0"
55
+ "@qodfy/core": "^0.2.1"
55
56
  },
56
57
  "devDependencies": {
57
58
  "@types/node": "^25.7.0",