qodfy 0.2.1 → 0.2.3
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 +164 -21
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -11,10 +11,10 @@ import {
|
|
|
11
11
|
scanProject,
|
|
12
12
|
validScanChecks
|
|
13
13
|
} from "@qodfy/core";
|
|
14
|
-
var DEFAULT_MAX_ISSUES =
|
|
14
|
+
var DEFAULT_MAX_ISSUES = 5;
|
|
15
15
|
var program = new Command();
|
|
16
|
-
program.name("qodfy").description("Launch readiness scanner for AI-built apps.").version("0.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) => {
|
|
16
|
+
program.name("qodfy").description("Launch readiness scanner for AI-built apps.").version("0.2.3");
|
|
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("--prompt <issue-id>", "Show the safe AI fix prompt for one issue").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) => {
|
|
18
18
|
const pathResult = await resolveProjectPath(options.path);
|
|
19
19
|
if (!pathResult.ok) {
|
|
20
20
|
printScanError(pathResult.reason);
|
|
@@ -35,13 +35,20 @@ program.command("scan").description("Scan a project for launch readiness issues.
|
|
|
35
35
|
console.log(pc.cyan("Qodfy is scanning your project...\n"));
|
|
36
36
|
const report = await scanProject({
|
|
37
37
|
projectPath: pathResult.projectPath,
|
|
38
|
-
checks: scanModeResult.checks
|
|
38
|
+
checks: scanModeResult.checks,
|
|
39
|
+
includeLowConfidence: Boolean(scanModeResult.includeLowConfidence)
|
|
39
40
|
});
|
|
41
|
+
if (options.prompt) {
|
|
42
|
+
printPromptFromReport(report, options.prompt);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
40
45
|
printReport(
|
|
41
46
|
report,
|
|
42
47
|
parseMaxIssues(options.maxIssues),
|
|
43
48
|
Boolean(options.prompts),
|
|
44
|
-
scanModeResult.label
|
|
49
|
+
scanModeResult.label,
|
|
50
|
+
Boolean(options.all),
|
|
51
|
+
Boolean(options.all)
|
|
45
52
|
);
|
|
46
53
|
} catch (error) {
|
|
47
54
|
if (isPromptCancelError(error)) {
|
|
@@ -52,6 +59,40 @@ program.command("scan").description("Scan a project for launch readiness issues.
|
|
|
52
59
|
process.exitCode = 1;
|
|
53
60
|
}
|
|
54
61
|
});
|
|
62
|
+
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) => {
|
|
63
|
+
const pathResult = await resolveProjectPath(options.path);
|
|
64
|
+
if (!pathResult.ok) {
|
|
65
|
+
printScanError(pathResult.reason);
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const checksResult = resolvePromptChecks(options);
|
|
70
|
+
if (!checksResult.ok) {
|
|
71
|
+
printPromptError(checksResult.reason);
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const report = await scanProject({
|
|
76
|
+
projectPath: pathResult.projectPath,
|
|
77
|
+
checks: checksResult.checks,
|
|
78
|
+
includeLowConfidence: true
|
|
79
|
+
});
|
|
80
|
+
const issue = report.issues.find((scanIssue) => scanIssue.id === issueId);
|
|
81
|
+
if (!issue) {
|
|
82
|
+
printPromptError(
|
|
83
|
+
`Issue "${issueId}" was not found in this project scan.
|
|
84
|
+
Run qodfy scan --all to see the current issue IDs.`
|
|
85
|
+
);
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!issue.fixPrompt) {
|
|
90
|
+
printPromptError(`Issue "${issueId}" does not have an AI fix prompt yet.`);
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
printFixPrompt(issue);
|
|
95
|
+
});
|
|
55
96
|
var categoryOrder = [
|
|
56
97
|
"security",
|
|
57
98
|
"webhook",
|
|
@@ -87,7 +128,16 @@ async function resolveScanMode(options) {
|
|
|
87
128
|
return {
|
|
88
129
|
ok: true,
|
|
89
130
|
checks: [...validScanChecks],
|
|
90
|
-
label: "All checks"
|
|
131
|
+
label: "All checks",
|
|
132
|
+
includeLowConfidence: true
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (options.prompt) {
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
checks: [...validScanChecks],
|
|
139
|
+
label: "All checks",
|
|
140
|
+
includeLowConfidence: true
|
|
91
141
|
};
|
|
92
142
|
}
|
|
93
143
|
if (options.interactive === false) {
|
|
@@ -276,6 +326,24 @@ function hasSameChecks(leftChecks, rightChecks) {
|
|
|
276
326
|
function isNonInteractiveTerminal() {
|
|
277
327
|
return Boolean(process.env.CI) || !process.stdin.isTTY || !process.stdout.isTTY;
|
|
278
328
|
}
|
|
329
|
+
function resolvePromptChecks(options) {
|
|
330
|
+
if (options.checks) {
|
|
331
|
+
const parsedChecks = parseChecks(options.checks);
|
|
332
|
+
if (!parsedChecks.ok) {
|
|
333
|
+
return parsedChecks;
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
ok: true,
|
|
337
|
+
checks: parsedChecks.checks,
|
|
338
|
+
label: getScanModeLabel(parsedChecks.checks)
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
ok: true,
|
|
343
|
+
checks: [...validScanChecks],
|
|
344
|
+
label: "All checks"
|
|
345
|
+
};
|
|
346
|
+
}
|
|
279
347
|
async function resolveProjectPath(projectPath) {
|
|
280
348
|
const inputPath = projectPath.trim() || process.cwd();
|
|
281
349
|
const resolvedPath = path.resolve(inputPath);
|
|
@@ -311,7 +379,7 @@ async function resolveProjectPath(projectPath) {
|
|
|
311
379
|
};
|
|
312
380
|
}
|
|
313
381
|
}
|
|
314
|
-
function printReport(report, maxIssues, showPrompts, scanModeLabel) {
|
|
382
|
+
function printReport(report, maxIssues, showPrompts, scanModeLabel, showDetails, showAllIssues) {
|
|
315
383
|
console.log(pc.bold("Qodfy Report"));
|
|
316
384
|
console.log("");
|
|
317
385
|
const scoreColor = report.score >= 80 ? pc.green : report.score >= 60 ? pc.yellow : pc.red;
|
|
@@ -330,21 +398,26 @@ function printReport(report, maxIssues, showPrompts, scanModeLabel) {
|
|
|
330
398
|
console.log(pc.green("No issues found. Your project looks clean."));
|
|
331
399
|
return;
|
|
332
400
|
}
|
|
333
|
-
console.log(pc.bold("Issues"));
|
|
401
|
+
console.log(pc.bold(showAllIssues ? "Issues" : "Top issues"));
|
|
334
402
|
const displayIssues = getSortedDisplayIssues(report.issues);
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
console.log(`
|
|
403
|
+
const issueLimit = showAllIssues ? displayIssues.length : maxIssues;
|
|
404
|
+
const issuesToShow = displayIssues.slice(0, issueLimit);
|
|
405
|
+
if (report.issues.length > issueLimit) {
|
|
406
|
+
console.log(`Showing ${issueLimit} of ${report.issues.length} issues.`);
|
|
407
|
+
console.log(`Use --max-issues <number> to show more, or --all for full details.`);
|
|
339
408
|
}
|
|
340
|
-
printGroupedIssues(issuesToShow, showPrompts);
|
|
409
|
+
printGroupedIssues(issuesToShow, showPrompts, showDetails, report.projectPath);
|
|
341
410
|
console.log("");
|
|
342
411
|
console.log(pc.bold("Recommended next step:"));
|
|
343
412
|
console.log("Fix critical issues first, then warnings, then cleanup items.");
|
|
344
413
|
console.log("");
|
|
345
414
|
console.log(pc.bold("Next commands:"));
|
|
415
|
+
const firstPromptIssue = issuesToShow.find((issue) => issue.fixPrompt);
|
|
416
|
+
if (firstPromptIssue) {
|
|
417
|
+
console.log(getPromptCommand(firstPromptIssue.id, report.projectPath));
|
|
418
|
+
}
|
|
346
419
|
console.log("qodfy scan --checks api,environment");
|
|
347
|
-
console.log("qodfy scan --
|
|
420
|
+
console.log("qodfy scan --all");
|
|
348
421
|
}
|
|
349
422
|
function printSummary(issues) {
|
|
350
423
|
const criticalCount = countIssuesBySeverity(issues, "critical");
|
|
@@ -375,7 +448,7 @@ function printSummary(issues) {
|
|
|
375
448
|
}
|
|
376
449
|
console.log("");
|
|
377
450
|
}
|
|
378
|
-
function printGroupedIssues(issues, showPrompts) {
|
|
451
|
+
function printGroupedIssues(issues, showPrompts, showDetails, projectPath) {
|
|
379
452
|
for (const category of categoryOrder) {
|
|
380
453
|
const categoryIssues = issues.filter((issue) => issue.category === category);
|
|
381
454
|
if (categoryIssues.length === 0) {
|
|
@@ -384,25 +457,56 @@ function printGroupedIssues(issues, showPrompts) {
|
|
|
384
457
|
console.log("");
|
|
385
458
|
console.log(pc.bold(categoryLabels[category]));
|
|
386
459
|
for (const issue of categoryIssues) {
|
|
387
|
-
printIssue(issue, showPrompts);
|
|
460
|
+
printIssue(issue, showPrompts, showDetails, projectPath);
|
|
388
461
|
}
|
|
389
462
|
}
|
|
390
463
|
}
|
|
391
|
-
function printIssue(issue, showPrompts) {
|
|
464
|
+
function printIssue(issue, showPrompts, showDetails, projectPath) {
|
|
392
465
|
console.log("");
|
|
393
|
-
console.log(`${pc.dim(`[${issue.id}]`)} ${getSeverityLabel(issue.severity)} ${pc.bold(issue.title)}`);
|
|
394
|
-
console.log(issue.message);
|
|
466
|
+
console.log(`${pc.dim(`[${issue.id}]`)} ${getSeverityLabel(issue.severity)} ${pc.bold(issue.title)} ${pc.dim(`(${issue.confidence} confidence)`)}`);
|
|
395
467
|
if (issue.file) {
|
|
396
468
|
console.log(pc.dim(`File: ${issue.file}`));
|
|
397
469
|
}
|
|
398
|
-
if (
|
|
470
|
+
if (showDetails || showPrompts) {
|
|
471
|
+
console.log(issue.message);
|
|
472
|
+
}
|
|
473
|
+
if ((showDetails || showPrompts) && issue.suggestion) {
|
|
399
474
|
console.log(pc.dim(`Suggestion: ${issue.suggestion}`));
|
|
400
475
|
}
|
|
401
476
|
if (showPrompts && issue.fixPrompt) {
|
|
402
477
|
console.log("");
|
|
403
478
|
console.log(pc.bold("Fix Prompt:"));
|
|
404
479
|
console.log(issue.fixPrompt);
|
|
480
|
+
} else if (issue.fixPrompt) {
|
|
481
|
+
console.log(pc.dim(`Fix: ${getPromptCommand(issue.id, projectPath)}`));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function printFixPrompt(issue) {
|
|
485
|
+
console.log(pc.bold("Qodfy Fix Prompt"));
|
|
486
|
+
console.log("");
|
|
487
|
+
console.log(`${pc.dim(`[${issue.id}]`)} ${getSeverityLabel(issue.severity)} ${pc.bold(issue.title)} ${pc.dim(`(${issue.confidence} confidence)`)}`);
|
|
488
|
+
if (issue.file) {
|
|
489
|
+
console.log(pc.dim(`File: ${issue.file}`));
|
|
490
|
+
}
|
|
491
|
+
console.log("");
|
|
492
|
+
console.log(issue.fixPrompt);
|
|
493
|
+
}
|
|
494
|
+
function printPromptFromReport(report, issueId) {
|
|
495
|
+
const issue = report.issues.find((scanIssue) => scanIssue.id === issueId);
|
|
496
|
+
if (!issue) {
|
|
497
|
+
printPromptError(
|
|
498
|
+
`Issue "${issueId}" was not found in this project scan.
|
|
499
|
+
Run qodfy scan --all to see the current issue IDs.`
|
|
500
|
+
);
|
|
501
|
+
process.exitCode = 1;
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (!issue.fixPrompt) {
|
|
505
|
+
printPromptError(`Issue "${issueId}" does not have an AI fix prompt yet.`);
|
|
506
|
+
process.exitCode = 1;
|
|
507
|
+
return;
|
|
405
508
|
}
|
|
509
|
+
printFixPrompt(issue);
|
|
406
510
|
}
|
|
407
511
|
function getSeverityLabel(severity) {
|
|
408
512
|
if (severity === "critical") {
|
|
@@ -418,7 +522,7 @@ function countIssuesBySeverity(issues, severity) {
|
|
|
418
522
|
}
|
|
419
523
|
function getSortedDisplayIssues(issues) {
|
|
420
524
|
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);
|
|
525
|
+
return getSeverityRank(leftIssue.severity) - getSeverityRank(rightIssue.severity) || getConfidenceRank(leftIssue.confidence) - getConfidenceRank(rightIssue.confidence) || 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
526
|
});
|
|
423
527
|
}
|
|
424
528
|
function getIssueNumber(issueId) {
|
|
@@ -434,6 +538,15 @@ function getSeverityRank(severity) {
|
|
|
434
538
|
}
|
|
435
539
|
return 2;
|
|
436
540
|
}
|
|
541
|
+
function getConfidenceRank(confidence) {
|
|
542
|
+
if (confidence === "high") {
|
|
543
|
+
return 0;
|
|
544
|
+
}
|
|
545
|
+
if (confidence === "medium") {
|
|
546
|
+
return 1;
|
|
547
|
+
}
|
|
548
|
+
return 2;
|
|
549
|
+
}
|
|
437
550
|
function getTopPriorities(issues) {
|
|
438
551
|
const priorities = [
|
|
439
552
|
{
|
|
@@ -483,6 +596,27 @@ function getTopPriorities(issues) {
|
|
|
483
596
|
(priority) => issues.some((issue) => priority.ruleIds.includes(issue.ruleId))
|
|
484
597
|
).slice(0, 3).map((priority) => priority.message);
|
|
485
598
|
}
|
|
599
|
+
function getPromptCommand(issueId, projectPath) {
|
|
600
|
+
const relativeProjectPath = path.relative(process.cwd(), projectPath);
|
|
601
|
+
const promptPath = getPromptPath(projectPath, relativeProjectPath);
|
|
602
|
+
const pathOption = promptPath ? ` --path ${shellQuote(promptPath)}` : "";
|
|
603
|
+
return `qodfy prompt ${issueId}${pathOption}`;
|
|
604
|
+
}
|
|
605
|
+
function getPromptPath(projectPath, relativeProjectPath) {
|
|
606
|
+
if (!relativeProjectPath) {
|
|
607
|
+
return "";
|
|
608
|
+
}
|
|
609
|
+
if (!relativeProjectPath.startsWith("..") && !path.isAbsolute(relativeProjectPath)) {
|
|
610
|
+
return relativeProjectPath;
|
|
611
|
+
}
|
|
612
|
+
return projectPath;
|
|
613
|
+
}
|
|
614
|
+
function shellQuote(value) {
|
|
615
|
+
if (/^[A-Za-z0-9_./:-]+$/.test(value)) {
|
|
616
|
+
return value;
|
|
617
|
+
}
|
|
618
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
619
|
+
}
|
|
486
620
|
function printScanError(reason) {
|
|
487
621
|
console.error(pc.red("Qodfy could not scan this project."));
|
|
488
622
|
console.error("");
|
|
@@ -492,6 +626,15 @@ function printScanError(reason) {
|
|
|
492
626
|
console.error(pc.bold("Try:"));
|
|
493
627
|
console.error("qodfy scan --path ./my-next-app");
|
|
494
628
|
}
|
|
629
|
+
function printPromptError(reason) {
|
|
630
|
+
console.error(pc.red("Qodfy could not create this fix prompt."));
|
|
631
|
+
console.error("");
|
|
632
|
+
console.error(pc.bold("Reason:"));
|
|
633
|
+
console.error(reason);
|
|
634
|
+
console.error("");
|
|
635
|
+
console.error(pc.bold("Try:"));
|
|
636
|
+
console.error("qodfy scan --all");
|
|
637
|
+
}
|
|
495
638
|
function getErrorMessage(error) {
|
|
496
639
|
if (error instanceof Error && error.message) {
|
|
497
640
|
return error.message;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qodfy",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Open-source launch readiness scanner for AI-built apps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"qodfy",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@inquirer/prompts": "^8.4.3",
|
|
53
53
|
"commander": "^14.0.3",
|
|
54
54
|
"picocolors": "^1.1.1",
|
|
55
|
-
"@qodfy/core": "^0.2.
|
|
55
|
+
"@qodfy/core": "^0.2.3"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/node": "^25.7.0",
|