karajan-code 1.2.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/LICENSE +21 -0
- package/README.md +441 -0
- package/docs/karajan-code-logo-small.png +0 -0
- package/package.json +60 -0
- package/scripts/install.js +898 -0
- package/scripts/install.sh +7 -0
- package/scripts/postinstall.js +117 -0
- package/scripts/setup-multi-instance.sh +150 -0
- package/src/activity-log.js +59 -0
- package/src/agents/aider-agent.js +25 -0
- package/src/agents/availability.js +32 -0
- package/src/agents/base-agent.js +27 -0
- package/src/agents/claude-agent.js +24 -0
- package/src/agents/codex-agent.js +27 -0
- package/src/agents/gemini-agent.js +25 -0
- package/src/agents/index.js +19 -0
- package/src/agents/resolve-bin.js +60 -0
- package/src/cli.js +200 -0
- package/src/commands/code.js +32 -0
- package/src/commands/config.js +74 -0
- package/src/commands/doctor.js +155 -0
- package/src/commands/init.js +181 -0
- package/src/commands/plan.js +67 -0
- package/src/commands/report.js +340 -0
- package/src/commands/resume.js +39 -0
- package/src/commands/review.js +26 -0
- package/src/commands/roles.js +117 -0
- package/src/commands/run.js +91 -0
- package/src/commands/scan.js +18 -0
- package/src/commands/sonar.js +53 -0
- package/src/config.js +322 -0
- package/src/git/automation.js +100 -0
- package/src/mcp/progress.js +69 -0
- package/src/mcp/run-kj.js +87 -0
- package/src/mcp/server-handlers.js +259 -0
- package/src/mcp/server.js +37 -0
- package/src/mcp/tool-arg-normalizers.js +16 -0
- package/src/mcp/tools.js +184 -0
- package/src/orchestrator.js +1277 -0
- package/src/planning-game/adapter.js +105 -0
- package/src/planning-game/client.js +81 -0
- package/src/prompts/coder.js +60 -0
- package/src/prompts/planner.js +26 -0
- package/src/prompts/reviewer.js +45 -0
- package/src/repeat-detector.js +77 -0
- package/src/review/diff-generator.js +22 -0
- package/src/review/parser.js +93 -0
- package/src/review/profiles.js +66 -0
- package/src/review/schema.js +31 -0
- package/src/review/tdd-policy.js +57 -0
- package/src/roles/base-role.js +127 -0
- package/src/roles/coder-role.js +60 -0
- package/src/roles/commiter-role.js +94 -0
- package/src/roles/index.js +12 -0
- package/src/roles/planner-role.js +81 -0
- package/src/roles/refactorer-role.js +66 -0
- package/src/roles/researcher-role.js +134 -0
- package/src/roles/reviewer-role.js +132 -0
- package/src/roles/security-role.js +128 -0
- package/src/roles/solomon-role.js +199 -0
- package/src/roles/sonar-role.js +65 -0
- package/src/roles/tester-role.js +114 -0
- package/src/roles/triage-role.js +128 -0
- package/src/session-store.js +80 -0
- package/src/sonar/api.js +78 -0
- package/src/sonar/enforcer.js +19 -0
- package/src/sonar/manager.js +163 -0
- package/src/sonar/project-key.js +83 -0
- package/src/sonar/scanner.js +267 -0
- package/src/utils/agent-detect.js +32 -0
- package/src/utils/budget.js +123 -0
- package/src/utils/display.js +346 -0
- package/src/utils/events.js +23 -0
- package/src/utils/fs.js +19 -0
- package/src/utils/git.js +101 -0
- package/src/utils/logger.js +86 -0
- package/src/utils/paths.js +18 -0
- package/src/utils/pricing.js +28 -0
- package/src/utils/process.js +67 -0
- package/src/utils/wizard.js +41 -0
- package/templates/coder-rules.md +24 -0
- package/templates/docker-compose.sonar.yml +60 -0
- package/templates/kj.config.yml +82 -0
- package/templates/review-rules.md +11 -0
- package/templates/roles/coder.md +42 -0
- package/templates/roles/commiter.md +44 -0
- package/templates/roles/planner.md +45 -0
- package/templates/roles/refactorer.md +39 -0
- package/templates/roles/researcher.md +37 -0
- package/templates/roles/reviewer-paranoid.md +38 -0
- package/templates/roles/reviewer-relaxed.md +34 -0
- package/templates/roles/reviewer-strict.md +37 -0
- package/templates/roles/reviewer.md +55 -0
- package/templates/roles/security.md +54 -0
- package/templates/roles/solomon.md +106 -0
- package/templates/roles/sonar.md +49 -0
- package/templates/roles/tester.md +41 -0
- package/templates/roles/triage.md +25 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { applyRunOverrides, loadConfig, validateConfig } from "./config.js";
|
|
4
|
+
import { createLogger } from "./utils/logger.js";
|
|
5
|
+
import { initCommand } from "./commands/init.js";
|
|
6
|
+
import { configCommand } from "./commands/config.js";
|
|
7
|
+
import { codeCommand } from "./commands/code.js";
|
|
8
|
+
import { reviewCommand } from "./commands/review.js";
|
|
9
|
+
import { scanCommand } from "./commands/scan.js";
|
|
10
|
+
import { doctorCommand } from "./commands/doctor.js";
|
|
11
|
+
import { reportCommand } from "./commands/report.js";
|
|
12
|
+
import { planCommand } from "./commands/plan.js";
|
|
13
|
+
import { runCommandHandler } from "./commands/run.js";
|
|
14
|
+
import { resumeCommand } from "./commands/resume.js";
|
|
15
|
+
import { sonarCommand, sonarOpenCommand } from "./commands/sonar.js";
|
|
16
|
+
import { rolesCommand } from "./commands/roles.js";
|
|
17
|
+
|
|
18
|
+
async function withConfig(commandName, flags, fn) {
|
|
19
|
+
const { config } = await loadConfig();
|
|
20
|
+
const merged = applyRunOverrides(config, flags || {});
|
|
21
|
+
validateConfig(merged, commandName);
|
|
22
|
+
const logger = createLogger(merged.output.log_level);
|
|
23
|
+
await fn({ config: merged, logger, flags });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const program = new Command();
|
|
27
|
+
program.name("kj").description("Karajan Code CLI").version("1.2.0");
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.command("init")
|
|
31
|
+
.description("Initialize config, review rules and SonarQube")
|
|
32
|
+
.option("--no-interactive", "Skip wizard, use defaults (for CI/scripts)")
|
|
33
|
+
.action(async (flags) => {
|
|
34
|
+
await withConfig("init", flags, async ({ config, logger }) => {
|
|
35
|
+
await initCommand({ logger, flags });
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
program
|
|
40
|
+
.command("config")
|
|
41
|
+
.description("Show current config")
|
|
42
|
+
.option("--json", "Show as JSON")
|
|
43
|
+
.option("--edit", "Open config in $EDITOR for editing")
|
|
44
|
+
.action(async (flags) => {
|
|
45
|
+
await configCommand(flags);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
program
|
|
49
|
+
.command("run")
|
|
50
|
+
.description("Run coder+sonar+reviewer loop")
|
|
51
|
+
.argument("<task>")
|
|
52
|
+
.option("--planner <name>")
|
|
53
|
+
.option("--coder <name>")
|
|
54
|
+
.option("--reviewer <name>")
|
|
55
|
+
.option("--refactorer <name>")
|
|
56
|
+
.option("--planner-model <name>")
|
|
57
|
+
.option("--coder-model <name>")
|
|
58
|
+
.option("--reviewer-model <name>")
|
|
59
|
+
.option("--refactorer-model <name>")
|
|
60
|
+
.option("--enable-planner")
|
|
61
|
+
.option("--enable-reviewer")
|
|
62
|
+
.option("--enable-refactorer")
|
|
63
|
+
.option("--enable-researcher")
|
|
64
|
+
.option("--enable-tester")
|
|
65
|
+
.option("--enable-security")
|
|
66
|
+
.option("--enable-triage")
|
|
67
|
+
.option("--enable-serena")
|
|
68
|
+
.option("--mode <name>")
|
|
69
|
+
.option("--max-iterations <n>")
|
|
70
|
+
.option("--max-iteration-minutes <n>")
|
|
71
|
+
.option("--max-total-minutes <n>")
|
|
72
|
+
.option("--base-branch <name>")
|
|
73
|
+
.option("--base-ref <ref>")
|
|
74
|
+
.option("--reviewer-fallback <name>")
|
|
75
|
+
.option("--reviewer-retries <n>")
|
|
76
|
+
.option("--auto-commit")
|
|
77
|
+
.option("--auto-push")
|
|
78
|
+
.option("--auto-pr")
|
|
79
|
+
.option("--branch-prefix <prefix>")
|
|
80
|
+
.option("--methodology <name>")
|
|
81
|
+
.option("--no-auto-rebase")
|
|
82
|
+
.option("--no-sonar")
|
|
83
|
+
.option("--pg-task <cardId>", "Planning Game card ID (e.g., KJC-TSK-0042)")
|
|
84
|
+
.option("--pg-project <projectId>", "Planning Game project ID")
|
|
85
|
+
.option("--dry-run", "Show what would be executed without running anything")
|
|
86
|
+
.option("--json", "Output JSON only (no styled display)")
|
|
87
|
+
.action(async (task, flags) => {
|
|
88
|
+
await withConfig("run", flags, async ({ config, logger }) => {
|
|
89
|
+
await runCommandHandler({ task, config, logger, flags });
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
program
|
|
94
|
+
.command("code")
|
|
95
|
+
.description("Run only coder")
|
|
96
|
+
.argument("<task>")
|
|
97
|
+
.option("--coder <name>")
|
|
98
|
+
.option("--coder-model <name>")
|
|
99
|
+
.action(async (task, flags) => {
|
|
100
|
+
await withConfig("code", flags, async ({ config, logger }) => {
|
|
101
|
+
await codeCommand({ task, config, logger });
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
program
|
|
106
|
+
.command("review")
|
|
107
|
+
.description("Run only reviewer")
|
|
108
|
+
.argument("<task>")
|
|
109
|
+
.option("--reviewer <name>")
|
|
110
|
+
.option("--reviewer-model <name>")
|
|
111
|
+
.option("--base-ref <ref>")
|
|
112
|
+
.action(async (task, flags) => {
|
|
113
|
+
await withConfig("review", flags, async ({ config, logger }) => {
|
|
114
|
+
await reviewCommand({ task, config, logger, baseRef: flags.baseRef });
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
program
|
|
119
|
+
.command("scan")
|
|
120
|
+
.description("Run SonarQube scan")
|
|
121
|
+
.action(async () => {
|
|
122
|
+
await withConfig("scan", {}, scanCommand);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
program
|
|
126
|
+
.command("doctor")
|
|
127
|
+
.description("Check environment requirements")
|
|
128
|
+
.action(async () => {
|
|
129
|
+
await withConfig("doctor", {}, doctorCommand);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
program
|
|
133
|
+
.command("report")
|
|
134
|
+
.description("Show latest session report")
|
|
135
|
+
.option("--list", "List session ids")
|
|
136
|
+
.option("--session-id <id>", "Show report for a specific session ID")
|
|
137
|
+
.option("--format <type>", "Output format: text|json", "text")
|
|
138
|
+
.option("--trace", "Show chronological trace of all pipeline stages")
|
|
139
|
+
.option("--currency <code>", "Display costs in currency: usd|eur", "usd")
|
|
140
|
+
.action(async (flags) => {
|
|
141
|
+
await reportCommand(flags);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
program
|
|
145
|
+
.command("roles [subcommand] [role]")
|
|
146
|
+
.description("List pipeline roles or show role template instructions")
|
|
147
|
+
.action(async (subcommand, role) => {
|
|
148
|
+
await withConfig("roles", {}, async ({ config }) => {
|
|
149
|
+
await rolesCommand({ config, subcommand: subcommand || "list", roleName: role });
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
program
|
|
154
|
+
.command("plan")
|
|
155
|
+
.description("Generate implementation plan")
|
|
156
|
+
.argument("<task>")
|
|
157
|
+
.option("--planner <name>")
|
|
158
|
+
.option("--planner-model <name>")
|
|
159
|
+
.option("--context <text>", "Additional context for the planner")
|
|
160
|
+
.option("--json", "Output raw JSON plan")
|
|
161
|
+
.action(async (task, flags) => {
|
|
162
|
+
await withConfig("plan", flags, async ({ config, logger }) => {
|
|
163
|
+
await planCommand({ task, config, logger, json: flags.json, context: flags.context });
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
program
|
|
168
|
+
.command("resume")
|
|
169
|
+
.description("Resume a paused session")
|
|
170
|
+
.argument("<sessionId>")
|
|
171
|
+
.option("--answer <text>", "Answer to the question that caused the pause")
|
|
172
|
+
.option("--json", "Output JSON only")
|
|
173
|
+
.action(async (sessionId, flags) => {
|
|
174
|
+
await withConfig("resume", flags, async ({ config, logger }) => {
|
|
175
|
+
await resumeCommand({ sessionId, answer: flags.answer, config, logger, flags });
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const sonar = program.command("sonar").description("Manage SonarQube container");
|
|
180
|
+
sonar.command("status").action(async () => sonarCommand({ action: "status" }));
|
|
181
|
+
sonar.command("start").action(async () => sonarCommand({ action: "start" }));
|
|
182
|
+
sonar.command("stop").action(async () => sonarCommand({ action: "stop" }));
|
|
183
|
+
sonar.command("logs").action(async () => sonarCommand({ action: "logs" }));
|
|
184
|
+
sonar
|
|
185
|
+
.command("open")
|
|
186
|
+
.description("Open SonarQube dashboard in the browser")
|
|
187
|
+
.action(async () => {
|
|
188
|
+
const { config } = await loadConfig();
|
|
189
|
+
const result = await sonarOpenCommand({ config });
|
|
190
|
+
if (!result.ok) {
|
|
191
|
+
console.error(result.error);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
console.log(`Opened ${result.url}`);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
program.parseAsync().catch((error) => {
|
|
198
|
+
console.error(error.message);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { createAgent } from "../agents/index.js";
|
|
3
|
+
import { assertAgentsAvailable } from "../agents/availability.js";
|
|
4
|
+
import { buildCoderPrompt } from "../prompts/coder.js";
|
|
5
|
+
import { resolveRole } from "../config.js";
|
|
6
|
+
|
|
7
|
+
export async function codeCommand({ task, config, logger }) {
|
|
8
|
+
const coderRole = resolveRole(config, "coder");
|
|
9
|
+
await assertAgentsAvailable([coderRole.provider]);
|
|
10
|
+
logger.info(`Coder (${coderRole.provider}) starting...`);
|
|
11
|
+
const coder = createAgent(coderRole.provider, config, logger);
|
|
12
|
+
let coderRules = null;
|
|
13
|
+
if (config.coder_rules) {
|
|
14
|
+
try {
|
|
15
|
+
coderRules = await fs.readFile(config.coder_rules, "utf8");
|
|
16
|
+
} catch { /* no coder rules file, that's ok */ }
|
|
17
|
+
}
|
|
18
|
+
const prompt = buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology || "tdd" });
|
|
19
|
+
const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
|
|
20
|
+
const result = await coder.runTask({ prompt, onOutput, role: "coder" });
|
|
21
|
+
if (!result.ok) {
|
|
22
|
+
if (result.error) logger.error(result.error);
|
|
23
|
+
throw new Error(result.error || result.output || `Coder failed (exit ${result.exitCode})`);
|
|
24
|
+
}
|
|
25
|
+
if (result.output) {
|
|
26
|
+
console.log(result.output);
|
|
27
|
+
}
|
|
28
|
+
if (result.error) {
|
|
29
|
+
logger.warn(result.error);
|
|
30
|
+
}
|
|
31
|
+
logger.info(`Coder completed (exit ${result.exitCode})`);
|
|
32
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
import { loadConfig, validateConfig, getConfigPath, writeConfig } from "../config.js";
|
|
5
|
+
import { ensureDir } from "../utils/fs.js";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
function resolveEditor() {
|
|
9
|
+
const raw = process.env.VISUAL || process.env.EDITOR || "vi";
|
|
10
|
+
const parts = raw.split(/\s+/);
|
|
11
|
+
return { cmd: parts[0], args: parts.slice(1) };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function editConfigOnce(configPath) {
|
|
15
|
+
const { cmd, args } = resolveEditor();
|
|
16
|
+
const result = spawnSync(cmd, [...args, configPath], { stdio: "inherit" });
|
|
17
|
+
|
|
18
|
+
if (result.status !== 0) {
|
|
19
|
+
return { ok: false, error: `Editor exited with code ${result.status}` };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let raw;
|
|
23
|
+
try {
|
|
24
|
+
raw = await fs.readFile(configPath, "utf8");
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return { ok: false, error: `Could not read config: ${err.message}` };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = yaml.load(raw);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
return { ok: false, error: `Invalid YAML: ${err.message}` };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
validateConfig(parsed || {});
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return { ok: false, error: err.message };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { ok: true, config: parsed };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function configCommand({ json = false, edit = false }) {
|
|
46
|
+
if (edit) {
|
|
47
|
+
const configPath = getConfigPath();
|
|
48
|
+
await ensureDir(path.dirname(configPath));
|
|
49
|
+
const { exists: configExists } = await loadConfig();
|
|
50
|
+
if (!configExists) {
|
|
51
|
+
const { config: defaults } = await loadConfig();
|
|
52
|
+
await writeConfig(configPath, defaults);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result = await editConfigOnce(configPath);
|
|
56
|
+
if (result.ok) {
|
|
57
|
+
console.log("Configuration saved and validated.");
|
|
58
|
+
} else {
|
|
59
|
+
console.error(`Validation error: ${result.error}`);
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { config, path: cfgPath, exists } = await loadConfig();
|
|
66
|
+
if (json) {
|
|
67
|
+
console.log(JSON.stringify({ path: cfgPath, exists, config }, null, 2));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(`Config path: ${cfgPath}`);
|
|
72
|
+
console.log(`Exists: ${exists}`);
|
|
73
|
+
console.log(JSON.stringify(config, null, 2));
|
|
74
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { runCommand } from "../utils/process.js";
|
|
2
|
+
import { exists } from "../utils/fs.js";
|
|
3
|
+
import { getConfigPath } from "../config.js";
|
|
4
|
+
import { isSonarReachable } from "../sonar/manager.js";
|
|
5
|
+
import { resolveRoleMdPath, loadFirstExisting } from "../roles/base-role.js";
|
|
6
|
+
import { ensureGitRepo } from "../utils/git.js";
|
|
7
|
+
import { checkBinary, KNOWN_AGENTS } from "../utils/agent-detect.js";
|
|
8
|
+
|
|
9
|
+
export async function runChecks({ config }) {
|
|
10
|
+
const checks = [];
|
|
11
|
+
|
|
12
|
+
// 1. Config file
|
|
13
|
+
const configPath = getConfigPath();
|
|
14
|
+
const configExists = await exists(configPath);
|
|
15
|
+
checks.push({
|
|
16
|
+
name: "config",
|
|
17
|
+
label: "Config file",
|
|
18
|
+
ok: configExists,
|
|
19
|
+
detail: configExists ? configPath : "Not found",
|
|
20
|
+
fix: configExists ? null : "Run 'kj init' to create the config file."
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// 2. Git repository
|
|
24
|
+
let gitOk = false;
|
|
25
|
+
try {
|
|
26
|
+
gitOk = await ensureGitRepo();
|
|
27
|
+
} catch {
|
|
28
|
+
gitOk = false;
|
|
29
|
+
}
|
|
30
|
+
checks.push({
|
|
31
|
+
name: "git",
|
|
32
|
+
label: "Git repository",
|
|
33
|
+
ok: gitOk,
|
|
34
|
+
detail: gitOk ? "Inside a git repository" : "Not a git repository",
|
|
35
|
+
fix: gitOk ? null : "Run 'git init' or navigate to a git-managed project."
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// 3. Docker
|
|
39
|
+
const docker = await checkBinary("docker", "--version");
|
|
40
|
+
checks.push({
|
|
41
|
+
name: "docker",
|
|
42
|
+
label: "Docker",
|
|
43
|
+
ok: docker.ok,
|
|
44
|
+
detail: docker.ok ? docker.version : "Not found",
|
|
45
|
+
fix: docker.ok ? null : "Install Docker: https://docs.docker.com/get-docker/"
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// 4. SonarQube reachability
|
|
49
|
+
const sonarHost = config.sonarqube?.host || "http://localhost:9000";
|
|
50
|
+
let sonarOk = false;
|
|
51
|
+
if (config.sonarqube?.enabled !== false) {
|
|
52
|
+
try {
|
|
53
|
+
sonarOk = await isSonarReachable(sonarHost);
|
|
54
|
+
} catch {
|
|
55
|
+
sonarOk = false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
checks.push({
|
|
59
|
+
name: "sonarqube",
|
|
60
|
+
label: "SonarQube",
|
|
61
|
+
ok: sonarOk || config.sonarqube?.enabled === false,
|
|
62
|
+
detail: config.sonarqube?.enabled === false
|
|
63
|
+
? "Disabled in config"
|
|
64
|
+
: sonarOk
|
|
65
|
+
? `Reachable at ${sonarHost}`
|
|
66
|
+
: `Not reachable at ${sonarHost}`,
|
|
67
|
+
fix: sonarOk || config.sonarqube?.enabled === false
|
|
68
|
+
? null
|
|
69
|
+
: "Run 'kj sonar start' or 'docker start karajan-sonarqube'. Use --no-sonar to skip."
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 5. Agent CLIs
|
|
73
|
+
for (const agent of KNOWN_AGENTS) {
|
|
74
|
+
const result = await checkBinary(agent.name);
|
|
75
|
+
checks.push({
|
|
76
|
+
name: `agent:${agent.name}`,
|
|
77
|
+
label: `Agent: ${agent.name}`,
|
|
78
|
+
ok: result.ok,
|
|
79
|
+
detail: result.ok ? `${result.version} (${result.path})` : "Not found",
|
|
80
|
+
fix: result.ok ? null : `Install: ${agent.install}`
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 6. Core binaries
|
|
85
|
+
for (const bin of ["node", "npm", "git"]) {
|
|
86
|
+
const result = await checkBinary(bin);
|
|
87
|
+
checks.push({
|
|
88
|
+
name: bin,
|
|
89
|
+
label: bin,
|
|
90
|
+
ok: result.ok,
|
|
91
|
+
detail: result.ok ? result.version : "Not found",
|
|
92
|
+
fix: result.ok ? null : `Install ${bin} from its official website.`
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 7. Serena MCP
|
|
97
|
+
if (config.serena?.enabled) {
|
|
98
|
+
let serenaOk = false;
|
|
99
|
+
try {
|
|
100
|
+
const serenaCheck = await runCommand("serena", ["--version"]);
|
|
101
|
+
serenaOk = serenaCheck.exitCode === 0;
|
|
102
|
+
} catch {
|
|
103
|
+
serenaOk = false;
|
|
104
|
+
}
|
|
105
|
+
checks.push({
|
|
106
|
+
name: "serena",
|
|
107
|
+
label: "Serena MCP",
|
|
108
|
+
ok: serenaOk,
|
|
109
|
+
detail: serenaOk ? "Available" : "Not found (prompts will still include Serena instructions)",
|
|
110
|
+
fix: serenaOk ? null : "Install Serena: uvx --from git+https://github.com/oraios/serena serena --help"
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 8. Review rules / Coder rules
|
|
115
|
+
const projectDir = config.projectDir || process.cwd();
|
|
116
|
+
const reviewRules = await loadFirstExisting(resolveRoleMdPath("reviewer", projectDir));
|
|
117
|
+
const coderRules = await loadFirstExisting(resolveRoleMdPath("coder", projectDir));
|
|
118
|
+
checks.push({
|
|
119
|
+
name: "review-rules",
|
|
120
|
+
label: "Reviewer rules (.md)",
|
|
121
|
+
ok: Boolean(reviewRules),
|
|
122
|
+
detail: reviewRules ? "Found" : "Not found (will use defaults)",
|
|
123
|
+
fix: null
|
|
124
|
+
});
|
|
125
|
+
checks.push({
|
|
126
|
+
name: "coder-rules",
|
|
127
|
+
label: "Coder rules (.md)",
|
|
128
|
+
ok: Boolean(coderRules),
|
|
129
|
+
detail: coderRules ? "Found" : "Not found (will use defaults)",
|
|
130
|
+
fix: null
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return checks;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function doctorCommand({ config }) {
|
|
137
|
+
const checks = await runChecks({ config });
|
|
138
|
+
|
|
139
|
+
for (const check of checks) {
|
|
140
|
+
const icon = check.ok ? "OK " : "MISS";
|
|
141
|
+
console.log(`${icon} ${check.label}: ${check.detail}`);
|
|
142
|
+
if (check.fix) {
|
|
143
|
+
console.log(` -> ${check.fix}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const failures = checks.filter((c) => !c.ok && c.fix);
|
|
148
|
+
if (failures.length === 0) {
|
|
149
|
+
console.log("\nAll checks passed.");
|
|
150
|
+
} else {
|
|
151
|
+
console.log(`\n${failures.length} issue(s) found.`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return checks;
|
|
155
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { getConfigPath, loadConfig, writeConfig } from "../config.js";
|
|
5
|
+
import { sonarUp, checkVmMaxMapCount } from "../sonar/manager.js";
|
|
6
|
+
import { exists, ensureDir } from "../utils/fs.js";
|
|
7
|
+
import { getKarajanHome } from "../utils/paths.js";
|
|
8
|
+
import { detectAvailableAgents } from "../utils/agent-detect.js";
|
|
9
|
+
import { createWizard, isTTY } from "../utils/wizard.js";
|
|
10
|
+
|
|
11
|
+
async function runWizard(config, logger) {
|
|
12
|
+
const agents = await detectAvailableAgents();
|
|
13
|
+
const available = agents.filter((a) => a.available);
|
|
14
|
+
const unavailable = agents.filter((a) => !a.available);
|
|
15
|
+
|
|
16
|
+
logger.info("\n=== Karajan Code Setup Wizard ===\n");
|
|
17
|
+
|
|
18
|
+
if (available.length === 0) {
|
|
19
|
+
logger.warn("No AI agents detected. Install at least one:");
|
|
20
|
+
for (const agent of unavailable) {
|
|
21
|
+
logger.warn(` ${agent.name}: ${agent.install}`);
|
|
22
|
+
}
|
|
23
|
+
logger.info("\nGenerating config with defaults (claude). You can change it later in kj.config.yml.\n");
|
|
24
|
+
return config;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
logger.info("Detected agents:");
|
|
28
|
+
for (const agent of available) {
|
|
29
|
+
logger.info(` OK ${agent.name} (${agent.version})`);
|
|
30
|
+
}
|
|
31
|
+
for (const agent of unavailable) {
|
|
32
|
+
logger.info(` MISS ${agent.name}`);
|
|
33
|
+
}
|
|
34
|
+
logger.info("");
|
|
35
|
+
|
|
36
|
+
if (available.length === 1) {
|
|
37
|
+
const only = available[0].name;
|
|
38
|
+
logger.info(`Only one agent available: ${only}. Using it for all roles.\n`);
|
|
39
|
+
config.coder = only;
|
|
40
|
+
config.reviewer = only;
|
|
41
|
+
config.roles.coder.provider = only;
|
|
42
|
+
config.roles.reviewer.provider = only;
|
|
43
|
+
return config;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const wizard = createWizard();
|
|
47
|
+
try {
|
|
48
|
+
const agentOptions = available.map((a) => ({
|
|
49
|
+
label: `${a.name} (${a.version})`,
|
|
50
|
+
value: a.name,
|
|
51
|
+
available: true
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
const coder = await wizard.select("Select default CODER agent:", agentOptions);
|
|
55
|
+
config.coder = coder;
|
|
56
|
+
config.roles.coder.provider = coder;
|
|
57
|
+
logger.info(` -> Coder: ${coder}`);
|
|
58
|
+
|
|
59
|
+
const reviewer = await wizard.select("Select default REVIEWER agent:", agentOptions);
|
|
60
|
+
config.reviewer = reviewer;
|
|
61
|
+
config.roles.reviewer.provider = reviewer;
|
|
62
|
+
logger.info(` -> Reviewer: ${reviewer}`);
|
|
63
|
+
|
|
64
|
+
const enableTriage = await wizard.confirm("Enable triage (auto-classify task complexity)?", false);
|
|
65
|
+
config.pipeline.triage = config.pipeline.triage || {};
|
|
66
|
+
config.pipeline.triage.enabled = enableTriage;
|
|
67
|
+
logger.info(` -> Triage: ${enableTriage ? "enabled" : "disabled"}`);
|
|
68
|
+
|
|
69
|
+
const enableSonar = await wizard.confirm("Enable SonarQube analysis?", true);
|
|
70
|
+
config.sonarqube.enabled = enableSonar;
|
|
71
|
+
logger.info(` -> SonarQube: ${enableSonar ? "enabled" : "disabled"}`);
|
|
72
|
+
|
|
73
|
+
const methodology = await wizard.select("Development methodology:", [
|
|
74
|
+
{ label: "TDD (test-driven development)", value: "tdd", available: true },
|
|
75
|
+
{ label: "Standard (no TDD enforcement)", value: "standard", available: true }
|
|
76
|
+
]);
|
|
77
|
+
config.development.methodology = methodology;
|
|
78
|
+
config.development.require_test_changes = methodology === "tdd";
|
|
79
|
+
logger.info(` -> Methodology: ${methodology}`);
|
|
80
|
+
|
|
81
|
+
logger.info("");
|
|
82
|
+
} finally {
|
|
83
|
+
wizard.close();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return config;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function initCommand({ logger, flags = {} }) {
|
|
90
|
+
const karajanHome = getKarajanHome();
|
|
91
|
+
await ensureDir(karajanHome);
|
|
92
|
+
logger.info(`Ensured ${karajanHome} exists`);
|
|
93
|
+
|
|
94
|
+
const configPath = getConfigPath();
|
|
95
|
+
const reviewRulesPath = path.resolve(process.cwd(), "review-rules.md");
|
|
96
|
+
const coderRulesPath = path.resolve(process.cwd(), "coder-rules.md");
|
|
97
|
+
|
|
98
|
+
const { config, exists: configExists } = await loadConfig();
|
|
99
|
+
const interactive = flags.noInteractive !== true && isTTY();
|
|
100
|
+
|
|
101
|
+
if (configExists && interactive) {
|
|
102
|
+
const wizard = createWizard();
|
|
103
|
+
try {
|
|
104
|
+
const reconfigure = await wizard.confirm("Config already exists. Reconfigure?", false);
|
|
105
|
+
if (reconfigure) {
|
|
106
|
+
const updated = await runWizard(config, logger);
|
|
107
|
+
await writeConfig(configPath, updated);
|
|
108
|
+
logger.info(`Updated ${configPath}`);
|
|
109
|
+
} else {
|
|
110
|
+
logger.info("Keeping existing config.");
|
|
111
|
+
}
|
|
112
|
+
} finally {
|
|
113
|
+
wizard.close();
|
|
114
|
+
}
|
|
115
|
+
} else if (!configExists && interactive) {
|
|
116
|
+
const updated = await runWizard(config, logger);
|
|
117
|
+
await writeConfig(configPath, updated);
|
|
118
|
+
logger.info(`Created ${configPath}`);
|
|
119
|
+
} else if (!configExists) {
|
|
120
|
+
await writeConfig(configPath, config);
|
|
121
|
+
logger.info(`Created ${configPath}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!(await exists(reviewRulesPath))) {
|
|
125
|
+
await fs.writeFile(
|
|
126
|
+
reviewRulesPath,
|
|
127
|
+
"# Review Rules\n\n- Focus on security, correctness, and test coverage.\n",
|
|
128
|
+
"utf8"
|
|
129
|
+
);
|
|
130
|
+
logger.info("Created review-rules.md");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!(await exists(coderRulesPath))) {
|
|
134
|
+
const templatePath = path.resolve(import.meta.dirname, "../../templates/coder-rules.md");
|
|
135
|
+
let content;
|
|
136
|
+
try {
|
|
137
|
+
content = await fs.readFile(templatePath, "utf8");
|
|
138
|
+
} catch {
|
|
139
|
+
content = [
|
|
140
|
+
"# Coder Rules",
|
|
141
|
+
"",
|
|
142
|
+
"## File modification safety",
|
|
143
|
+
"",
|
|
144
|
+
"- NEVER overwrite existing files entirely. Always make targeted, minimal edits.",
|
|
145
|
+
"- After each edit, verify with `git diff` that ONLY the intended lines changed.",
|
|
146
|
+
"- Do not modify code unrelated to the task.",
|
|
147
|
+
""
|
|
148
|
+
].join("\n");
|
|
149
|
+
}
|
|
150
|
+
await fs.writeFile(coderRulesPath, content, "utf8");
|
|
151
|
+
logger.info("Created coder-rules.md");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (config.sonarqube?.enabled !== false) {
|
|
155
|
+
const vmCheck = await checkVmMaxMapCount(os.platform());
|
|
156
|
+
if (!vmCheck.ok) {
|
|
157
|
+
logger.warn(`vm.max_map_count check failed: ${vmCheck.reason}`);
|
|
158
|
+
if (vmCheck.fix) {
|
|
159
|
+
logger.warn(`Fix: ${vmCheck.fix}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const sonar = await sonarUp();
|
|
164
|
+
if (sonar.exitCode !== 0) {
|
|
165
|
+
throw new Error(`Failed to start SonarQube: ${sonar.stderr || sonar.stdout}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
logger.info("SonarQube container started");
|
|
169
|
+
|
|
170
|
+
logger.info("");
|
|
171
|
+
logger.info("To configure the SonarQube token:");
|
|
172
|
+
logger.info(" 1. Open http://localhost:9000");
|
|
173
|
+
logger.info(" 2. Log in (default credentials: admin / admin)");
|
|
174
|
+
logger.info(" 3. Go to: My Account > Security > Generate Token");
|
|
175
|
+
logger.info(" 4. Name: karajan-cli, Type: Global Analysis Token");
|
|
176
|
+
logger.info(" 5. Set the token in ~/.karajan/kj.config.yml under sonarqube.token");
|
|
177
|
+
logger.info(' or export KJ_SONAR_TOKEN="<your-token>"');
|
|
178
|
+
} else {
|
|
179
|
+
logger.info("SonarQube disabled — skipping container setup.");
|
|
180
|
+
}
|
|
181
|
+
}
|