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.
- package/package.json +1 -1
- package/src/agents/resolve-bin.js +1 -1
- package/src/audit/basal-cost.js +26 -23
- package/src/bootstrap.js +2 -2
- package/src/commands/agents.js +1 -1
- package/src/commands/board.js +3 -3
- package/src/commands/code.js +1 -1
- package/src/commands/doctor.js +4 -4
- package/src/commands/init.js +2 -2
- package/src/commands/report.js +1 -1
- package/src/commands/review.js +1 -1
- package/src/commands/roles.js +1 -1
- package/src/hu/store.js +9 -5
- package/src/mcp/handlers/direct-handlers.js +393 -0
- package/src/mcp/handlers/hu-handlers.js +93 -0
- package/src/mcp/handlers/management-handlers.js +182 -0
- package/src/mcp/handlers/run-handler.js +283 -0
- package/src/mcp/orphan-guard.js +1 -1
- package/src/mcp/server-handlers.js +41 -871
- package/src/mcp/suggest-handler.js +2 -2
- package/src/orchestrator/hu-sub-pipeline.js +1 -1
- package/src/orchestrator/iteration-stages.js +9 -890
- package/src/orchestrator/pre-loop-stages.js +8 -906
- package/src/orchestrator/preflight-checks.js +13 -19
- package/src/orchestrator/stages/architect-stage.js +176 -0
- package/src/orchestrator/stages/coder-stage.js +307 -0
- package/src/orchestrator/stages/hu-reviewer-stage.js +467 -0
- package/src/orchestrator/stages/planner-stage.js +90 -0
- package/src/orchestrator/stages/research-stage.js +74 -0
- package/src/orchestrator/stages/reviewer-stage.js +350 -0
- package/src/orchestrator/stages/sonar-stage.js +267 -0
- package/src/orchestrator/stages/triage-stage.js +137 -0
- package/src/plugins/loader.js +1 -1
- package/src/prompts/architect.js +1 -1
- package/src/review/parser.js +3 -3
- package/src/roles/architect-role.js +1 -1
- package/src/roles/audit-role.js +3 -3
- package/src/roles/base-role.js +1 -1
- package/src/roles/discover-role.js +1 -1
- package/src/roles/hu-reviewer-role.js +1 -1
- package/src/roles/researcher-role.js +1 -1
- package/src/roles/triage-role.js +1 -1
- package/src/session-cleanup.js +1 -1
- package/src/session-store.js +2 -2
- package/src/skills/openskills-client.js +1 -1
- package/src/skills/skill-detector.js +2 -2
- package/src/sonar/api.js +4 -3
- package/src/sonar/config-resolver.js +69 -0
- package/src/sonar/credentials.js +1 -1
- package/src/sonar/project-key.js +1 -1
- package/src/sonar/scanner.js +13 -21
- package/src/utils/fs.js +1 -1
- package/src/utils/json-extract.js +1 -1
- package/src/utils/project-detect.js +1 -1
- 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
|
+
}
|
package/src/plugins/loader.js
CHANGED
package/src/prompts/architect.js
CHANGED
package/src/review/parser.js
CHANGED
|
@@ -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
|
}
|
package/src/roles/audit-role.js
CHANGED
|
@@ -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 },
|
package/src/roles/base-role.js
CHANGED
|
@@ -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
|
}
|
package/src/roles/triage-role.js
CHANGED
|
@@ -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: {
|
package/src/session-cleanup.js
CHANGED
|
@@ -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
|
|
package/src/session-store.js
CHANGED
|
@@ -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
|
}
|
|
@@ -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
|
|
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
|
+
}
|
package/src/sonar/credentials.js
CHANGED
package/src/sonar/project-key.js
CHANGED
|
@@ -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
|
}
|
package/src/sonar/scanner.js
CHANGED
|
@@ -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 {
|
|
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
|
|
137
|
-
const explicitToken =
|
|
136
|
+
async function resolveSonarTokenWithFallback(config, apiHost) {
|
|
137
|
+
const explicitToken = resolveToken(config);
|
|
138
138
|
if (explicitToken) return explicitToken;
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
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 =
|
|
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
|
|
214
|
+
const token = await resolveSonarTokenWithFallback(config, apiHost);
|
|
223
215
|
if (!token) {
|
|
224
216
|
return {
|
|
225
217
|
ok: false,
|
package/src/utils/fs.js
CHANGED
|
@@ -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
|
}
|
package/src/utils/rtk-detect.js
CHANGED