karajan-code 1.48.0 → 1.49.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/orphan-guard.js +1 -1
- package/src/mcp/server-handlers.js +3 -3
- package/src/mcp/suggest-handler.js +2 -2
- package/src/orchestrator/hu-sub-pipeline.js +1 -1
- package/src/orchestrator/pre-loop-stages.js +2 -2
- package/src/orchestrator/preflight-checks.js +13 -19
- 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
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@ function getNvmDirs() {
|
|
|
26
26
|
const versionsDir = path.join(nvmDir, "versions", "node");
|
|
27
27
|
try {
|
|
28
28
|
return readdirSync(versionsDir).map(v => path.join(versionsDir, v, "bin"));
|
|
29
|
-
} catch {
|
|
29
|
+
} catch { /* nvm versions dir may not exist */
|
|
30
30
|
return [];
|
|
31
31
|
}
|
|
32
32
|
}
|
package/src/audit/basal-cost.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { execFile
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
4
5
|
import { getKarajanHome } from "../utils/paths.js";
|
|
5
6
|
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
|
|
6
9
|
const SOURCE_EXTENSIONS = new Set([
|
|
7
10
|
".js", ".mjs", ".cjs", ".ts", ".mts", ".cts", ".tsx", ".jsx",
|
|
8
11
|
".py", ".rb", ".go", ".rs", ".java", ".kt", ".swift", ".c", ".cpp", ".h",
|
|
@@ -18,11 +21,11 @@ function slugify(projectDir) {
|
|
|
18
21
|
return path.basename(projectDir).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
function walkSourceFiles(dir) {
|
|
24
|
+
async function walkSourceFiles(dir) {
|
|
22
25
|
// Use git ls-files for speed (instant vs recursive walk)
|
|
23
26
|
try {
|
|
24
|
-
const
|
|
25
|
-
return
|
|
27
|
+
const { stdout } = await execFileAsync("git", ["ls-files", "--cached", "--others", "--exclude-standard"], { cwd: dir, encoding: "utf8", timeout: 5000 });
|
|
28
|
+
return stdout.trim().split("\n")
|
|
26
29
|
.filter(f => f && SOURCE_EXTENSIONS.has(path.extname(f)) && !EXCLUDE_DIRS.has(f.split("/")[0]))
|
|
27
30
|
.map(f => path.join(dir, f));
|
|
28
31
|
} catch {
|
|
@@ -30,15 +33,15 @@ function walkSourceFiles(dir) {
|
|
|
30
33
|
const files = [];
|
|
31
34
|
let entries;
|
|
32
35
|
try {
|
|
33
|
-
entries = fs.
|
|
34
|
-
} catch {
|
|
36
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
37
|
+
} catch { /* directory not readable */
|
|
35
38
|
return files;
|
|
36
39
|
}
|
|
37
40
|
for (const entry of entries) {
|
|
38
41
|
if (EXCLUDE_DIRS.has(entry.name)) continue;
|
|
39
42
|
const full = path.join(dir, entry.name);
|
|
40
43
|
if (entry.isDirectory()) {
|
|
41
|
-
files.push(...walkSourceFiles(full));
|
|
44
|
+
files.push(...await walkSourceFiles(full));
|
|
42
45
|
} else if (SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
43
46
|
files.push(full);
|
|
44
47
|
}
|
|
@@ -47,19 +50,19 @@ function walkSourceFiles(dir) {
|
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
function countLines(filePath) {
|
|
53
|
+
async function countLines(filePath) {
|
|
51
54
|
try {
|
|
52
|
-
const content = fs.
|
|
55
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
53
56
|
return content.split("\n").length;
|
|
54
57
|
} catch {
|
|
55
58
|
return 0;
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
function countDependencies(projectDir) {
|
|
62
|
+
async function countDependencies(projectDir) {
|
|
60
63
|
const pkgPath = path.join(projectDir, "package.json");
|
|
61
64
|
try {
|
|
62
|
-
const pkg = JSON.parse(fs.
|
|
65
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
|
|
63
66
|
const deps = Object.keys(pkg.dependencies || {});
|
|
64
67
|
const devDeps = Object.keys(pkg.devDependencies || {});
|
|
65
68
|
return { dependencies: deps.length, devDependencies: devDeps.length, total: deps.length + devDeps.length };
|
|
@@ -89,7 +92,7 @@ function runDepcheck(projectDir) {
|
|
|
89
92
|
});
|
|
90
93
|
}
|
|
91
94
|
|
|
92
|
-
function findDeadExports(sourceFiles) {
|
|
95
|
+
async function findDeadExports(sourceFiles) {
|
|
93
96
|
const exportMap = new Map(); // file -> [exportName]
|
|
94
97
|
const importedNames = new Set();
|
|
95
98
|
|
|
@@ -101,7 +104,7 @@ function findDeadExports(sourceFiles) {
|
|
|
101
104
|
for (const file of sourceFiles) {
|
|
102
105
|
let content;
|
|
103
106
|
try {
|
|
104
|
-
content = fs.
|
|
107
|
+
content = await fs.readFile(file, "utf8");
|
|
105
108
|
} catch {
|
|
106
109
|
continue;
|
|
107
110
|
}
|
|
@@ -139,15 +142,15 @@ function findDeadExports(sourceFiles) {
|
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
export async function measureBasalCost(projectDir) {
|
|
142
|
-
const sourceFiles = walkSourceFiles(projectDir);
|
|
145
|
+
const sourceFiles = await walkSourceFiles(projectDir);
|
|
143
146
|
let totalLines = 0;
|
|
144
147
|
for (const f of sourceFiles) {
|
|
145
|
-
totalLines += countLines(f);
|
|
148
|
+
totalLines += await countLines(f);
|
|
146
149
|
}
|
|
147
150
|
|
|
148
|
-
const depInfo = countDependencies(projectDir);
|
|
151
|
+
const depInfo = await countDependencies(projectDir);
|
|
149
152
|
const depcheck = await runDepcheck(projectDir);
|
|
150
|
-
const deadExports = findDeadExports(sourceFiles);
|
|
153
|
+
const deadExports = await findDeadExports(sourceFiles);
|
|
151
154
|
|
|
152
155
|
return {
|
|
153
156
|
totalLines,
|
|
@@ -162,20 +165,20 @@ function auditDir() {
|
|
|
162
165
|
return path.join(getKarajanHome(), "audits");
|
|
163
166
|
}
|
|
164
167
|
|
|
165
|
-
export function loadPreviousAudit(projectDir) {
|
|
168
|
+
export async function loadPreviousAudit(projectDir) {
|
|
166
169
|
const file = path.join(auditDir(), slugify(projectDir), "latest.json");
|
|
167
170
|
try {
|
|
168
|
-
return JSON.parse(fs.
|
|
171
|
+
return JSON.parse(await fs.readFile(file, "utf8"));
|
|
169
172
|
} catch {
|
|
170
173
|
return null;
|
|
171
174
|
}
|
|
172
175
|
}
|
|
173
176
|
|
|
174
|
-
export function saveAuditSnapshot(projectDir, metrics) {
|
|
177
|
+
export async function saveAuditSnapshot(projectDir, metrics) {
|
|
175
178
|
const dir = path.join(auditDir(), slugify(projectDir));
|
|
176
|
-
fs.
|
|
179
|
+
await fs.mkdir(dir, { recursive: true });
|
|
177
180
|
const snapshot = { ...metrics, timestamp: new Date().toISOString() };
|
|
178
|
-
fs.
|
|
181
|
+
await fs.writeFile(path.join(dir, "latest.json"), JSON.stringify(snapshot, null, 2));
|
|
179
182
|
return snapshot;
|
|
180
183
|
}
|
|
181
184
|
|
package/src/bootstrap.js
CHANGED
|
@@ -38,7 +38,7 @@ async function checkGitRepo() {
|
|
|
38
38
|
let ok = false;
|
|
39
39
|
try {
|
|
40
40
|
ok = await ensureGitRepo();
|
|
41
|
-
} catch {
|
|
41
|
+
} catch { /* git may not be installed */
|
|
42
42
|
ok = false;
|
|
43
43
|
}
|
|
44
44
|
return {
|
|
@@ -156,7 +156,7 @@ async function readBootstrapFile(projectDir) {
|
|
|
156
156
|
try {
|
|
157
157
|
const raw = await fs.readFile(bootstrapPath(projectDir), "utf8");
|
|
158
158
|
return JSON.parse(raw);
|
|
159
|
-
} catch {
|
|
159
|
+
} catch { /* bootstrap file may not exist or contain invalid JSON */
|
|
160
160
|
return null;
|
|
161
161
|
}
|
|
162
162
|
}
|
package/src/commands/agents.js
CHANGED
|
@@ -51,7 +51,7 @@ export async function setAgent(role, provider, { global: isGlobal = false } = {}
|
|
|
51
51
|
const { setSessionOverride } = await import("../mcp/preflight.js");
|
|
52
52
|
setSessionOverride(role, provider);
|
|
53
53
|
return { role, provider, scope: "session" };
|
|
54
|
-
} catch {
|
|
54
|
+
} catch { /* preflight module not available in CLI mode */
|
|
55
55
|
// preflight module not available (CLI mode) — write to project config
|
|
56
56
|
const projectConfigPath = getProjectConfigPath();
|
|
57
57
|
const projectConfig = (await loadProjectConfig()) || {};
|
package/src/commands/board.js
CHANGED
|
@@ -10,7 +10,7 @@ function readPid() {
|
|
|
10
10
|
try {
|
|
11
11
|
const pid = Number.parseInt(fs.readFileSync(PID_FILE, "utf8").trim(), 10);
|
|
12
12
|
return Number.isFinite(pid) ? pid : null;
|
|
13
|
-
} catch {
|
|
13
|
+
} catch { /* PID file does not exist */
|
|
14
14
|
return null;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -19,7 +19,7 @@ function isProcessAlive(pid) {
|
|
|
19
19
|
try {
|
|
20
20
|
process.kill(pid, 0);
|
|
21
21
|
return true;
|
|
22
|
-
} catch {
|
|
22
|
+
} catch { /* process does not exist */
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -55,7 +55,7 @@ export async function stopBoard() {
|
|
|
55
55
|
|
|
56
56
|
try {
|
|
57
57
|
process.kill(pid, "SIGTERM");
|
|
58
|
-
} catch {
|
|
58
|
+
} catch { /* process already dead */
|
|
59
59
|
// already dead
|
|
60
60
|
}
|
|
61
61
|
try { fs.unlinkSync(PID_FILE); } catch { /* ignore */ }
|
package/src/commands/code.js
CHANGED
|
@@ -13,7 +13,7 @@ export async function codeCommand({ task, config, logger }) {
|
|
|
13
13
|
if (config.coder_rules) {
|
|
14
14
|
try {
|
|
15
15
|
coderRules = await fs.readFile(config.coder_rules, "utf8");
|
|
16
|
-
} catch {
|
|
16
|
+
} catch { /* configured coder_rules path not found — try fallback */
|
|
17
17
|
try { coderRules = await fs.readFile("coder-rules.md", "utf8"); } catch { /* no coder rules file */ }
|
|
18
18
|
}
|
|
19
19
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -44,7 +44,7 @@ async function checkGitRepo() {
|
|
|
44
44
|
let gitOk = false;
|
|
45
45
|
try {
|
|
46
46
|
gitOk = await ensureGitRepo();
|
|
47
|
-
} catch {
|
|
47
|
+
} catch { /* git check failed */
|
|
48
48
|
gitOk = false;
|
|
49
49
|
}
|
|
50
50
|
return {
|
|
@@ -79,7 +79,7 @@ async function checkSonarQube(config) {
|
|
|
79
79
|
if (config.sonarqube?.enabled !== false) {
|
|
80
80
|
try {
|
|
81
81
|
sonarOk = await isSonarReachable(sonarHost);
|
|
82
|
-
} catch {
|
|
82
|
+
} catch { /* sonar check failed */
|
|
83
83
|
sonarOk = false;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
@@ -130,7 +130,7 @@ async function checkSerena() {
|
|
|
130
130
|
try {
|
|
131
131
|
const serenaCheck = await runCommand("serena", ["--version"]);
|
|
132
132
|
serenaOk = serenaCheck.exitCode === 0;
|
|
133
|
-
} catch {
|
|
133
|
+
} catch { /* serena not installed */
|
|
134
134
|
serenaOk = false;
|
|
135
135
|
}
|
|
136
136
|
return {
|
|
@@ -181,7 +181,7 @@ async function checkBecariaSecrets() {
|
|
|
181
181
|
detail: secretsOk ? "BECARIA_APP_ID + BECARIA_APP_PRIVATE_KEY found" : `Missing: ${missing}`,
|
|
182
182
|
fix: secretsOk ? null : "Add BECARIA_APP_ID and BECARIA_APP_PRIVATE_KEY as GitHub repository secrets"
|
|
183
183
|
};
|
|
184
|
-
} catch {
|
|
184
|
+
} catch { /* GitHub API error */
|
|
185
185
|
return null;
|
|
186
186
|
}
|
|
187
187
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -163,7 +163,7 @@ async function ensureCoderRules(coderRulesPath, logger) {
|
|
|
163
163
|
let content;
|
|
164
164
|
try {
|
|
165
165
|
content = await fs.readFile(templatePath, "utf8");
|
|
166
|
-
} catch {
|
|
166
|
+
} catch { /* template not found, use inline default */
|
|
167
167
|
content = [
|
|
168
168
|
"# Coder Rules",
|
|
169
169
|
"",
|
|
@@ -333,7 +333,7 @@ export async function initCommand({ logger, flags = {} }) {
|
|
|
333
333
|
try {
|
|
334
334
|
const rtkRes = await runCommand("rtk", ["--version"]);
|
|
335
335
|
hasRtk = rtkRes.exitCode === 0;
|
|
336
|
-
} catch {
|
|
336
|
+
} catch { /* rtk not installed */
|
|
337
337
|
hasRtk = false;
|
|
338
338
|
}
|
|
339
339
|
if (!hasRtk) {
|
package/src/commands/report.js
CHANGED
package/src/commands/review.js
CHANGED
package/src/commands/roles.js
CHANGED
package/src/hu/store.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
4
7
|
import { getKarajanHome } from "../utils/paths.js";
|
|
5
8
|
|
|
6
9
|
// FUTURE: hu-storage adapter for PG/Trello/etc — currently local files only
|
|
@@ -237,13 +240,14 @@ export function addSplittingMetadata(batch, huId, metadata) {
|
|
|
237
240
|
/**
|
|
238
241
|
* Detect project info from the given directory.
|
|
239
242
|
* @param {string} cwd - Directory to detect project from.
|
|
240
|
-
* @returns {{ name: string, remoteUrl: string|null }}
|
|
243
|
+
* @returns {Promise<{ name: string, remoteUrl: string|null }>}
|
|
241
244
|
*/
|
|
242
|
-
export function detectProject(cwd) {
|
|
245
|
+
export async function detectProject(cwd) {
|
|
243
246
|
const name = path.basename(cwd);
|
|
244
247
|
let remoteUrl = null;
|
|
245
248
|
try {
|
|
246
|
-
|
|
249
|
+
const { stdout } = await execFileAsync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
250
|
+
remoteUrl = stdout.trim();
|
|
247
251
|
} catch { /* not a git repo or no remote */ }
|
|
248
252
|
return { name, remoteUrl };
|
|
249
253
|
}
|
|
@@ -276,7 +280,7 @@ async function loadProjectBatch(projectDir) {
|
|
|
276
280
|
try {
|
|
277
281
|
const raw = await fs.readFile(filePath, "utf8");
|
|
278
282
|
return JSON.parse(raw);
|
|
279
|
-
} catch {
|
|
283
|
+
} catch { /* no saved batch for this project */
|
|
280
284
|
return {
|
|
281
285
|
project: projectSlug(projectDir),
|
|
282
286
|
created_at: new Date().toISOString(),
|
package/src/mcp/orphan-guard.js
CHANGED
|
@@ -201,7 +201,7 @@ export function buildAskQuestion(server) {
|
|
|
201
201
|
}
|
|
202
202
|
});
|
|
203
203
|
return result.action === "accept" ? result.content?.answer || null : null;
|
|
204
|
-
} catch {
|
|
204
|
+
} catch { /* elicitInput not supported by client */
|
|
205
205
|
return null;
|
|
206
206
|
}
|
|
207
207
|
};
|
|
@@ -221,7 +221,7 @@ async function attemptAutoResume({ err, config, logger, emitter, askQuestion, ru
|
|
|
221
221
|
let session;
|
|
222
222
|
try {
|
|
223
223
|
session = await loadMostRecentSession();
|
|
224
|
-
} catch {
|
|
224
|
+
} catch { /* no session store or read error */
|
|
225
225
|
return null;
|
|
226
226
|
}
|
|
227
227
|
if (!session || !["failed", "stopped"].includes(session.status)) return null;
|
|
@@ -484,7 +484,7 @@ export async function handleCodeDirect(a, server, extra) {
|
|
|
484
484
|
if (config.coder_rules) {
|
|
485
485
|
try {
|
|
486
486
|
coderRules = await fs.readFile(config.coder_rules, "utf8");
|
|
487
|
-
} catch {
|
|
487
|
+
} catch { /* configured coder_rules path not found */
|
|
488
488
|
try { coderRules = await fs.readFile("coder-rules.md", "utf8"); } catch { /* no coder rules file */ }
|
|
489
489
|
}
|
|
490
490
|
}
|
|
@@ -16,7 +16,7 @@ async function findActiveSession(projectDir) {
|
|
|
16
16
|
let entries;
|
|
17
17
|
try {
|
|
18
18
|
entries = await fs.readdir(sessionRoot, { withFileTypes: true });
|
|
19
|
-
} catch {
|
|
19
|
+
} catch { /* session root dir may not exist */
|
|
20
20
|
return null;
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -29,7 +29,7 @@ async function findActiveSession(projectDir) {
|
|
|
29
29
|
if (session.status !== "running") continue;
|
|
30
30
|
if (projectDir && session.projectDir && session.projectDir !== projectDir) continue;
|
|
31
31
|
return session;
|
|
32
|
-
} catch {
|
|
32
|
+
} catch { /* session.json missing or malformed */
|
|
33
33
|
continue;
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -205,7 +205,7 @@ export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitt
|
|
|
205
205
|
let orderedIds;
|
|
206
206
|
try {
|
|
207
207
|
orderedIds = topologicalSort(certifiedStories);
|
|
208
|
-
} catch {
|
|
208
|
+
} catch { /* cyclic dependency */
|
|
209
209
|
orderedIds = certifiedStories.map(s => s.id);
|
|
210
210
|
}
|
|
211
211
|
|
|
@@ -677,7 +677,7 @@ export async function runHuReviewerStage({ config, logger, emitter, eventBase, s
|
|
|
677
677
|
let batch;
|
|
678
678
|
try {
|
|
679
679
|
batch = await loadHuBatch(batchSessionId);
|
|
680
|
-
} catch {
|
|
680
|
+
} catch { /* no existing batch */
|
|
681
681
|
batch = await createHuBatch(batchSessionId, stories);
|
|
682
682
|
}
|
|
683
683
|
|
|
@@ -881,7 +881,7 @@ export async function runHuReviewerStage({ config, logger, emitter, eventBase, s
|
|
|
881
881
|
let orderedIds;
|
|
882
882
|
try {
|
|
883
883
|
orderedIds = topologicalSort(certifiedStories);
|
|
884
|
-
} catch {
|
|
884
|
+
} catch { /* cyclic dependency */
|
|
885
885
|
orderedIds = certifiedStories.map(s => s.id);
|
|
886
886
|
}
|
|
887
887
|
|
|
@@ -13,16 +13,16 @@ import { checkBinary } from "../utils/agent-detect.js";
|
|
|
13
13
|
import { isSonarReachable, sonarUp } from "../sonar/manager.js";
|
|
14
14
|
import { runCommand } from "../utils/process.js";
|
|
15
15
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
16
|
+
import {
|
|
17
|
+
resolveSonarHost,
|
|
18
|
+
resolveSonarToken,
|
|
19
|
+
resolveSonarCredentials,
|
|
20
|
+
} from "../sonar/config-resolver.js";
|
|
21
21
|
|
|
22
22
|
function parseJsonSafe(text) {
|
|
23
23
|
try {
|
|
24
24
|
return JSON.parse(text);
|
|
25
|
-
} catch {
|
|
25
|
+
} catch { /* invalid JSON */
|
|
26
26
|
return null;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -60,10 +60,10 @@ async function checkSonarReachable(host) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
async function checkSonarAuth(config) {
|
|
63
|
-
const host =
|
|
63
|
+
const host = resolveSonarHost(config.sonarqube?.host);
|
|
64
64
|
|
|
65
65
|
// Check explicit token first
|
|
66
|
-
const explicitToken =
|
|
66
|
+
const explicitToken = resolveSonarToken(config);
|
|
67
67
|
if (explicitToken) {
|
|
68
68
|
// Validate the token works
|
|
69
69
|
const res = await runCommand("curl", [
|
|
@@ -77,20 +77,14 @@ async function checkSonarAuth(config) {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
// Try admin credentials
|
|
81
|
-
const
|
|
82
|
-
const adminUser = process.env.KJ_SONAR_ADMIN_USER || config.sonarqube?.admin_user || fileCreds.user;
|
|
83
|
-
const candidates = [
|
|
84
|
-
process.env.KJ_SONAR_ADMIN_PASSWORD,
|
|
85
|
-
config.sonarqube?.admin_password,
|
|
86
|
-
fileCreds.password
|
|
87
|
-
].filter(Boolean);
|
|
80
|
+
// Try admin credentials via centralized resolver
|
|
81
|
+
const { user: adminUser, passwords } = await resolveSonarCredentials(config);
|
|
88
82
|
|
|
89
|
-
if (!adminUser ||
|
|
83
|
+
if (!adminUser || passwords.length === 0) {
|
|
90
84
|
return { name: "sonar-auth", ok: false, detail: "No Sonar token or admin credentials configured. Set KJ_SONAR_TOKEN, configure sonarqube.token in kj.config.yml, or save credentials in ~/.karajan/sonar-credentials.json." };
|
|
91
85
|
}
|
|
92
86
|
|
|
93
|
-
for (const password of
|
|
87
|
+
for (const password of passwords) {
|
|
94
88
|
const validateRes = await runCommand("curl", [
|
|
95
89
|
"-sS", "-u", `${adminUser}:${password}`,
|
|
96
90
|
`${host}/api/authentication/validate`
|
|
@@ -151,7 +145,7 @@ async function checkSecurityAgent(config) {
|
|
|
151
145
|
export async function runPreflightChecks({ config, logger, emitter, eventBase, resolvedPolicies, securityEnabled }) {
|
|
152
146
|
const sonarEnabled = Boolean(config.sonarqube?.enabled) && resolvedPolicies.sonar !== false;
|
|
153
147
|
const isExternalSonar = Boolean(config.sonarqube?.external);
|
|
154
|
-
const sonarHost =
|
|
148
|
+
const sonarHost = resolveSonarHost(config.sonarqube?.host);
|
|
155
149
|
|
|
156
150
|
const result = {
|
|
157
151
|
ok: true,
|
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