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.
- package/dist/index.js +369 -14
- 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.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(
|
|
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
|
});
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
55
|
+
"@qodfy/core": "^0.2.2"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
57
58
|
"@types/node": "^25.7.0",
|