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.
- package/dist/index.js +268 -10
- 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
|
-
|
|
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.
|
|
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",
|
|
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(
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
55
|
+
"@qodfy/core": "^0.2.1"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
57
58
|
"@types/node": "^25.7.0",
|