qodfy 0.1.6 → 0.2.0
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 +145 -16
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -5,10 +5,12 @@ import fs from "fs/promises";
|
|
|
5
5
|
import path from "path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import pc from "picocolors";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
scanProject
|
|
10
|
+
} from "@qodfy/core";
|
|
9
11
|
var program = new Command();
|
|
10
|
-
program.name("qodfy").description("Launch readiness scanner for AI-built apps.").version("0.
|
|
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) => {
|
|
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) => {
|
|
12
14
|
const pathResult = await resolveProjectPath(options.path);
|
|
13
15
|
if (!pathResult.ok) {
|
|
14
16
|
printScanError(pathResult.reason);
|
|
@@ -18,12 +20,30 @@ program.command("scan").description("Scan a project for launch readiness issues.
|
|
|
18
20
|
try {
|
|
19
21
|
console.log(pc.cyan("Qodfy is scanning your project...\n"));
|
|
20
22
|
const report = await scanProject(pathResult.projectPath);
|
|
21
|
-
printReport(report, parseMaxIssues(options.maxIssues));
|
|
23
|
+
printReport(report, parseMaxIssues(options.maxIssues), Boolean(options.prompts));
|
|
22
24
|
} catch (error) {
|
|
23
25
|
printScanError(getErrorMessage(error));
|
|
24
26
|
process.exitCode = 1;
|
|
25
27
|
}
|
|
26
28
|
});
|
|
29
|
+
var categoryOrder = [
|
|
30
|
+
"security",
|
|
31
|
+
"api",
|
|
32
|
+
"webhook",
|
|
33
|
+
"ai",
|
|
34
|
+
"environment",
|
|
35
|
+
"maintainability",
|
|
36
|
+
"project"
|
|
37
|
+
];
|
|
38
|
+
var categoryLabels = {
|
|
39
|
+
security: "Security",
|
|
40
|
+
api: "API",
|
|
41
|
+
webhook: "Webhooks",
|
|
42
|
+
ai: "AI",
|
|
43
|
+
environment: "Environment",
|
|
44
|
+
maintainability: "Maintainability",
|
|
45
|
+
project: "Project"
|
|
46
|
+
};
|
|
27
47
|
await program.parseAsync();
|
|
28
48
|
async function resolveProjectPath(projectPath) {
|
|
29
49
|
const inputPath = projectPath.trim() || process.cwd();
|
|
@@ -60,7 +80,7 @@ async function resolveProjectPath(projectPath) {
|
|
|
60
80
|
};
|
|
61
81
|
}
|
|
62
82
|
}
|
|
63
|
-
function printReport(report, maxIssues) {
|
|
83
|
+
function printReport(report, maxIssues, showPrompts) {
|
|
64
84
|
console.log(pc.bold("Qodfy Report"));
|
|
65
85
|
console.log("");
|
|
66
86
|
const scoreColor = report.score >= 80 ? pc.green : report.score >= 60 ? pc.yellow : pc.red;
|
|
@@ -73,6 +93,7 @@ function printReport(report, maxIssues) {
|
|
|
73
93
|
console.log(`Large files: ${report.stats.largeFiles}`);
|
|
74
94
|
console.log(`Scan duration: ${formatDuration(report.stats.durationMs)}`);
|
|
75
95
|
console.log("");
|
|
96
|
+
printSummary(report.issues);
|
|
76
97
|
if (report.issues.length === 0) {
|
|
77
98
|
console.log(pc.green("No issues found. Your project looks clean."));
|
|
78
99
|
return;
|
|
@@ -83,21 +104,129 @@ function printReport(report, maxIssues) {
|
|
|
83
104
|
console.log(`Showing ${maxIssues} of ${report.issues.length} issues.`);
|
|
84
105
|
console.log(`Use --max-issues <number> to show more.`);
|
|
85
106
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
107
|
+
printGroupedIssues(issuesToShow, showPrompts);
|
|
108
|
+
console.log("");
|
|
109
|
+
console.log(pc.bold("Recommended next step:"));
|
|
110
|
+
console.log("Fix critical issues first, then warnings, then cleanup items.");
|
|
111
|
+
}
|
|
112
|
+
function printSummary(issues) {
|
|
113
|
+
const criticalCount = countIssuesBySeverity(issues, "critical");
|
|
114
|
+
const warningCount = countIssuesBySeverity(issues, "warning");
|
|
115
|
+
const infoCount = countIssuesBySeverity(issues, "info");
|
|
116
|
+
console.log(pc.bold("Summary"));
|
|
117
|
+
console.log(`Critical: ${criticalCount}`);
|
|
118
|
+
console.log(`Warnings: ${warningCount}`);
|
|
119
|
+
console.log(`Info: ${infoCount}`);
|
|
120
|
+
console.log("");
|
|
121
|
+
if (issues.length === 0) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
console.log(pc.bold("Categories"));
|
|
125
|
+
for (const category of categoryOrder) {
|
|
126
|
+
const count = issues.filter((issue) => issue.category === category).length;
|
|
127
|
+
if (count > 0) {
|
|
128
|
+
console.log(`${categoryLabels[category]}: ${count}`);
|
|
93
129
|
}
|
|
94
|
-
|
|
95
|
-
|
|
130
|
+
}
|
|
131
|
+
const priorities = getTopPriorities(issues);
|
|
132
|
+
if (priorities.length > 0) {
|
|
133
|
+
console.log("");
|
|
134
|
+
console.log(pc.bold("Top priorities"));
|
|
135
|
+
for (const [index, priority] of priorities.entries()) {
|
|
136
|
+
console.log(`${index + 1}. ${priority}`);
|
|
96
137
|
}
|
|
97
138
|
}
|
|
98
139
|
console.log("");
|
|
99
|
-
|
|
100
|
-
|
|
140
|
+
}
|
|
141
|
+
function printGroupedIssues(issues, showPrompts) {
|
|
142
|
+
for (const category of categoryOrder) {
|
|
143
|
+
const categoryIssues = issues.filter((issue) => issue.category === category);
|
|
144
|
+
if (categoryIssues.length === 0) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
console.log("");
|
|
148
|
+
console.log(pc.bold(categoryLabels[category]));
|
|
149
|
+
for (const issue of categoryIssues) {
|
|
150
|
+
printIssue(issue, showPrompts);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function printIssue(issue, showPrompts) {
|
|
155
|
+
console.log("");
|
|
156
|
+
console.log(`${pc.dim(`[${issue.id}]`)} ${getSeverityLabel(issue.severity)} ${pc.bold(issue.title)}`);
|
|
157
|
+
console.log(issue.message);
|
|
158
|
+
if (issue.file) {
|
|
159
|
+
console.log(pc.dim(`File: ${issue.file}`));
|
|
160
|
+
}
|
|
161
|
+
if (issue.suggestion) {
|
|
162
|
+
console.log(pc.dim(`Suggestion: ${issue.suggestion}`));
|
|
163
|
+
}
|
|
164
|
+
if (showPrompts && issue.fixPrompt) {
|
|
165
|
+
console.log("");
|
|
166
|
+
console.log(pc.bold("Fix Prompt:"));
|
|
167
|
+
console.log(issue.fixPrompt);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function getSeverityLabel(severity) {
|
|
171
|
+
if (severity === "critical") {
|
|
172
|
+
return pc.red("CRITICAL");
|
|
173
|
+
}
|
|
174
|
+
if (severity === "warning") {
|
|
175
|
+
return pc.yellow("WARNING");
|
|
176
|
+
}
|
|
177
|
+
return pc.blue("INFO");
|
|
178
|
+
}
|
|
179
|
+
function countIssuesBySeverity(issues, severity) {
|
|
180
|
+
return issues.filter((issue) => issue.severity === severity).length;
|
|
181
|
+
}
|
|
182
|
+
function getTopPriorities(issues) {
|
|
183
|
+
const priorities = [
|
|
184
|
+
{
|
|
185
|
+
ruleIds: ["security-hardcoded-secret"],
|
|
186
|
+
message: "Remove possible hardcoded secrets and rotate any real exposed values."
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
ruleIds: ["webhook-missing-signature-verification"],
|
|
190
|
+
message: "Verify webhook signatures before handling external events."
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
ruleIds: ["ai-route-missing-rate-limit"],
|
|
194
|
+
message: "Add cost and abuse protection to AI-related API routes."
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
ruleIds: ["security-client-side-secret"],
|
|
198
|
+
message: "Move possible server-only secrets out of client-side code."
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
ruleIds: ["api-route-missing-auth"],
|
|
202
|
+
message: "Review API routes that may be missing authentication."
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
ruleIds: [
|
|
206
|
+
"environment-missing-env-example",
|
|
207
|
+
"environment-variable-missing-from-example"
|
|
208
|
+
],
|
|
209
|
+
message: "Add missing environment variables to .env.example."
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
ruleIds: [
|
|
213
|
+
"maintainability-large-file",
|
|
214
|
+
"maintainability-large-file-skipped"
|
|
215
|
+
],
|
|
216
|
+
message: "Review large files for maintainability before launch."
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
ruleIds: [
|
|
220
|
+
"project-missing-package-json",
|
|
221
|
+
"project-invalid-package-json",
|
|
222
|
+
"project-next-not-detected"
|
|
223
|
+
],
|
|
224
|
+
message: "Confirm Qodfy is scanning the correct project root."
|
|
225
|
+
}
|
|
226
|
+
];
|
|
227
|
+
return priorities.filter(
|
|
228
|
+
(priority) => issues.some((issue) => priority.ruleIds.includes(issue.ruleId))
|
|
229
|
+
).slice(0, 3).map((priority) => priority.message);
|
|
101
230
|
}
|
|
102
231
|
function printScanError(reason) {
|
|
103
232
|
console.error(pc.red("Qodfy could not scan this project."));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qodfy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Open-source launch readiness scanner for AI-built apps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"qodfy",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"commander": "^14.0.3",
|
|
53
53
|
"picocolors": "^1.1.1",
|
|
54
|
-
"@qodfy/core": "^0.
|
|
54
|
+
"@qodfy/core": "^0.2.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/node": "^25.7.0",
|