karajan-code 1.48.0 → 1.50.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 (55) hide show
  1. package/package.json +1 -1
  2. package/src/agents/resolve-bin.js +1 -1
  3. package/src/audit/basal-cost.js +26 -23
  4. package/src/bootstrap.js +2 -2
  5. package/src/commands/agents.js +1 -1
  6. package/src/commands/board.js +3 -3
  7. package/src/commands/code.js +1 -1
  8. package/src/commands/doctor.js +4 -4
  9. package/src/commands/init.js +2 -2
  10. package/src/commands/report.js +1 -1
  11. package/src/commands/review.js +1 -1
  12. package/src/commands/roles.js +1 -1
  13. package/src/hu/store.js +9 -5
  14. package/src/mcp/handlers/direct-handlers.js +393 -0
  15. package/src/mcp/handlers/hu-handlers.js +93 -0
  16. package/src/mcp/handlers/management-handlers.js +182 -0
  17. package/src/mcp/handlers/run-handler.js +283 -0
  18. package/src/mcp/orphan-guard.js +1 -1
  19. package/src/mcp/server-handlers.js +41 -871
  20. package/src/mcp/suggest-handler.js +2 -2
  21. package/src/orchestrator/hu-sub-pipeline.js +1 -1
  22. package/src/orchestrator/iteration-stages.js +9 -890
  23. package/src/orchestrator/pre-loop-stages.js +8 -906
  24. package/src/orchestrator/preflight-checks.js +13 -19
  25. package/src/orchestrator/stages/architect-stage.js +176 -0
  26. package/src/orchestrator/stages/coder-stage.js +307 -0
  27. package/src/orchestrator/stages/hu-reviewer-stage.js +467 -0
  28. package/src/orchestrator/stages/planner-stage.js +90 -0
  29. package/src/orchestrator/stages/research-stage.js +74 -0
  30. package/src/orchestrator/stages/reviewer-stage.js +350 -0
  31. package/src/orchestrator/stages/sonar-stage.js +267 -0
  32. package/src/orchestrator/stages/triage-stage.js +137 -0
  33. package/src/plugins/loader.js +1 -1
  34. package/src/prompts/architect.js +1 -1
  35. package/src/review/parser.js +3 -3
  36. package/src/roles/architect-role.js +1 -1
  37. package/src/roles/audit-role.js +3 -3
  38. package/src/roles/base-role.js +1 -1
  39. package/src/roles/discover-role.js +1 -1
  40. package/src/roles/hu-reviewer-role.js +1 -1
  41. package/src/roles/researcher-role.js +1 -1
  42. package/src/roles/triage-role.js +1 -1
  43. package/src/session-cleanup.js +1 -1
  44. package/src/session-store.js +2 -2
  45. package/src/skills/openskills-client.js +1 -1
  46. package/src/skills/skill-detector.js +2 -2
  47. package/src/sonar/api.js +4 -3
  48. package/src/sonar/config-resolver.js +69 -0
  49. package/src/sonar/credentials.js +1 -1
  50. package/src/sonar/project-key.js +1 -1
  51. package/src/sonar/scanner.js +13 -21
  52. package/src/utils/fs.js +1 -1
  53. package/src/utils/json-extract.js +1 -1
  54. package/src/utils/project-detect.js +1 -1
  55. package/src/utils/rtk-detect.js +1 -1
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Triage stage logic.
3
+ * Extracted from pre-loop-stages.js for maintainability.
4
+ */
5
+
6
+ import { TriageRole } from "../../roles/triage-role.js";
7
+ import { addCheckpoint } from "../../session-store.js";
8
+ import { emitProgress, makeEvent } from "../../utils/events.js";
9
+ import { selectModelsForRoles } from "../../utils/model-selector.js";
10
+ import { createStallDetector } from "../../utils/stall-detector.js";
11
+
12
+ const ROLE_NAMES = ["planner", "researcher", "architect", "refactorer", "reviewer", "tester", "security", "impeccable"];
13
+
14
+ function buildRoleOverrides(recommendedRoles, pipelineConfig) {
15
+ const overrides = {};
16
+ for (const role of ROLE_NAMES) {
17
+ overrides[`${role}Enabled`] = recommendedRoles.has(role) || Boolean(pipelineConfig[role]?.enabled);
18
+ }
19
+ return overrides;
20
+ }
21
+
22
+ function applyModelSelection(triageOutput, config, emitter, eventBase) {
23
+ if (!triageOutput.ok || !config?.model_selection?.enabled) return null;
24
+ const level = triageOutput.result?.level;
25
+ if (!level) return null;
26
+
27
+ const { modelOverrides, reasoning } = selectModelsForRoles({ level, config });
28
+ for (const [role, model] of Object.entries(modelOverrides)) {
29
+ if (config.roles?.[role] && !config.roles[role].model) {
30
+ config.roles[role].model = model;
31
+ }
32
+ }
33
+ const modelSelection = { modelOverrides, reasoning };
34
+ emitProgress(
35
+ emitter,
36
+ makeEvent("model-selection:applied", { ...eventBase, stage: "triage" }, {
37
+ message: "Smart model selection applied",
38
+ detail: modelSelection
39
+ })
40
+ );
41
+ return modelSelection;
42
+ }
43
+
44
+ export async function runTriageStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget }) {
45
+ logger.setContext({ iteration: 0, stage: "triage" });
46
+
47
+ const triageProvider = config?.roles?.triage?.provider || coderRole.provider;
48
+ emitProgress(
49
+ emitter,
50
+ makeEvent("triage:start", { ...eventBase, stage: "triage" }, {
51
+ message: "Triage classifying task complexity",
52
+ detail: { provider: triageProvider, executorType: "agent" }
53
+ })
54
+ );
55
+
56
+ const triageOnOutput = ({ stream, line }) => {
57
+ emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "triage" }, {
58
+ message: line,
59
+ detail: { stream, agent: triageProvider }
60
+ }));
61
+ };
62
+ const triageStall = createStallDetector({
63
+ onOutput: triageOnOutput, emitter, eventBase, stage: "triage", provider: triageProvider
64
+ });
65
+
66
+ const triage = new TriageRole({ config, logger, emitter });
67
+ await triage.init({ task: session.task, sessionId: session.id, iteration: 0 });
68
+ const triageStart = Date.now();
69
+ let triageOutput;
70
+ try {
71
+ triageOutput = await triage.run({ task: session.task, onOutput: triageStall.onOutput });
72
+ } catch (err) {
73
+ logger.warn(`Triage threw: ${err.message}`);
74
+ triageOutput = { ok: false, summary: `Triage error: ${err.message}`, result: { error: err.message } };
75
+ } finally {
76
+ triageStall.stop();
77
+ }
78
+ trackBudget({
79
+ role: "triage",
80
+ provider: config?.roles?.triage?.provider || coderRole.provider,
81
+ model: config?.roles?.triage?.model || coderRole.model,
82
+ result: triageOutput,
83
+ duration_ms: Date.now() - triageStart
84
+ });
85
+
86
+ await addCheckpoint(session, {
87
+ stage: "triage",
88
+ iteration: 0,
89
+ ok: triageOutput.ok,
90
+ provider: config?.roles?.triage?.provider || coderRole.provider,
91
+ model: config?.roles?.triage?.model || coderRole.model || null
92
+ });
93
+
94
+ const recommendedRoles = new Set(triageOutput.result?.roles || []);
95
+ const roleOverrides = triageOutput.ok
96
+ ? buildRoleOverrides(recommendedRoles, config.pipeline || {})
97
+ : {};
98
+
99
+ const shouldDecompose = triageOutput.result?.shouldDecompose || false;
100
+ const subtasks = triageOutput.result?.subtasks || [];
101
+
102
+ const stageResult = {
103
+ ok: triageOutput.ok,
104
+ level: triageOutput.result?.level || null,
105
+ roles: Array.from(recommendedRoles),
106
+ reasoning: triageOutput.result?.reasoning || null,
107
+ taskType: triageOutput.result?.taskType || "sw",
108
+ shouldDecompose,
109
+ subtasks
110
+ };
111
+
112
+ const modelSelection = applyModelSelection(triageOutput, config, emitter, eventBase);
113
+ if (modelSelection) {
114
+ stageResult.modelSelection = modelSelection;
115
+ }
116
+
117
+ if (shouldDecompose && subtasks.length > 0) {
118
+ emitProgress(
119
+ emitter,
120
+ makeEvent("triage:decompose", { ...eventBase, stage: "triage" }, {
121
+ message: `Task decomposition recommended: ${subtasks.length} subtask${subtasks.length === 1 ? "" : "s"}`,
122
+ detail: { shouldDecompose, subtasks }
123
+ })
124
+ );
125
+ }
126
+
127
+ emitProgress(
128
+ emitter,
129
+ makeEvent("triage:end", { ...eventBase, stage: "triage" }, {
130
+ status: triageOutput.ok ? "ok" : "fail",
131
+ message: triageOutput.ok ? "Triage completed" : `Triage failed: ${triageOutput.summary}`,
132
+ detail: { ...stageResult, provider: triageProvider, executorType: "agent" }
133
+ })
134
+ );
135
+
136
+ return { roleOverrides, stageResult };
137
+ }
@@ -21,7 +21,7 @@ async function listPluginFiles(dir) {
21
21
  return entries
22
22
  .filter((e) => e.isFile() && e.name.endsWith(".js"))
23
23
  .map((e) => path.join(dir, e.name));
24
- } catch {
24
+ } catch { /* plugin dir does not exist */
25
25
  return [];
26
26
  }
27
27
  }
@@ -69,7 +69,7 @@ export function parseArchitectOutput(raw) {
69
69
  let parsed;
70
70
  try {
71
71
  parsed = JSON.parse(jsonMatch[0]);
72
- } catch {
72
+ } catch { /* architect output is not valid JSON */
73
73
  return null;
74
74
  }
75
75
 
@@ -47,7 +47,7 @@ export function parseJsonOutput(raw) {
47
47
  try {
48
48
  const parsed = JSON.parse(cleaned);
49
49
  return normalizeReviewPayload(parsed);
50
- } catch {
50
+ } catch { /* not a single JSON blob */
51
51
  const lines = cleaned
52
52
  .split("\n")
53
53
  .map((x) => x.trim())
@@ -57,7 +57,7 @@ export function parseJsonOutput(raw) {
57
57
  for (const line of lines) {
58
58
  try {
59
59
  parsedLines.push(JSON.parse(line));
60
- } catch {
60
+ } catch { /* line is not JSON */
61
61
  continue;
62
62
  }
63
63
  }
@@ -71,7 +71,7 @@ export function parseJsonOutput(raw) {
71
71
  try {
72
72
  const parsed = JSON.parse(lines[i]);
73
73
  return normalizeReviewPayload(parsed);
74
- } catch {
74
+ } catch { /* line is not JSON */
75
75
  continue;
76
76
  }
77
77
  }
@@ -104,7 +104,7 @@ export class ArchitectRole extends BaseRole {
104
104
  summary: buildSummary(parsed),
105
105
  usage: result.usage
106
106
  };
107
- } catch {
107
+ } catch { /* agent output is not structured JSON */
108
108
  return {
109
109
  ok: true,
110
110
  result: {
@@ -63,7 +63,7 @@ export class AuditRole extends BaseRole {
63
63
  let growthDelta = null;
64
64
  try {
65
65
  basalCost = await measureBasalCost(projectDir);
66
- const previous = loadPreviousAudit(projectDir);
66
+ const previous = await loadPreviousAudit(projectDir);
67
67
  growthDelta = computeGrowthDelta(basalCost, previous);
68
68
  } catch {
69
69
  // basal cost is best-effort, don't fail the audit
@@ -95,7 +95,7 @@ export class AuditRole extends BaseRole {
95
95
  }
96
96
 
97
97
  if (basalCost) {
98
- try { saveAuditSnapshot(projectDir, basalCost); } catch { /* best-effort */ }
98
+ try { await saveAuditSnapshot(projectDir, basalCost); } catch { /* best-effort */ }
99
99
  }
100
100
 
101
101
  return {
@@ -111,7 +111,7 @@ export class AuditRole extends BaseRole {
111
111
  summary: buildSummary(parsed),
112
112
  usage: result.usage
113
113
  };
114
- } catch {
114
+ } catch { /* agent output is not structured JSON */
115
115
  return {
116
116
  ok: true,
117
117
  result: { raw: result.output, provider },
@@ -35,7 +35,7 @@ async function loadFirstExisting(paths) {
35
35
  for (const p of paths) {
36
36
  try {
37
37
  return await fs.readFile(p, "utf8");
38
- } catch {
38
+ } catch { /* file not found */
39
39
  continue;
40
40
  }
41
41
  }
@@ -115,7 +115,7 @@ export class DiscoverRole extends BaseRole {
115
115
  summary: buildSummary(parsed, mode),
116
116
  usage: result.usage
117
117
  };
118
- } catch {
118
+ } catch { /* agent output is not structured JSON */
119
119
  return buildUnstructuredResult(result.output, mode, provider, result.usage);
120
120
  }
121
121
  }
@@ -95,7 +95,7 @@ export class HuReviewerRole extends BaseRole {
95
95
  summary: buildSummary(parsed),
96
96
  usage: result.usage
97
97
  };
98
- } catch {
98
+ } catch { /* agent output is not structured JSON */
99
99
  return {
100
100
  ok: true,
101
101
  result: {
@@ -116,7 +116,7 @@ export class ResearcherRole extends BaseRole {
116
116
  },
117
117
  summary: buildSummary(parsed)
118
118
  };
119
- } catch {
119
+ } catch { /* agent output is not structured JSON */
120
120
  return {
121
121
  ok: true,
122
122
  result: {
@@ -112,7 +112,7 @@ export class TriageRole extends BaseRole {
112
112
  summary: `Triage: ${level} (${roles.length} role${roles.length === 1 ? "" : "s"})${decomposeNote}`,
113
113
  usage: result.usage
114
114
  };
115
- } catch {
115
+ } catch { /* agent output is not structured JSON */
116
116
  return {
117
117
  ok: true,
118
118
  result: {
@@ -71,7 +71,7 @@ export async function cleanupExpiredSessions({ logger } = {}) {
71
71
  let entries;
72
72
  try {
73
73
  entries = await fs.readdir(sessionRoot, { withFileTypes: true });
74
- } catch {
74
+ } catch { /* session root does not exist */
75
75
  return { removed: 0, errors: [] };
76
76
  }
77
77
 
@@ -67,14 +67,14 @@ export async function loadMostRecentSession() {
67
67
  let entries;
68
68
  try {
69
69
  entries = await fs.readdir(SESSION_ROOT, { withFileTypes: true });
70
- } catch {
70
+ } catch { /* session root does not exist yet */
71
71
  return null;
72
72
  }
73
73
  const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
74
74
  for (let i = dirs.length - 1; i >= 0; i--) {
75
75
  try {
76
76
  return await loadSession(dirs[i]);
77
- } catch {
77
+ } catch { /* session.json missing or malformed */
78
78
  continue;
79
79
  }
80
80
  }
@@ -22,7 +22,7 @@ export async function isOpenSkillsAvailable() {
22
22
  timeout: 15_000
23
23
  });
24
24
  return result.exitCode === 0;
25
- } catch {
25
+ } catch { /* openskills CLI not available */
26
26
  return false;
27
27
  }
28
28
  }
@@ -149,7 +149,7 @@ export async function autoInstallSkills(neededSkills, projectDir) {
149
149
  } else {
150
150
  result.failed.push(skillName);
151
151
  }
152
- } catch {
152
+ } catch { /* skill install threw */
153
153
  result.failed.push(skillName);
154
154
  }
155
155
  }
@@ -178,7 +178,7 @@ export async function cleanupAutoInstalledSkills(skillNames, projectDir) {
178
178
  } else {
179
179
  result.failed.push(name);
180
180
  }
181
- } catch {
181
+ } catch { /* skill removal threw */
182
182
  result.failed.push(name);
183
183
  }
184
184
  }
package/src/sonar/api.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { runCommand } from "../utils/process.js";
2
2
  import { withRetry } from "../utils/retry.js";
3
3
  import { resolveSonarProjectKey } from "./project-key.js";
4
+ import { resolveSonarToken } from "./config-resolver.js";
4
5
 
5
6
  export class SonarApiError extends Error {
6
7
  constructor(message, { url, httpStatus, hint } = {}) {
@@ -13,7 +14,7 @@ export class SonarApiError extends Error {
13
14
  }
14
15
 
15
16
  function tokenFromConfig(config) {
16
- return process.env.KJ_SONAR_TOKEN || config.sonarqube.token || "";
17
+ return resolveSonarToken(config) || "";
17
18
  }
18
19
 
19
20
  function parseHttpResponse(stdout) {
@@ -73,7 +74,7 @@ export async function getQualityGateStatus(config, projectKey = null) {
73
74
  try {
74
75
  const parsed = JSON.parse(body);
75
76
  return { ok: true, status: parsed.projectStatus?.status || "ERROR", conditions: parsed.projectStatus?.conditions || [], raw: parsed };
76
- } catch {
77
+ } catch { /* SonarQube response is not valid JSON */
77
78
  return { ok: false, status: "ERROR", conditions: [], raw: body };
78
79
  }
79
80
  }
@@ -85,7 +86,7 @@ export async function getOpenIssues(config, projectKey = null) {
85
86
  try {
86
87
  const parsed = JSON.parse(body);
87
88
  return { total: parsed.total || 0, issues: parsed.issues || [], raw: parsed };
88
- } catch {
89
+ } catch { /* SonarQube response is not valid JSON */
89
90
  return { total: 0, issues: [], raw: body };
90
91
  }
91
92
  }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Centralized SonarQube configuration resolution.
3
+ *
4
+ * Single source of truth for host URL, token, and admin credentials.
5
+ * Consumers: scanner.js, preflight-checks.js, api.js.
6
+ */
7
+
8
+ import { loadSonarCredentials } from "./credentials.js";
9
+
10
+ const DEFAULT_HOST = "http://localhost:9000";
11
+
12
+ /**
13
+ * Normalize SonarQube host URL for API calls (replace Docker-internal hostname).
14
+ * @param {string|undefined} rawHost - Raw host from config or env
15
+ * @returns {string} Normalized host URL without trailing slash
16
+ */
17
+ export function resolveSonarHost(rawHost) {
18
+ const host = String(rawHost || DEFAULT_HOST)
19
+ .replace(/host\.docker\.internal/g, "localhost")
20
+ .replace(/\/+$/, "");
21
+ return host;
22
+ }
23
+
24
+ /**
25
+ * Resolve SonarQube auth token from (in priority order):
26
+ * 1. KJ_SONAR_TOKEN env var
27
+ * 2. config.sonarqube.token
28
+ * 3. SONAR_TOKEN env var
29
+ *
30
+ * @param {object} [config={}] - Karajan config object
31
+ * @returns {string|null} Token string or null if none found
32
+ */
33
+ export function resolveSonarToken(config = {}) {
34
+ const token =
35
+ process.env.KJ_SONAR_TOKEN ||
36
+ config.sonarqube?.token ||
37
+ process.env.SONAR_TOKEN ||
38
+ null;
39
+ return token || null;
40
+ }
41
+
42
+ /**
43
+ * Resolve SonarQube admin credentials from (in priority order):
44
+ * 1. Env vars: KJ_SONAR_ADMIN_USER / KJ_SONAR_ADMIN_PASSWORD
45
+ * 2. Config: sonarqube.admin_user / sonarqube.admin_password
46
+ * 3. Credentials file: ~/.karajan/sonar-credentials.json
47
+ *
48
+ * Returns all candidate passwords (de-duped) so callers can try each.
49
+ *
50
+ * @param {object} [config={}] - Karajan config object
51
+ * @returns {Promise<{ user: string|null, passwords: string[] }>}
52
+ */
53
+ export async function resolveSonarCredentials(config = {}) {
54
+ const fileCreds = (await loadSonarCredentials()) || {};
55
+
56
+ const user =
57
+ process.env.KJ_SONAR_ADMIN_USER ||
58
+ config.sonarqube?.admin_user ||
59
+ fileCreds.user ||
60
+ null;
61
+
62
+ const candidates = [
63
+ process.env.KJ_SONAR_ADMIN_PASSWORD,
64
+ config.sonarqube?.admin_password,
65
+ fileCreds.password,
66
+ ].filter(Boolean);
67
+
68
+ return { user, passwords: [...new Set(candidates)] };
69
+ }
@@ -25,7 +25,7 @@ export async function loadSonarCredentials() {
25
25
  user: data.user || null,
26
26
  password: data.password || null
27
27
  };
28
- } catch {
28
+ } catch { /* credentials file may not exist */
29
29
  return { user: null, password: null };
30
30
  }
31
31
  }
@@ -30,7 +30,7 @@ function parseUrlLikeRemote(remoteUrl) {
30
30
  try {
31
31
  const parsed = new URL(String(remoteUrl || "").trim());
32
32
  return { host: parsed.hostname, path: parsed.pathname.replace(/^\/+/, "") };
33
- } catch {
33
+ } catch { /* not a valid URL */
34
34
  return null;
35
35
  }
36
36
  }
@@ -4,7 +4,11 @@ import path from "node:path";
4
4
  import { runCommand } from "../utils/process.js";
5
5
  import { sonarUp } from "./manager.js";
6
6
  import { resolveSonarProjectKey } from "./project-key.js";
7
- import { loadSonarCredentials } from "./credentials.js";
7
+ import {
8
+ resolveSonarHost,
9
+ resolveSonarToken as resolveToken,
10
+ resolveSonarCredentials,
11
+ } from "./config-resolver.js";
8
12
 
9
13
  export function buildScannerOpts(projectKey, scanner = {}) {
10
14
  const opts = [`-Dsonar.projectKey=${projectKey}`];
@@ -45,15 +49,11 @@ function normalizeScannerConfig(scanner = {}) {
45
49
  function parseJsonSafe(text) {
46
50
  try {
47
51
  return JSON.parse(text);
48
- } catch {
52
+ } catch { /* invalid JSON */
49
53
  return null;
50
54
  }
51
55
  }
52
56
 
53
- function normalizeApiHost(rawHost) {
54
- return String(rawHost || "http://localhost:9000").replaceAll("host.docker.internal", "localhost");
55
- }
56
-
57
57
  async function validateAdminCredentials(host, user, password) {
58
58
  const res = await runCommand("curl", [
59
59
  "-sS",
@@ -133,22 +133,14 @@ async function maybeRunCoverage(config) {
133
133
  return checkLcovExists(lcovPath, blockOnFailure, run.stdout || "");
134
134
  }
135
135
 
136
- async function resolveSonarToken(config, apiHost) {
137
- const explicitToken = process.env.KJ_SONAR_TOKEN || process.env.SONAR_TOKEN || config.sonarqube.token;
136
+ async function resolveSonarTokenWithFallback(config, apiHost) {
137
+ const explicitToken = resolveToken(config);
138
138
  if (explicitToken) return explicitToken;
139
139
 
140
- // Resolve admin credentials from: env vars → config → ~/.karajan/sonar-credentials.json
141
- const fileCreds = await loadSonarCredentials() || {};
142
- const adminUser = process.env.KJ_SONAR_ADMIN_USER || config.sonarqube.admin_user || fileCreds.user;
143
- const candidates = [
144
- process.env.KJ_SONAR_ADMIN_PASSWORD,
145
- config.sonarqube.admin_password,
146
- fileCreds.password
147
- ].filter(Boolean);
148
-
149
- if (!adminUser || candidates.length === 0) return null;
140
+ const { user: adminUser, passwords } = await resolveSonarCredentials(config);
141
+ if (!adminUser || passwords.length === 0) return null;
150
142
 
151
- for (const password of new Set(candidates)) {
143
+ for (const password of passwords) {
152
144
  const valid = await validateAdminCredentials(apiHost, adminUser, password);
153
145
  if (!valid) continue;
154
146
  const token = await generateUserToken(apiHost, adminUser, password);
@@ -205,7 +197,7 @@ export async function runSonarScan(config, projectKey = null) {
205
197
  ? Number(sonarConfig.timeouts.scanner_ms)
206
198
  : 15 * 60 * 1000;
207
199
  const sonarNetwork = sonarConfig.network || "karajan_sonar_net";
208
- const apiHost = normalizeApiHost(rawHost);
200
+ const apiHost = resolveSonarHost(rawHost);
209
201
  const isLocalHost = /localhost|127\.0\.0\.1/.test(rawHost);
210
202
  const host = isLocalHost ? rawHost.replaceAll(/localhost|127\.0\.0\.1/g, "host.docker.internal") : rawHost;
211
203
 
@@ -219,7 +211,7 @@ export async function runSonarScan(config, projectKey = null) {
219
211
  };
220
212
  }
221
213
  await ensureSonarProjectProperties();
222
- const token = await resolveSonarToken(config, apiHost);
214
+ const token = await resolveSonarTokenWithFallback(config, apiHost);
223
215
  if (!token) {
224
216
  return {
225
217
  ok: false,
package/src/utils/fs.js CHANGED
@@ -9,7 +9,7 @@ export async function exists(filePath) {
9
9
  try {
10
10
  await fs.access(filePath);
11
11
  return true;
12
- } catch {
12
+ } catch { /* file does not exist */
13
13
  return false;
14
14
  }
15
15
  }
@@ -54,7 +54,7 @@ export function extractFirstJson(raw) {
54
54
  if (depth === 0) {
55
55
  try {
56
56
  return JSON.parse(str.substring(start, i + 1));
57
- } catch {
57
+ } catch { /* matched braces but not valid JSON */
58
58
  return null;
59
59
  }
60
60
  }
@@ -120,7 +120,7 @@ export async function detectSonarConfig(cwd = process.cwd()) {
120
120
  try {
121
121
  await fs.access(path.join(cwd, "sonar-project.properties"));
122
122
  return { configured: true };
123
- } catch {
123
+ } catch { /* sonar-project.properties does not exist */
124
124
  return { configured: false };
125
125
  }
126
126
  }
@@ -12,7 +12,7 @@ export async function detectRtk() {
12
12
  return { available: true, version };
13
13
  }
14
14
  return { available: false, version: null };
15
- } catch {
15
+ } catch { /* rtk binary not found */
16
16
  return { available: false, version: null };
17
17
  }
18
18
  }