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.
Files changed (2) hide show
  1. package/dist/index.js +174 -290
  2. 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 MODEL4 = "claude-sonnet-4-20250514";
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: MODEL4,
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 runTextOnlyFix(targetPath, options) {
22189
- const { compiled: policy } = await loadPolicy(options.config, options.rules);
22190
- const files = await collectTsFiles(targetPath);
22191
- if (files.length === 0) {
22192
- console.log(import_chalk4.default.yellow("No .ts/.tsx files found."));
22193
- process.exit(0);
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
- const changedFiles = await Promise.all(
22196
- files.map(async (filePath) => ({
22197
- path: filePath,
22198
- content: await fs5.readFile(filePath, "utf-8"),
22199
- status: "added"
22200
- }))
22201
- );
22202
- const input = { changedFiles, policy, headSha: "local", projectRoot: path5.resolve(targetPath) };
22203
- const result = await (0, import_analyzers3.runFullAnalysis)(input);
22204
- let violations = result.violations.filter((v) => v.severity === "critical" || v.severity === "warning").sort((a, b) => {
22205
- if (a.severity === "critical" && b.severity !== "critical") return -1;
22206
- if (a.severity !== "critical" && b.severity === "critical") return 1;
22207
- return b.debtPoints - a.debtPoints;
22208
- });
22209
- if (options.severity === "critical") {
22210
- violations = violations.filter((v) => v.severity === "critical");
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
- const apiKey = options.apiKey || process.env.ANTHROPIC_API_KEY;
22231
- if (!apiKey) {
22232
- console.log(import_chalk4.default.yellow("No API key found."));
22233
- console.log(" Set ANTHROPIC_API_KEY environment variable");
22234
- console.log(" Or use: radar fix --api-key sk-ant-...");
22235
- console.log("");
22236
- console.log(import_chalk4.default.dim("Tip: Set ANTHROPIC_API_KEY for AI-powered auto-fixes"));
22237
- console.log(import_chalk4.default.dim(" export ANTHROPIC_API_KEY=sk-ant-..."));
22238
- console.log(import_chalk4.default.dim(" Then run: radar fix ."));
22239
- console.log("");
22240
- console.log(import_chalk4.default.dim("Without AI, showing fix instructions only:"));
22241
- console.log("");
22242
- return runTextOnlyFix(targetPath, options);
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 totalCost = 0;
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 relativePath = path5.relative(path5.resolve(targetPath), filePath);
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(filePath, "utf-8");
22121
+ fileContent = await fs5.readFile(absPath, "utf-8");
22282
22122
  } catch {
22283
- console.log(import_chalk4.default.red(` Could not read file: ${filePath}`));
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 (totalCost >= options.maxCost) {
22140
+ if (options.maxFixes > 0 && fixCount >= options.maxFixes) {
22301
22141
  console.log(import_chalk4.default.yellow(`
22302
- Cost ceiling reached ($${totalCost.toFixed(2)}). Stopping AI fixes.`));
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 generateGroupedFix(
22324
- {
22325
- violations: lineViolations.map((v) => ({
22326
- file: v.file,
22327
- line: v.line,
22328
- ruleId: v.ruleId,
22329
- message: v.message,
22330
- severity: v.severity,
22331
- category: v.category
22332
- })),
22333
- fileContent,
22334
- surroundingCode,
22335
- framework,
22336
- orm
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 generateFix(
22343
- {
22344
- violation: {
22345
- file: violation.file,
22346
- line: violation.line,
22347
- ruleId: violation.ruleId,
22348
- message: violation.message,
22349
- severity: violation.severity,
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
- apiKey
22358
- );
22189
+ fileContent,
22190
+ surroundingCode,
22191
+ framework,
22192
+ orm
22193
+ });
22359
22194
  }
22360
- totalCost += 3e-3 * lineViolations.length;
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 && fix.confidence === "high") {
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
- applyFix(filePath, fix);
22407
- totalFixed += lineViolations.length;
22408
- console.log(import_chalk4.default.green(` Fixed! (${lineViolations.length} violation${lineViolations.length > 1 ? "s" : ""})`));
22409
- fileContent = await fs5.readFile(filePath, "utf-8");
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
- console.log(import_chalk4.default.red(` AI fix failed: ${message}`));
22415
- totalFailed += lineViolations.length;
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: ${totalFixed}`));
22424
- console.log(import_chalk4.default.yellow(` Skipped: ${totalSkipped}`));
22425
- if (totalFailed > 0) console.log(import_chalk4.default.red(` Failed: ${totalFailed}`));
22426
- console.log(import_chalk4.default.dim(` AI cost: $${totalCost.toFixed(3)}`));
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 high-confidence fixes without asking", false).option("--file <file>", "Fix single file only").option("--max-cost <amount>", "Max AI cost in dollars", "1.00").option("--api-key <key>", "Anthropic API key (or ANTHROPIC_API_KEY env var)").action(async (targetPath, options) => {
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
- maxCost: parseFloat(options.maxCost)
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "technical-debt-radar",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
4
4
  "description": "Stop Node.js production crashes before merge. 47 detection patterns across 5 categories.",
5
5
  "bin": {
6
6
  "radar": "dist/index.js",