technical-debt-radar 1.10.0 → 1.12.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 +174 -290
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18867,7 +18867,7 @@ var require_cross_file_ai = __commonJS({
|
|
|
18867
18867
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
18868
18868
|
exports2.analyzeCrossFileWithAI = analyzeCrossFileWithAI2;
|
|
18869
18869
|
var sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
18870
|
-
var
|
|
18870
|
+
var MODEL3 = "claude-sonnet-4-20250514";
|
|
18871
18871
|
var MAX_TOKENS = 3e3;
|
|
18872
18872
|
async function analyzeCrossFileWithAI2(suspects, framework, orm, apiKey) {
|
|
18873
18873
|
if (suspects.length === 0)
|
|
@@ -18922,7 +18922,7 @@ ${suspectsText}
|
|
|
18922
18922
|
For each, determine if it's a real risk. Return JSON array.`;
|
|
18923
18923
|
try {
|
|
18924
18924
|
const response = await client.messages.create({
|
|
18925
|
-
model:
|
|
18925
|
+
model: MODEL3,
|
|
18926
18926
|
max_tokens: MAX_TOKENS,
|
|
18927
18927
|
system: systemPrompt,
|
|
18928
18928
|
messages: [{ role: "user", content: userPrompt }]
|
|
@@ -20725,6 +20725,18 @@ var RadarApiClient = class _RadarApiClient {
|
|
|
20725
20725
|
body: JSON.stringify(data)
|
|
20726
20726
|
});
|
|
20727
20727
|
}
|
|
20728
|
+
async requestFix(data) {
|
|
20729
|
+
return this.fetch("/cli/ai/fix", {
|
|
20730
|
+
method: "POST",
|
|
20731
|
+
body: JSON.stringify(data)
|
|
20732
|
+
});
|
|
20733
|
+
}
|
|
20734
|
+
async requestFixGrouped(data) {
|
|
20735
|
+
return this.fetch("/cli/ai/fix-grouped", {
|
|
20736
|
+
method: "POST",
|
|
20737
|
+
body: JSON.stringify(data)
|
|
20738
|
+
});
|
|
20739
|
+
}
|
|
20728
20740
|
};
|
|
20729
20741
|
|
|
20730
20742
|
// src/commands/scan.ts
|
|
@@ -21937,203 +21949,10 @@ async function validateCommand(options) {
|
|
|
21937
21949
|
var fs5 = __toESM(require("fs/promises"));
|
|
21938
21950
|
var fsSync2 = __toESM(require("fs"));
|
|
21939
21951
|
var path5 = __toESM(require("path"));
|
|
21952
|
+
var import_child_process2 = require("child_process");
|
|
21940
21953
|
var import_chalk4 = __toESM(require("chalk"));
|
|
21941
21954
|
var import_inquirer2 = __toESM(require("inquirer"));
|
|
21942
21955
|
var import_analyzers3 = __toESM(require_dist3());
|
|
21943
|
-
|
|
21944
|
-
// src/ai/fix-generator.ts
|
|
21945
|
-
var import_sdk3 = __toESM(require("@anthropic-ai/sdk"));
|
|
21946
|
-
var MODEL3 = "claude-sonnet-4-20250514";
|
|
21947
|
-
var MAX_FIX_TOKENS = 2e3;
|
|
21948
|
-
async function generateFix(request, apiKey) {
|
|
21949
|
-
const client = new import_sdk3.default({ apiKey });
|
|
21950
|
-
const ruleHint = getRuleSpecificHints(request.violation.ruleId, request.framework);
|
|
21951
|
-
const systemPrompt = `You are a senior Node.js/TypeScript developer fixing code violations found by a static analysis tool called Technical Debt Radar.
|
|
21952
|
-
|
|
21953
|
-
RULES:
|
|
21954
|
-
- Return ONLY the fixed code snippet, not the entire file
|
|
21955
|
-
- Keep the fix minimal \u2014 change only what's needed to resolve the violation
|
|
21956
|
-
- Preserve existing code style, indentation, and conventions
|
|
21957
|
-
- Add imports if needed (show them separately)
|
|
21958
|
-
- Use the project's framework patterns (${request.framework}, ${request.orm})
|
|
21959
|
-
${ruleHint ? `
|
|
21960
|
-
SPECIFIC GUIDANCE FOR THIS RULE:
|
|
21961
|
-
${ruleHint}` : ""}
|
|
21962
|
-
|
|
21963
|
-
RESPONSE FORMAT (JSON only, no markdown):
|
|
21964
|
-
{
|
|
21965
|
-
"originalCode": "the exact lines that need to change",
|
|
21966
|
-
"fixedCode": "the replacement code",
|
|
21967
|
-
"explanation": "one sentence explaining the fix",
|
|
21968
|
-
"startLine": <first line number to replace>,
|
|
21969
|
-
"endLine": <last line number to replace>,
|
|
21970
|
-
"newImports": ["import { X } from 'y'"] or [],
|
|
21971
|
-
"confidence": "high" | "medium" | "low"
|
|
21972
|
-
}`;
|
|
21973
|
-
const userPrompt = `Fix this violation:
|
|
21974
|
-
|
|
21975
|
-
RULE: ${request.violation.ruleId}
|
|
21976
|
-
MESSAGE: ${request.violation.message}
|
|
21977
|
-
FILE: ${request.violation.file}
|
|
21978
|
-
LINE: ${request.violation.line}
|
|
21979
|
-
SEVERITY: ${request.violation.severity}
|
|
21980
|
-
|
|
21981
|
-
SURROUNDING CODE (lines ${Math.max(1, request.violation.line - 15)}-${request.violation.line + 15}):
|
|
21982
|
-
\`\`\`typescript
|
|
21983
|
-
${request.surroundingCode}
|
|
21984
|
-
\`\`\`
|
|
21985
|
-
|
|
21986
|
-
FULL FILE (for context):
|
|
21987
|
-
\`\`\`typescript
|
|
21988
|
-
${request.fileContent}
|
|
21989
|
-
\`\`\`
|
|
21990
|
-
|
|
21991
|
-
Generate the minimal fix. Return JSON only.`;
|
|
21992
|
-
const response = await client.messages.create({
|
|
21993
|
-
model: MODEL3,
|
|
21994
|
-
max_tokens: MAX_FIX_TOKENS,
|
|
21995
|
-
system: systemPrompt,
|
|
21996
|
-
messages: [{ role: "user", content: userPrompt }]
|
|
21997
|
-
});
|
|
21998
|
-
const textBlock = response.content.find((b) => b.type === "text");
|
|
21999
|
-
if (!textBlock || textBlock.type !== "text") {
|
|
22000
|
-
throw new Error("No text content in AI response");
|
|
22001
|
-
}
|
|
22002
|
-
const cleaned = textBlock.text.replace(/^```json\s*/m, "").replace(/^```\s*/m, "").replace(/```\s*$/m, "").trim();
|
|
22003
|
-
const parsed = JSON.parse(cleaned);
|
|
22004
|
-
return {
|
|
22005
|
-
violation: request.violation,
|
|
22006
|
-
originalCode: String(parsed.originalCode || ""),
|
|
22007
|
-
fixedCode: String(parsed.fixedCode || ""),
|
|
22008
|
-
explanation: String(parsed.explanation || ""),
|
|
22009
|
-
startLine: Number(parsed.startLine),
|
|
22010
|
-
endLine: Number(parsed.endLine),
|
|
22011
|
-
newImports: Array.isArray(parsed.newImports) ? parsed.newImports.map(String) : [],
|
|
22012
|
-
confidence: validateConfidence(parsed.confidence)
|
|
22013
|
-
};
|
|
22014
|
-
}
|
|
22015
|
-
async function generateGroupedFix(request, apiKey) {
|
|
22016
|
-
const client = new import_sdk3.default({ apiKey });
|
|
22017
|
-
const ruleHints = request.violations.map((v) => getRuleSpecificHints(v.ruleId, request.framework)).filter(Boolean);
|
|
22018
|
-
const hintsBlock = ruleHints.length > 0 ? `
|
|
22019
|
-
SPECIFIC GUIDANCE:
|
|
22020
|
-
${ruleHints.join("\n")}` : "";
|
|
22021
|
-
const systemPrompt = `You are a senior Node.js/TypeScript developer fixing code violations found by a static analysis tool called Technical Debt Radar.
|
|
22022
|
-
|
|
22023
|
-
RULES:
|
|
22024
|
-
- Return ONLY the fixed code snippet, not the entire file
|
|
22025
|
-
- Keep the fix minimal \u2014 change only what's needed to resolve ALL violations listed
|
|
22026
|
-
- Preserve existing code style, indentation, and conventions
|
|
22027
|
-
- Add imports if needed (show them separately)
|
|
22028
|
-
- Use the project's framework patterns (${request.framework}, ${request.orm})
|
|
22029
|
-
- You MUST address EVERY violation listed \u2014 do not skip any
|
|
22030
|
-
${hintsBlock}
|
|
22031
|
-
|
|
22032
|
-
RESPONSE FORMAT (JSON only, no markdown):
|
|
22033
|
-
{
|
|
22034
|
-
"originalCode": "the exact lines that need to change",
|
|
22035
|
-
"fixedCode": "the replacement code",
|
|
22036
|
-
"explanation": "one sentence explaining the fix",
|
|
22037
|
-
"startLine": <first line number to replace>,
|
|
22038
|
-
"endLine": <last line number to replace>,
|
|
22039
|
-
"newImports": ["import { X } from 'y'"] or [],
|
|
22040
|
-
"confidence": "high" | "medium" | "low"
|
|
22041
|
-
}`;
|
|
22042
|
-
const violationList = request.violations.map((v, i) => ` ${i + 1}. [${v.ruleId}] ${v.message} (line ${v.line}, severity: ${v.severity})`).join("\n");
|
|
22043
|
-
const refLine = request.violations[0].line;
|
|
22044
|
-
const userPrompt = `Fix these ${request.violations.length} violations in the same code section:
|
|
22045
|
-
|
|
22046
|
-
${violationList}
|
|
22047
|
-
|
|
22048
|
-
FILE: ${request.violations[0].file}
|
|
22049
|
-
|
|
22050
|
-
SURROUNDING CODE (lines ${Math.max(1, refLine - 15)}-${refLine + 15}):
|
|
22051
|
-
\`\`\`typescript
|
|
22052
|
-
${request.surroundingCode}
|
|
22053
|
-
\`\`\`
|
|
22054
|
-
|
|
22055
|
-
FULL FILE (for context):
|
|
22056
|
-
\`\`\`typescript
|
|
22057
|
-
${request.fileContent}
|
|
22058
|
-
\`\`\`
|
|
22059
|
-
|
|
22060
|
-
Generate ONE fix that addresses ALL ${request.violations.length} violations. Return JSON only.`;
|
|
22061
|
-
const response = await client.messages.create({
|
|
22062
|
-
model: MODEL3,
|
|
22063
|
-
max_tokens: MAX_FIX_TOKENS,
|
|
22064
|
-
system: systemPrompt,
|
|
22065
|
-
messages: [{ role: "user", content: userPrompt }]
|
|
22066
|
-
});
|
|
22067
|
-
const textBlock = response.content.find((b) => b.type === "text");
|
|
22068
|
-
if (!textBlock || textBlock.type !== "text") {
|
|
22069
|
-
throw new Error("No text content in AI response");
|
|
22070
|
-
}
|
|
22071
|
-
const cleaned = textBlock.text.replace(/^```json\s*/m, "").replace(/^```\s*/m, "").replace(/```\s*$/m, "").trim();
|
|
22072
|
-
const parsed = JSON.parse(cleaned);
|
|
22073
|
-
return {
|
|
22074
|
-
violation: request.violations[0],
|
|
22075
|
-
originalCode: String(parsed.originalCode || ""),
|
|
22076
|
-
fixedCode: String(parsed.fixedCode || ""),
|
|
22077
|
-
explanation: String(parsed.explanation || ""),
|
|
22078
|
-
startLine: Number(parsed.startLine),
|
|
22079
|
-
endLine: Number(parsed.endLine),
|
|
22080
|
-
newImports: Array.isArray(parsed.newImports) ? parsed.newImports.map(String) : [],
|
|
22081
|
-
confidence: validateConfidence(parsed.confidence)
|
|
22082
|
-
};
|
|
22083
|
-
}
|
|
22084
|
-
function validateConfidence(value) {
|
|
22085
|
-
if (value === "high" || value === "medium" || value === "low") {
|
|
22086
|
-
return value;
|
|
22087
|
-
}
|
|
22088
|
-
return "medium";
|
|
22089
|
-
}
|
|
22090
|
-
function getRuleSpecificHints(ruleId, framework) {
|
|
22091
|
-
const hints = {
|
|
22092
|
-
"missing-null-guard": `Add a null/undefined check after the query. In ${framework === "NestJS" ? "NestJS, throw new NotFoundException()" : 'Express, return res.status(404).json({ error: "Not found" })'}. Check BEFORE any property access.`,
|
|
22093
|
-
"sync-fs-in-handler": "Replace fs.readFileSync/writeFileSync with await fs.promises.readFile/writeFile. Make the function async if needed.",
|
|
22094
|
-
"sync-crypto": "Replace crypto.pbkdf2Sync with the async crypto.pbkdf2 wrapped in a Promise, or use argon2/bcrypt async alternatives.",
|
|
22095
|
-
"sync-compression": "Replace zlib.deflateSync/gzipSync with the async stream or callback versions.",
|
|
22096
|
-
"redos-vulnerable-regex": "Rewrite the regex to avoid nested quantifiers. Remove patterns like (a+)+, (a|b)*, or use a regex linting library.",
|
|
22097
|
-
"n-plus-one-query": "Replace the loop + individual query pattern with a batch query. Use include/join/populate for relations, or WHERE IN for ID lists.",
|
|
22098
|
-
"unbounded-find-many": "Add pagination: { take: 20, skip: offset } for Prisma, { limit: 20, offset } for Sequelize, .limit(20) for Mongoose/Drizzle/Knex.",
|
|
22099
|
-
"find-many-no-where": "Add a where/filter clause to prevent full table scans.",
|
|
22100
|
-
"raw-sql-no-limit": "Add LIMIT clause to the SQL query.",
|
|
22101
|
-
"unhandled-promise-rejection": "Add await before the async call, or explicitly handle the promise with .catch().",
|
|
22102
|
-
"external-call-no-timeout": "Add { timeout: 10000 } (10s) to the HTTP client config. For axios: axios.post(url, data, { timeout: 10000 }). For fetch: use AbortController.",
|
|
22103
|
-
"empty-catch-block": "Add error logging inside the catch block: console.error(error) or logger.error(error).",
|
|
22104
|
-
"retry-without-backoff": "Replace fixed delay with exponential backoff: delay * Math.pow(2, attempt).",
|
|
22105
|
-
"missing-error-logging": "Add a logging statement in the catch block.",
|
|
22106
|
-
"missing-try-catch": "Wrap the multiple await calls in a try/catch block with proper error handling.",
|
|
22107
|
-
"transaction-no-timeout": "Add a timeout option to the transaction: { timeout: 5000 }.",
|
|
22108
|
-
"unfiltered-count-large-table": "Add a where/filter clause to the count query."
|
|
22109
|
-
};
|
|
22110
|
-
return hints[ruleId] || "";
|
|
22111
|
-
}
|
|
22112
|
-
|
|
22113
|
-
// src/commands/fix.ts
|
|
22114
|
-
function buildFixInstruction(violation) {
|
|
22115
|
-
const lines = [];
|
|
22116
|
-
lines.push(`In ${violation.file} line ${violation.line}:`);
|
|
22117
|
-
if (violation.suggestion) {
|
|
22118
|
-
lines.push(` ${violation.suggestion}`);
|
|
22119
|
-
} else {
|
|
22120
|
-
lines.push(` ${violation.message}`);
|
|
22121
|
-
}
|
|
22122
|
-
return lines.join("\n");
|
|
22123
|
-
}
|
|
22124
|
-
function formatFixPrompt(violations) {
|
|
22125
|
-
if (violations.length === 0) {
|
|
22126
|
-
return "No issues found \u2014 nothing to fix.";
|
|
22127
|
-
}
|
|
22128
|
-
const lines = [];
|
|
22129
|
-
lines.push("Fix these issues in my codebase:");
|
|
22130
|
-
lines.push("");
|
|
22131
|
-
violations.forEach((v, i) => {
|
|
22132
|
-
lines.push(`${i + 1}. ${buildFixInstruction(v)}`);
|
|
22133
|
-
lines.push("");
|
|
22134
|
-
});
|
|
22135
|
-
return lines.join("\n").trimEnd();
|
|
22136
|
-
}
|
|
22137
21956
|
function applyFix(filePath, fix) {
|
|
22138
21957
|
const content = fsSync2.readFileSync(filePath, "utf-8");
|
|
22139
21958
|
const lines = content.split("\n");
|
|
@@ -22185,32 +22004,37 @@ async function runScan(targetPath, options) {
|
|
|
22185
22004
|
policy
|
|
22186
22005
|
};
|
|
22187
22006
|
}
|
|
22188
|
-
async function
|
|
22189
|
-
|
|
22190
|
-
|
|
22191
|
-
|
|
22192
|
-
|
|
22193
|
-
|
|
22007
|
+
async function quickScanFile(targetPath, filePath, options) {
|
|
22008
|
+
try {
|
|
22009
|
+
const result = await runScan(targetPath, { ...options, file: filePath });
|
|
22010
|
+
return result.violations;
|
|
22011
|
+
} catch {
|
|
22012
|
+
return [];
|
|
22194
22013
|
}
|
|
22195
|
-
|
|
22196
|
-
|
|
22197
|
-
|
|
22198
|
-
|
|
22199
|
-
|
|
22200
|
-
|
|
22201
|
-
|
|
22202
|
-
|
|
22203
|
-
|
|
22204
|
-
|
|
22205
|
-
if (
|
|
22206
|
-
if (
|
|
22207
|
-
return
|
|
22208
|
-
}
|
|
22209
|
-
|
|
22210
|
-
|
|
22014
|
+
}
|
|
22015
|
+
function resolveViolationPath(vFile, projectRoot) {
|
|
22016
|
+
if (path5.isAbsolute(vFile)) return vFile;
|
|
22017
|
+
return path5.resolve(projectRoot, vFile);
|
|
22018
|
+
}
|
|
22019
|
+
function detectTestCommand(projectRoot) {
|
|
22020
|
+
try {
|
|
22021
|
+
const pkg = JSON.parse(fsSync2.readFileSync(path5.join(projectRoot, "package.json"), "utf-8"));
|
|
22022
|
+
const scripts = pkg.scripts ?? {};
|
|
22023
|
+
if (scripts.test && scripts.test !== 'echo "Error: no test specified" && exit 1') return "npm test";
|
|
22024
|
+
if (scripts["test:unit"]) return "npm run test:unit";
|
|
22025
|
+
if (scripts.jest) return "npm run jest";
|
|
22026
|
+
return null;
|
|
22027
|
+
} catch {
|
|
22028
|
+
return null;
|
|
22029
|
+
}
|
|
22030
|
+
}
|
|
22031
|
+
function runTests(projectRoot, testCmd) {
|
|
22032
|
+
try {
|
|
22033
|
+
(0, import_child_process2.execSync)(testCmd, { cwd: projectRoot, stdio: "pipe", timeout: 12e4 });
|
|
22034
|
+
return true;
|
|
22035
|
+
} catch {
|
|
22036
|
+
return false;
|
|
22211
22037
|
}
|
|
22212
|
-
const output = formatFixPrompt(violations);
|
|
22213
|
-
console.log(output);
|
|
22214
22038
|
}
|
|
22215
22039
|
function groupBy(items, key) {
|
|
22216
22040
|
const result = {};
|
|
@@ -22224,22 +22048,34 @@ function groupBy(items, key) {
|
|
|
22224
22048
|
async function fixCommand(targetPath, options) {
|
|
22225
22049
|
const client = RadarApiClient.fromConfigOrEnv();
|
|
22226
22050
|
if (!client) {
|
|
22227
|
-
console.error(import_chalk4.default.red("Authentication required. Run: radar login"));
|
|
22051
|
+
console.error(import_chalk4.default.red("\u274C Authentication required. Run: radar login"));
|
|
22228
22052
|
process.exit(1);
|
|
22229
22053
|
}
|
|
22230
|
-
|
|
22231
|
-
|
|
22232
|
-
|
|
22233
|
-
|
|
22234
|
-
|
|
22235
|
-
|
|
22236
|
-
|
|
22237
|
-
|
|
22238
|
-
|
|
22239
|
-
|
|
22240
|
-
|
|
22241
|
-
|
|
22242
|
-
|
|
22054
|
+
let verified;
|
|
22055
|
+
try {
|
|
22056
|
+
verified = await client.verifyToken();
|
|
22057
|
+
} catch (err) {
|
|
22058
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
22059
|
+
if (msg.includes("Network error")) {
|
|
22060
|
+
console.error(import_chalk4.default.red("\u26A0\uFE0F Could not reach Radar API. Check your connection."));
|
|
22061
|
+
} else {
|
|
22062
|
+
console.error(import_chalk4.default.red("\u274C Authentication failed. Run: radar login"));
|
|
22063
|
+
}
|
|
22064
|
+
process.exit(1);
|
|
22065
|
+
}
|
|
22066
|
+
if (!verified?.valid) {
|
|
22067
|
+
console.error(import_chalk4.default.red("\u274C Invalid or expired token. Run: radar login"));
|
|
22068
|
+
process.exit(1);
|
|
22069
|
+
}
|
|
22070
|
+
const projectRoot = path5.resolve(targetPath);
|
|
22071
|
+
let testCmd = null;
|
|
22072
|
+
if (options.safe) {
|
|
22073
|
+
testCmd = detectTestCommand(projectRoot);
|
|
22074
|
+
if (!testCmd) {
|
|
22075
|
+
console.log(import_chalk4.default.yellow("\u26A0\uFE0F No test command found in package.json. --safe will skip test verification."));
|
|
22076
|
+
} else {
|
|
22077
|
+
console.log(import_chalk4.default.dim(` Test command: ${testCmd}`));
|
|
22078
|
+
}
|
|
22243
22079
|
}
|
|
22244
22080
|
console.log(import_chalk4.default.blue("Scanning for violations..."));
|
|
22245
22081
|
const scanResult = await runScan(targetPath, {
|
|
@@ -22261,26 +22097,30 @@ async function fixCommand(targetPath, options) {
|
|
|
22261
22097
|
}
|
|
22262
22098
|
console.log(import_chalk4.default.yellow(`Found ${violations.length} violations. Generating AI fixes...
|
|
22263
22099
|
`));
|
|
22264
|
-
const byFile = groupBy(violations, (v) => v.file);
|
|
22100
|
+
const byFile = groupBy(violations, (v) => resolveViolationPath(v.file, projectRoot));
|
|
22265
22101
|
let totalFixed = 0;
|
|
22266
22102
|
let totalSkipped = 0;
|
|
22267
22103
|
let totalFailed = 0;
|
|
22268
|
-
let
|
|
22104
|
+
let totalReverted = 0;
|
|
22105
|
+
let totalCreditsUsed = 0;
|
|
22106
|
+
let creditsRemaining = -1;
|
|
22107
|
+
let fixCount = 0;
|
|
22269
22108
|
let applyAll = false;
|
|
22270
22109
|
let quit = false;
|
|
22271
22110
|
const framework = scanResult.config?.stack?.framework || "NestJS";
|
|
22272
22111
|
const orm = scanResult.config?.stack?.orm || "Prisma";
|
|
22273
22112
|
for (const [filePath, fileViolations] of Object.entries(byFile)) {
|
|
22274
22113
|
if (quit) break;
|
|
22275
|
-
const
|
|
22114
|
+
const absPath = path5.resolve(filePath);
|
|
22115
|
+
const relativePath = path5.relative(projectRoot, absPath);
|
|
22276
22116
|
console.log(import_chalk4.default.bold(`
|
|
22277
22117
|
${relativePath} (${fileViolations.length} violations)
|
|
22278
22118
|
`));
|
|
22279
22119
|
let fileContent;
|
|
22280
22120
|
try {
|
|
22281
|
-
fileContent = await fs5.readFile(
|
|
22121
|
+
fileContent = await fs5.readFile(absPath, "utf-8");
|
|
22282
22122
|
} catch {
|
|
22283
|
-
console.log(import_chalk4.default.red(` Could not read file: ${
|
|
22123
|
+
console.log(import_chalk4.default.red(` Could not read file: ${absPath}`));
|
|
22284
22124
|
totalFailed += fileViolations.length;
|
|
22285
22125
|
continue;
|
|
22286
22126
|
}
|
|
@@ -22297,9 +22137,9 @@ ${relativePath} (${fileViolations.length} violations)
|
|
|
22297
22137
|
const lineGroups = [...byLine.entries()].sort((a, b) => b[0] - a[0]);
|
|
22298
22138
|
for (const [lineNum, lineViolations] of lineGroups) {
|
|
22299
22139
|
if (quit) break;
|
|
22300
|
-
if (
|
|
22140
|
+
if (options.maxFixes > 0 && fixCount >= options.maxFixes) {
|
|
22301
22141
|
console.log(import_chalk4.default.yellow(`
|
|
22302
|
-
|
|
22142
|
+
Fix limit reached (${options.maxFixes}). Stopping.`));
|
|
22303
22143
|
quit = true;
|
|
22304
22144
|
break;
|
|
22305
22145
|
}
|
|
@@ -22318,46 +22158,43 @@ ${relativePath} (${fileViolations.length} violations)
|
|
|
22318
22158
|
}
|
|
22319
22159
|
console.log("");
|
|
22320
22160
|
try {
|
|
22161
|
+
console.log(import_chalk4.default.dim(" Requesting AI fix from Radar..."));
|
|
22321
22162
|
let fix;
|
|
22322
22163
|
if (lineViolations.length > 1) {
|
|
22323
|
-
fix = await
|
|
22324
|
-
{
|
|
22325
|
-
|
|
22326
|
-
|
|
22327
|
-
|
|
22328
|
-
|
|
22329
|
-
|
|
22330
|
-
|
|
22331
|
-
|
|
22332
|
-
|
|
22333
|
-
|
|
22334
|
-
|
|
22335
|
-
|
|
22336
|
-
|
|
22337
|
-
},
|
|
22338
|
-
apiKey
|
|
22339
|
-
);
|
|
22164
|
+
fix = await client.requestFixGrouped({
|
|
22165
|
+
violations: lineViolations.map((v) => ({
|
|
22166
|
+
file: v.file,
|
|
22167
|
+
line: v.line,
|
|
22168
|
+
ruleId: v.ruleId,
|
|
22169
|
+
message: v.message,
|
|
22170
|
+
severity: v.severity,
|
|
22171
|
+
category: v.category
|
|
22172
|
+
})),
|
|
22173
|
+
fileContent,
|
|
22174
|
+
surroundingCode,
|
|
22175
|
+
framework,
|
|
22176
|
+
orm
|
|
22177
|
+
});
|
|
22340
22178
|
} else {
|
|
22341
22179
|
const violation = lineViolations[0];
|
|
22342
|
-
fix = await
|
|
22343
|
-
{
|
|
22344
|
-
|
|
22345
|
-
|
|
22346
|
-
|
|
22347
|
-
|
|
22348
|
-
|
|
22349
|
-
|
|
22350
|
-
category: violation.category
|
|
22351
|
-
},
|
|
22352
|
-
fileContent,
|
|
22353
|
-
surroundingCode,
|
|
22354
|
-
framework,
|
|
22355
|
-
orm
|
|
22180
|
+
fix = await client.requestFix({
|
|
22181
|
+
violation: {
|
|
22182
|
+
file: violation.file,
|
|
22183
|
+
line: violation.line,
|
|
22184
|
+
ruleId: violation.ruleId,
|
|
22185
|
+
message: violation.message,
|
|
22186
|
+
severity: violation.severity,
|
|
22187
|
+
category: violation.category
|
|
22356
22188
|
},
|
|
22357
|
-
|
|
22358
|
-
|
|
22189
|
+
fileContent,
|
|
22190
|
+
surroundingCode,
|
|
22191
|
+
framework,
|
|
22192
|
+
orm
|
|
22193
|
+
});
|
|
22359
22194
|
}
|
|
22360
|
-
|
|
22195
|
+
totalCreditsUsed += fix.creditsUsed;
|
|
22196
|
+
creditsRemaining = fix.creditsRemaining;
|
|
22197
|
+
fixCount++;
|
|
22361
22198
|
console.log(import_chalk4.default.red(" BEFORE:"));
|
|
22362
22199
|
fix.originalCode.split("\n").forEach((l) => console.log(import_chalk4.default.red(` - ${l}`)));
|
|
22363
22200
|
console.log("");
|
|
@@ -22366,14 +22203,15 @@ ${relativePath} (${fileViolations.length} violations)
|
|
|
22366
22203
|
console.log("");
|
|
22367
22204
|
console.log(import_chalk4.default.dim(` ${fix.explanation}`));
|
|
22368
22205
|
console.log(import_chalk4.default.dim(` Confidence: ${fix.confidence}`));
|
|
22206
|
+
console.log(import_chalk4.default.cyan(` ${fix.creditsUsed} AI credit used (${fix.creditsRemaining} remaining)`));
|
|
22369
22207
|
console.log("");
|
|
22370
22208
|
let shouldApply = false;
|
|
22371
22209
|
if (options.dryRun) {
|
|
22372
22210
|
console.log(import_chalk4.default.dim(" [dry-run] Would apply this fix"));
|
|
22373
22211
|
totalSkipped += lineViolations.length;
|
|
22374
|
-
} else if (applyAll || options.auto
|
|
22212
|
+
} else if (applyAll || options.auto) {
|
|
22375
22213
|
shouldApply = true;
|
|
22376
|
-
console.log(import_chalk4.default.green(" Auto-applying..."));
|
|
22214
|
+
if (!options.auto) console.log(import_chalk4.default.green(" Auto-applying (all)..."));
|
|
22377
22215
|
} else {
|
|
22378
22216
|
const { action } = await import_inquirer2.default.prompt([
|
|
22379
22217
|
{
|
|
@@ -22403,16 +22241,58 @@ ${relativePath} (${fileViolations.length} violations)
|
|
|
22403
22241
|
}
|
|
22404
22242
|
}
|
|
22405
22243
|
if (shouldApply) {
|
|
22406
|
-
|
|
22407
|
-
|
|
22408
|
-
console.log(import_chalk4.default.green(`
|
|
22409
|
-
|
|
22244
|
+
const originalContent = options.safe ? fileContent : null;
|
|
22245
|
+
applyFix(absPath, fix);
|
|
22246
|
+
console.log(import_chalk4.default.green(` Applied fix (${lineViolations.length} violation${lineViolations.length > 1 ? "s" : ""})`));
|
|
22247
|
+
const remaining = await quickScanFile(targetPath, absPath, {
|
|
22248
|
+
config: options.config,
|
|
22249
|
+
rules: options.rules
|
|
22250
|
+
});
|
|
22251
|
+
const stillPresent = remaining.some(
|
|
22252
|
+
(rv) => lineViolations.some(
|
|
22253
|
+
(lv) => rv.ruleId === lv.ruleId && rv.file === lv.file && Math.abs(rv.line - lv.line) <= 3
|
|
22254
|
+
)
|
|
22255
|
+
);
|
|
22256
|
+
if (stillPresent) {
|
|
22257
|
+
console.log(import_chalk4.default.yellow(" \u26A0\uFE0F Fix applied but violation persists \u2014 manual review needed"));
|
|
22258
|
+
} else {
|
|
22259
|
+
console.log(import_chalk4.default.green(" \u2705 Violation resolved"));
|
|
22260
|
+
}
|
|
22261
|
+
if (options.safe && testCmd) {
|
|
22262
|
+
console.log(import_chalk4.default.dim(` Running tests: ${testCmd}...`));
|
|
22263
|
+
const testsPassed = runTests(projectRoot, testCmd);
|
|
22264
|
+
if (testsPassed) {
|
|
22265
|
+
console.log(import_chalk4.default.green(" \u2705 Tests passed"));
|
|
22266
|
+
totalFixed += lineViolations.length;
|
|
22267
|
+
} else {
|
|
22268
|
+
console.log(import_chalk4.default.red(" \u274C Tests failed \u2014 reverting fix"));
|
|
22269
|
+
fsSync2.writeFileSync(absPath, originalContent, "utf-8");
|
|
22270
|
+
totalReverted += lineViolations.length;
|
|
22271
|
+
}
|
|
22272
|
+
} else {
|
|
22273
|
+
totalFixed += lineViolations.length;
|
|
22274
|
+
}
|
|
22275
|
+
fileContent = await fs5.readFile(absPath, "utf-8");
|
|
22410
22276
|
lines = fileContent.split("\n");
|
|
22411
22277
|
}
|
|
22412
22278
|
} catch (error) {
|
|
22413
22279
|
const message = error instanceof Error ? error.message : String(error);
|
|
22414
|
-
|
|
22415
|
-
|
|
22280
|
+
if (message.includes("403") && message.includes("credits exhausted")) {
|
|
22281
|
+
console.log(import_chalk4.default.red(" \u274C AI credits exhausted. Upgrade for more: radar upgrade"));
|
|
22282
|
+
quit = true;
|
|
22283
|
+
break;
|
|
22284
|
+
} else if (message.includes("403") && message.includes("Solo")) {
|
|
22285
|
+
console.log(import_chalk4.default.red(" \u274C radar fix requires Solo plan or higher. Upgrade: radar upgrade"));
|
|
22286
|
+
quit = true;
|
|
22287
|
+
break;
|
|
22288
|
+
} else if (message.includes("401")) {
|
|
22289
|
+
console.log(import_chalk4.default.red(" \u274C Authentication failed. Run: radar login"));
|
|
22290
|
+
quit = true;
|
|
22291
|
+
break;
|
|
22292
|
+
} else {
|
|
22293
|
+
console.log(import_chalk4.default.red(` AI fix failed: ${message}`));
|
|
22294
|
+
totalFailed += lineViolations.length;
|
|
22295
|
+
}
|
|
22416
22296
|
}
|
|
22417
22297
|
}
|
|
22418
22298
|
}
|
|
@@ -22420,13 +22300,17 @@ ${relativePath} (${fileViolations.length} violations)
|
|
|
22420
22300
|
console.log(import_chalk4.default.bold("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
|
|
22421
22301
|
console.log(import_chalk4.default.bold(" FIX SUMMARY"));
|
|
22422
22302
|
console.log(import_chalk4.default.bold("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
|
|
22423
|
-
console.log(import_chalk4.default.green(` Fixed:
|
|
22424
|
-
console.log(import_chalk4.default.yellow(` Skipped:
|
|
22425
|
-
if (
|
|
22426
|
-
console.log(import_chalk4.default.
|
|
22303
|
+
console.log(import_chalk4.default.green(` Fixed: ${totalFixed}`));
|
|
22304
|
+
console.log(import_chalk4.default.yellow(` Skipped: ${totalSkipped}`));
|
|
22305
|
+
if (totalReverted > 0) console.log(import_chalk4.default.red(` Reverted: ${totalReverted} (tests failed)`));
|
|
22306
|
+
if (totalFailed > 0) console.log(import_chalk4.default.red(` Failed: ${totalFailed}`));
|
|
22307
|
+
console.log(import_chalk4.default.cyan(` AI credits used: ${totalCreditsUsed}`));
|
|
22308
|
+
if (creditsRemaining >= 0) {
|
|
22309
|
+
console.log(import_chalk4.default.cyan(` Credits remaining: ${creditsRemaining}`));
|
|
22310
|
+
}
|
|
22427
22311
|
console.log("");
|
|
22428
22312
|
if (totalFixed > 0 && !options.dryRun) {
|
|
22429
|
-
console.log(import_chalk4.default.blue("Re-scanning to verify fixes...\n"));
|
|
22313
|
+
console.log(import_chalk4.default.blue("Re-scanning to verify all fixes...\n"));
|
|
22430
22314
|
const newResult = await runScan(targetPath, {
|
|
22431
22315
|
config: options.config,
|
|
22432
22316
|
rules: options.rules,
|
|
@@ -22817,10 +22701,10 @@ program.command("check <file>").description("Check a single file against policy"
|
|
|
22817
22701
|
program.command("init").description("Generate radar.yml + rules.yml from project structure").option("--path <dir>", "Target directory", ".").option("--architecture <pattern>", "Force architecture: ddd, hexagonal, clean, layered, mvc, event-driven").option("--regenerate-rules", "Regenerate rules.yml from existing radar.yml", false).option("--no-ai", "Skip AI refinement (deterministic only)").action(async (options) => {
|
|
22818
22702
|
await initCommand(options);
|
|
22819
22703
|
});
|
|
22820
|
-
program.command("fix <path>").description("AI-powered fix: generates code diffs, applies with confirmation").option("-c, --config <path>", "Path to radar.yml", "./radar.yml").option("-r, --rules <path>", "Path to rules.yml (default: ./rules.yml)").option("--severity <level>", "Only include: critical, all", "all").option("--dry-run", "Show fixes without applying", false).option("--auto", "Apply all
|
|
22704
|
+
program.command("fix <path>").description("AI-powered fix: generates code diffs, applies with confirmation").option("-c, --config <path>", "Path to radar.yml", "./radar.yml").option("-r, --rules <path>", "Path to rules.yml (default: ./rules.yml)").option("--severity <level>", "Only include: critical, all", "all").option("--dry-run", "Show fixes without applying", false).option("--auto", "Apply all fixes automatically without asking", false).option("--safe", "Run tests after each fix, revert on failure", false).option("--file <file>", "Fix single file only").option("--max-fixes <count>", "Max number of AI fixes to generate (0 = unlimited)", "0").action(async (targetPath, options) => {
|
|
22821
22705
|
await fixCommand(targetPath, {
|
|
22822
22706
|
...options,
|
|
22823
|
-
|
|
22707
|
+
maxFixes: parseInt(options.maxFixes, 10)
|
|
22824
22708
|
});
|
|
22825
22709
|
});
|
|
22826
22710
|
program.command("validate").description("Validate radar.yml + rules.yml syntax and cross-references").option("-c, --config <path>", "Path to radar.yml", "./radar.yml").option("-r, --rules <path>", "Path to rules.yml (default: ./rules.yml)").action(async (options) => {
|