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
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import readline from "node:readline/promises";
|
|
7
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
8
|
+
|
|
9
|
+
const rl = readline.createInterface({ input, output });
|
|
10
|
+
const REGISTRY_PATH = path.join(os.homedir(), ".karajan", "instances.json");
|
|
11
|
+
const INSTALL_STATE_PATH = path.join(os.homedir(), ".karajan", "install-state.json");
|
|
12
|
+
const AGENT_META = {
|
|
13
|
+
codex: { bin: "codex", installUrl: "https://developers.openai.com/codex/cli" },
|
|
14
|
+
claude: { bin: "claude", installUrl: "https://docs.anthropic.com/en/docs/claude-code" },
|
|
15
|
+
gemini: { bin: "gemini", installUrl: "https://github.com/google-gemini/gemini-cli" },
|
|
16
|
+
aider: { bin: "aider", installUrl: "https://aider.chat/docs/install.html" }
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function printHeader() {
|
|
20
|
+
console.log("\nKarajan Code Installer\n");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function printHelp() {
|
|
24
|
+
console.log(`Usage:
|
|
25
|
+
./scripts/install.sh [options]
|
|
26
|
+
node scripts/install.js [options]
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
--non-interactive Run without prompts (CI-friendly)
|
|
30
|
+
--instance-name <name> Instance name (default: default)
|
|
31
|
+
--instance-action <mode> add | update | replace
|
|
32
|
+
--recovery-action <mode> continue | restart (if previous install was interrupted)
|
|
33
|
+
--link-global <bool> Run npm link (default: true interactive, false non-interactive)
|
|
34
|
+
--kj-home <path> KJ_HOME path
|
|
35
|
+
--sonar-host <url> SonarQube host (default: http://localhost:9000)
|
|
36
|
+
--generate-sonar-token <bool> Generate token using sonar admin credentials
|
|
37
|
+
--sonar-user <user> Sonar username (default: admin)
|
|
38
|
+
--sonar-password <pass> Sonar password
|
|
39
|
+
--sonar-token-name <name> Sonar token name (default: karajan-cli)
|
|
40
|
+
--sonar-token <token> Existing KJ_SONAR_TOKEN
|
|
41
|
+
--coder <name> Default coder (default: codex)
|
|
42
|
+
--reviewer <name> Default reviewer (default: claude)
|
|
43
|
+
--reviewer-fallback <name> Default reviewer fallback (default: codex)
|
|
44
|
+
--setup-mcp-claude <bool> Configure Claude MCP
|
|
45
|
+
--setup-mcp-codex <bool> Configure Codex MCP
|
|
46
|
+
--setup-chrome-devtools <bool> Configure Chrome DevTools MCP (default: true)
|
|
47
|
+
--run-doctor <bool> Run kj doctor at end
|
|
48
|
+
--run-tests <bool> Run tests at end (default: true)
|
|
49
|
+
--help Show this help
|
|
50
|
+
|
|
51
|
+
Environment variable equivalents:
|
|
52
|
+
KJ_INSTALL_NON_INTERACTIVE
|
|
53
|
+
KJ_INSTALL_INSTANCE_NAME
|
|
54
|
+
KJ_INSTALL_INSTANCE_ACTION
|
|
55
|
+
KJ_INSTALL_RECOVERY_ACTION
|
|
56
|
+
KJ_INSTALL_LINK_GLOBAL
|
|
57
|
+
KJ_INSTALL_KJ_HOME
|
|
58
|
+
KJ_INSTALL_SONAR_HOST
|
|
59
|
+
KJ_INSTALL_GENERATE_SONAR_TOKEN
|
|
60
|
+
KJ_INSTALL_SONAR_USER
|
|
61
|
+
KJ_INSTALL_SONAR_PASSWORD
|
|
62
|
+
KJ_INSTALL_SONAR_TOKEN_NAME
|
|
63
|
+
KJ_SONAR_TOKEN
|
|
64
|
+
KJ_INSTALL_CODER
|
|
65
|
+
KJ_INSTALL_REVIEWER
|
|
66
|
+
KJ_INSTALL_REVIEWER_FALLBACK
|
|
67
|
+
KJ_INSTALL_SETUP_MCP_CLAUDE
|
|
68
|
+
KJ_INSTALL_SETUP_MCP_CODEX
|
|
69
|
+
KJ_INSTALL_SETUP_CHROME_DEVTOOLS
|
|
70
|
+
KJ_INSTALL_RUN_DOCTOR
|
|
71
|
+
KJ_INSTALL_RUN_TESTS
|
|
72
|
+
`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseArgs(argv) {
|
|
76
|
+
const out = {};
|
|
77
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
78
|
+
const token = argv[i];
|
|
79
|
+
if (!token.startsWith("--")) continue;
|
|
80
|
+
|
|
81
|
+
const key = token.slice(2);
|
|
82
|
+
if (key === "help") {
|
|
83
|
+
out.help = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (key === "non-interactive") {
|
|
88
|
+
out.nonInteractive = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const next = argv[i + 1];
|
|
93
|
+
if (!next || next.startsWith("--")) {
|
|
94
|
+
out[toCamelCase(key)] = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
out[toCamelCase(key)] = next;
|
|
99
|
+
i += 1;
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function toCamelCase(kebab) {
|
|
105
|
+
return kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseBool(value, defaultValue = false) {
|
|
109
|
+
if (value === undefined || value === null || value === "") return defaultValue;
|
|
110
|
+
if (typeof value === "boolean") return value;
|
|
111
|
+
const normalized = String(value).trim().toLowerCase();
|
|
112
|
+
if (["1", "true", "yes", "y", "si", "s"].includes(normalized)) return true;
|
|
113
|
+
if (["0", "false", "no", "n"].includes(normalized)) return false;
|
|
114
|
+
return defaultValue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function pickSetting({ cli, cliKey, envKey, fallback }) {
|
|
118
|
+
const cliValue = cli?.[cliKey];
|
|
119
|
+
if (cliValue !== undefined) return cliValue;
|
|
120
|
+
const envValue = process.env[envKey];
|
|
121
|
+
if (envValue !== undefined) return envValue;
|
|
122
|
+
return fallback;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function run(command, args = [], options = {}) {
|
|
126
|
+
const { cwd, env, timeout } = options;
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
const child = spawn(command, args, {
|
|
129
|
+
cwd,
|
|
130
|
+
env,
|
|
131
|
+
shell: false
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
let stdout = "";
|
|
135
|
+
let stderr = "";
|
|
136
|
+
let killedByTimeout = false;
|
|
137
|
+
let timer = null;
|
|
138
|
+
|
|
139
|
+
if (timeout) {
|
|
140
|
+
timer = setTimeout(() => {
|
|
141
|
+
killedByTimeout = true;
|
|
142
|
+
child.kill("SIGKILL");
|
|
143
|
+
}, Number(timeout));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
child.stdout.on("data", (chunk) => {
|
|
147
|
+
stdout += chunk.toString();
|
|
148
|
+
});
|
|
149
|
+
child.stderr.on("data", (chunk) => {
|
|
150
|
+
stderr += chunk.toString();
|
|
151
|
+
});
|
|
152
|
+
child.on("error", (error) => {
|
|
153
|
+
if (timer) clearTimeout(timer);
|
|
154
|
+
resolve({ exitCode: 1, stdout, stderr: `${stderr}${error.message}` });
|
|
155
|
+
});
|
|
156
|
+
child.on("close", (code) => {
|
|
157
|
+
if (timer) clearTimeout(timer);
|
|
158
|
+
if (killedByTimeout) {
|
|
159
|
+
resolve({ exitCode: 143, stdout, stderr: `${stderr}Command timed out` });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
resolve({ exitCode: code ?? 1, stdout, stderr });
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function ensureCommand(name, checkArgs = ["--version"]) {
|
|
168
|
+
const res = await run(name, checkArgs);
|
|
169
|
+
return res.exitCode === 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function ask(question, defaultValue = "") {
|
|
173
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
174
|
+
const value = (await rl.question(`${question}${suffix}: `)).trim();
|
|
175
|
+
return value || defaultValue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function askBool(question, defaultYes = true) {
|
|
179
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
180
|
+
const raw = (await rl.question(`${question} (${hint}): `)).trim().toLowerCase();
|
|
181
|
+
if (!raw) return defaultYes;
|
|
182
|
+
return ["y", "yes", "s", "si"].includes(raw);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function askChoice(question, options) {
|
|
186
|
+
console.log(question);
|
|
187
|
+
for (let i = 0; i < options.length; i += 1) {
|
|
188
|
+
console.log(`${i + 1}) ${options[i].label}`);
|
|
189
|
+
}
|
|
190
|
+
const raw = (await rl.question("Selecciona una opcion: ")).trim();
|
|
191
|
+
const idx = Number(raw) - 1;
|
|
192
|
+
if (!Number.isFinite(idx) || idx < 0 || idx >= options.length) {
|
|
193
|
+
return options[0].value;
|
|
194
|
+
}
|
|
195
|
+
return options[idx].value;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function readJson(file) {
|
|
199
|
+
const raw = await fs.readFile(file, "utf8");
|
|
200
|
+
return JSON.parse(raw);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function writeJson(file, obj) {
|
|
204
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
205
|
+
await fs.writeFile(file, `${JSON.stringify(obj, null, 2)}\n`, "utf8");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function loadRegistry() {
|
|
209
|
+
try {
|
|
210
|
+
return await readJson(REGISTRY_PATH);
|
|
211
|
+
} catch {
|
|
212
|
+
return { instances: {} };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function saveRegistry(registry) {
|
|
217
|
+
await writeJson(REGISTRY_PATH, registry);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function loadInstallState() {
|
|
221
|
+
try {
|
|
222
|
+
return await readJson(INSTALL_STATE_PATH);
|
|
223
|
+
} catch {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function saveInstallState(state) {
|
|
229
|
+
await writeJson(INSTALL_STATE_PATH, state);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function clearInstallState() {
|
|
233
|
+
await fs.rm(INSTALL_STATE_PATH, { force: true });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function upsertCodexMcpBlock(toml, block) {
|
|
237
|
+
const begin = "# BEGIN karajan-mcp";
|
|
238
|
+
const end = "# END karajan-mcp";
|
|
239
|
+
const startIdx = toml.indexOf(begin);
|
|
240
|
+
const endIdx = toml.indexOf(end);
|
|
241
|
+
let base = toml;
|
|
242
|
+
|
|
243
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
244
|
+
base = `${toml.slice(0, startIdx).trimEnd()}\n\n${toml.slice(endIdx + end.length).trimStart()}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return `${base.trimEnd()}\n\n${begin}\n${block}\n${end}\n`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function setupCodexMcp({ rootDir, kjHome }) {
|
|
251
|
+
const codexConfigPath = path.join(os.homedir(), ".codex", "config.toml");
|
|
252
|
+
let toml = "";
|
|
253
|
+
try {
|
|
254
|
+
toml = await fs.readFile(codexConfigPath, "utf8");
|
|
255
|
+
} catch {
|
|
256
|
+
toml = "";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const block = [
|
|
260
|
+
'[mcp_servers."karajan-mcp"]',
|
|
261
|
+
'command = "node"',
|
|
262
|
+
`args = ["${path.join(rootDir, "src", "mcp", "server.js")}"]`,
|
|
263
|
+
`cwd = "${rootDir}"`,
|
|
264
|
+
'[mcp_servers."karajan-mcp".env]',
|
|
265
|
+
`KJ_HOME = "${kjHome}"`
|
|
266
|
+
].join("\n");
|
|
267
|
+
|
|
268
|
+
const updated = upsertCodexMcpBlock(toml, block);
|
|
269
|
+
await fs.mkdir(path.dirname(codexConfigPath), { recursive: true });
|
|
270
|
+
await fs.writeFile(codexConfigPath, updated, "utf8");
|
|
271
|
+
return codexConfigPath;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function setupClaudeMcp({ rootDir, kjHome }) {
|
|
275
|
+
const claudeJsonPath = path.join(os.homedir(), ".claude.json");
|
|
276
|
+
let config = {};
|
|
277
|
+
try {
|
|
278
|
+
config = await readJson(claudeJsonPath);
|
|
279
|
+
} catch {
|
|
280
|
+
config = {};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
config.mcpServers = config.mcpServers || {};
|
|
284
|
+
config.mcpServers["karajan-mcp"] = {
|
|
285
|
+
type: "stdio",
|
|
286
|
+
command: "node",
|
|
287
|
+
args: [path.join(rootDir, "src", "mcp", "server.js")],
|
|
288
|
+
cwd: rootDir,
|
|
289
|
+
env: { KJ_HOME: kjHome }
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
await writeJson(claudeJsonPath, config);
|
|
293
|
+
return claudeJsonPath;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function generateSonarToken({ host, user, password, tokenName }) {
|
|
297
|
+
const url = `${host.replace(/\/$/, "")}/api/user_tokens/generate?name=${encodeURIComponent(tokenName)}`;
|
|
298
|
+
const res = await run("curl", ["-s", "-u", `${user}:${password}`, "-X", "POST", url]);
|
|
299
|
+
if (res.exitCode !== 0) {
|
|
300
|
+
throw new Error(res.stderr || res.stdout || "Could not generate Sonar token");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const parsed = JSON.parse(res.stdout || "{}");
|
|
304
|
+
if (!parsed.token) {
|
|
305
|
+
throw new Error(`Unexpected Sonar response: ${res.stdout}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return parsed.token;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function readExistingSonarToken(kjHome) {
|
|
312
|
+
if (process.env.KJ_SONAR_TOKEN) return process.env.KJ_SONAR_TOKEN;
|
|
313
|
+
if (!kjHome) return "";
|
|
314
|
+
try {
|
|
315
|
+
const envContent = await fs.readFile(path.join(kjHome, "karajan.env"), "utf8");
|
|
316
|
+
const match = envContent.match(/export KJ_SONAR_TOKEN="([^"]*)"/);
|
|
317
|
+
return match?.[1] || "";
|
|
318
|
+
} catch {
|
|
319
|
+
return "";
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function ensureSonarQube(kjHome, nonInteractive) {
|
|
324
|
+
const composePath = path.join(kjHome, "docker-compose.sonar.yml");
|
|
325
|
+
const sonarUrl = "http://localhost:9000";
|
|
326
|
+
|
|
327
|
+
const hasDocker = await ensureCommand("docker");
|
|
328
|
+
if (!hasDocker) {
|
|
329
|
+
console.log("Docker not found. SonarQube requires Docker.");
|
|
330
|
+
console.log("Install Docker: https://docs.docker.com/get-docker/");
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Check if sonarqube container is already running
|
|
335
|
+
const ps = await run("docker", ["ps", "--format", "{{.Names}}"]);
|
|
336
|
+
if (ps.stdout.split("\n").some((n) => n.trim() === "karajan-sonarqube")) {
|
|
337
|
+
console.log("SonarQube already running at " + sonarUrl);
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Check if container exists but is stopped
|
|
342
|
+
const psAll = await run("docker", ["ps", "-a", "--format", "{{.Names}}"]);
|
|
343
|
+
if (psAll.stdout.split("\n").some((n) => n.trim() === "karajan-sonarqube")) {
|
|
344
|
+
console.log("SonarQube container exists but stopped, starting...");
|
|
345
|
+
const start = await run("docker", ["start", "karajan-sonarqube"]);
|
|
346
|
+
if (start.exitCode === 0) {
|
|
347
|
+
console.log("SonarQube started at " + sonarUrl);
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
console.log("Failed to start existing container, will recreate.");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Fresh install — copy compose if not present
|
|
354
|
+
try {
|
|
355
|
+
await fs.access(composePath);
|
|
356
|
+
} catch {
|
|
357
|
+
const templatePath = path.join(process.cwd(), "templates", "docker-compose.sonar.yml");
|
|
358
|
+
try {
|
|
359
|
+
await fs.mkdir(kjHome, { recursive: true });
|
|
360
|
+
await fs.copyFile(templatePath, composePath);
|
|
361
|
+
} catch {
|
|
362
|
+
console.log("docker-compose.sonar.yml template not found, skipping Docker setup.");
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Check vm.max_map_count on Linux/WSL2 (required by Elasticsearch in SonarQube)
|
|
368
|
+
if (os.platform() === "linux") {
|
|
369
|
+
const sysctl = await run("sysctl", ["vm.max_map_count"]);
|
|
370
|
+
const match = sysctl.stdout?.match(/=\s*(\d+)/);
|
|
371
|
+
const current = match ? Number(match[1]) : 0;
|
|
372
|
+
if (current < 262144) {
|
|
373
|
+
console.log(`\nvm.max_map_count = ${current} (needs >= 262144 for SonarQube)`);
|
|
374
|
+
console.log("Fix with: sudo sysctl -w vm.max_map_count=262144");
|
|
375
|
+
console.log("Persist: echo 'vm.max_map_count=262144' | sudo tee -a /etc/sysctl.conf\n");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!nonInteractive) {
|
|
380
|
+
console.log("\nSonarQube is not running. Karajan uses SonarQube for code quality analysis.");
|
|
381
|
+
const install = await askBool("Install SonarQube + PostgreSQL via Docker now", true);
|
|
382
|
+
if (!install) return false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
console.log("Starting SonarQube + PostgreSQL (first start pulls images, may take a few minutes)...");
|
|
386
|
+
const up = await run("docker", ["compose", "-f", composePath, "up", "-d"], { timeout: 300_000 });
|
|
387
|
+
if (up.exitCode !== 0) {
|
|
388
|
+
console.log("Failed to start SonarQube: " + (up.stderr || up.stdout));
|
|
389
|
+
console.log("Start manually: docker compose -f " + composePath + " up -d");
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Wait for SonarQube to be ready
|
|
394
|
+
console.log("Waiting for SonarQube to be ready...");
|
|
395
|
+
const maxWait = 120_000;
|
|
396
|
+
const start = Date.now();
|
|
397
|
+
while (Date.now() - start < maxWait) {
|
|
398
|
+
const check = await run("curl", ["-s", sonarUrl + "/api/system/status"], { timeout: 5_000 });
|
|
399
|
+
try {
|
|
400
|
+
const status = JSON.parse(check.stdout || "{}").status;
|
|
401
|
+
if (status === "UP") {
|
|
402
|
+
console.log("SonarQube ready at " + sonarUrl);
|
|
403
|
+
console.log("Default credentials: admin / admin (change on first login)");
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
} catch { /* not ready yet */ }
|
|
407
|
+
await new Promise((r) => setTimeout(r, 5_000));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
console.log("SonarQube is still starting. Check status at: " + sonarUrl);
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function setupChromeDevtoolsMcp() {
|
|
415
|
+
const hasClaude = await ensureCommand("claude", ["--version"]);
|
|
416
|
+
if (!hasClaude) return null;
|
|
417
|
+
|
|
418
|
+
const mcpList = await run("claude", ["mcp", "list"]);
|
|
419
|
+
if (mcpList.stdout.includes("chrome-devtools")) {
|
|
420
|
+
console.log("Chrome DevTools MCP already configured.");
|
|
421
|
+
return "already-configured";
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const res = await run("claude", ["mcp", "add", "chrome-devtools", "--scope", "user", "npx", "chrome-devtools-mcp@latest"]);
|
|
425
|
+
if (res.exitCode === 0) {
|
|
426
|
+
console.log("Chrome DevTools MCP configured.");
|
|
427
|
+
return "configured";
|
|
428
|
+
}
|
|
429
|
+
console.log("Failed to configure Chrome DevTools MCP. Manual install:");
|
|
430
|
+
console.log(" claude mcp add chrome-devtools --scope user npx chrome-devtools-mcp@latest");
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const SHELL_SOURCE_MARKER = "# karajan-code env";
|
|
435
|
+
|
|
436
|
+
async function addSourceToShellProfile(envPath) {
|
|
437
|
+
const home = os.homedir();
|
|
438
|
+
const profiles = [path.join(home, ".bashrc"), path.join(home, ".zshrc")];
|
|
439
|
+
const sourceLine = `${SHELL_SOURCE_MARKER}\n[ -f "${envPath}" ] && source "${envPath}"`;
|
|
440
|
+
const updated = [];
|
|
441
|
+
|
|
442
|
+
for (const profile of profiles) {
|
|
443
|
+
let content = "";
|
|
444
|
+
try {
|
|
445
|
+
content = await fs.readFile(profile, "utf8");
|
|
446
|
+
} catch {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (content.includes(SHELL_SOURCE_MARKER)) {
|
|
451
|
+
const lines = content.split("\n");
|
|
452
|
+
const filtered = [];
|
|
453
|
+
let skip = false;
|
|
454
|
+
for (const line of lines) {
|
|
455
|
+
if (line.trim() === SHELL_SOURCE_MARKER.trim()) {
|
|
456
|
+
skip = true;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (skip) {
|
|
460
|
+
skip = false;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
filtered.push(line);
|
|
464
|
+
}
|
|
465
|
+
content = filtered.join("\n");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const newContent = `${content.trimEnd()}\n\n${sourceLine}\n`;
|
|
469
|
+
await fs.writeFile(profile, newContent, "utf8");
|
|
470
|
+
updated.push(profile);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return updated;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function writeKarajanEnv({ kjHome, sonarToken, sonarHost }) {
|
|
477
|
+
const envPath = path.join(kjHome, "karajan.env");
|
|
478
|
+
const lines = [
|
|
479
|
+
`export KJ_HOME=\"${kjHome}\"`,
|
|
480
|
+
sonarToken ? `export KJ_SONAR_TOKEN=\"${sonarToken}\"` : "",
|
|
481
|
+
sonarHost ? `export KJ_SONAR_HOST=\"${sonarHost}\"` : ""
|
|
482
|
+
].filter(Boolean);
|
|
483
|
+
|
|
484
|
+
await fs.mkdir(kjHome, { recursive: true });
|
|
485
|
+
await fs.writeFile(envPath, `${lines.join("\n")}\n`, { mode: 0o600 });
|
|
486
|
+
return envPath;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function bootstrapKjConfig({ rootDir, kjHome, sonarToken, sonarHost, coder, reviewer, reviewerFallback }) {
|
|
490
|
+
const env = { ...process.env, KJ_HOME: kjHome };
|
|
491
|
+
if (sonarToken) env.KJ_SONAR_TOKEN = sonarToken;
|
|
492
|
+
|
|
493
|
+
const initRes = await run("node", [path.join(rootDir, "src", "cli.js"), "init"], {
|
|
494
|
+
cwd: rootDir,
|
|
495
|
+
env
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
if (initRes.exitCode !== 0) {
|
|
499
|
+
throw new Error(`kj init failed: ${initRes.stderr || initRes.stdout}`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const configPath = path.join(rootDir, "kj.config.yml");
|
|
503
|
+
let config = await fs.readFile(configPath, "utf8");
|
|
504
|
+
config = config.replace(/^coder:\s.*$/m, `coder: ${coder}`);
|
|
505
|
+
config = config.replace(/^reviewer:\s.*$/m, `reviewer: ${reviewer}`);
|
|
506
|
+
config = config.replace(/^\s*fallback_reviewer:\s.*$/m, ` fallback_reviewer: ${reviewerFallback}`);
|
|
507
|
+
config = config.replace(/^\s*max_iteration_minutes:\s.*$/m, " max_iteration_minutes: 5");
|
|
508
|
+
config = config.replace(/^\s*host:\s.*$/m, ` host: ${sonarHost}`);
|
|
509
|
+
config = config.replace(/^\s*token:\s.*$/m, " token: null");
|
|
510
|
+
await fs.writeFile(configPath, config, "utf8");
|
|
511
|
+
|
|
512
|
+
return configPath;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function resolveInstance(rootDir, parsedArgs, registry, nonInteractive) {
|
|
516
|
+
const existingNames = Object.keys(registry.instances || {});
|
|
517
|
+
|
|
518
|
+
if (nonInteractive) {
|
|
519
|
+
const instanceName = String(
|
|
520
|
+
pickSetting({ cli: parsedArgs, cliKey: "instanceName", envKey: "KJ_INSTALL_INSTANCE_NAME", fallback: "default" })
|
|
521
|
+
);
|
|
522
|
+
const action = String(
|
|
523
|
+
pickSetting({ cli: parsedArgs, cliKey: "instanceAction", envKey: "KJ_INSTALL_INSTANCE_ACTION", fallback: "" })
|
|
524
|
+
);
|
|
525
|
+
const exists = Boolean(registry.instances?.[instanceName]);
|
|
526
|
+
const resolvedAction = action || (exists ? "update" : "add");
|
|
527
|
+
|
|
528
|
+
if (!["add", "update", "replace"].includes(resolvedAction)) {
|
|
529
|
+
throw new Error("--instance-action must be one of: add, update, replace");
|
|
530
|
+
}
|
|
531
|
+
if (resolvedAction === "add" && exists) {
|
|
532
|
+
throw new Error(`Instance '${instanceName}' already exists. Use --instance-action update|replace`);
|
|
533
|
+
}
|
|
534
|
+
if ((resolvedAction === "update" || resolvedAction === "replace") && !exists) {
|
|
535
|
+
throw new Error(`Instance '${instanceName}' does not exist. Use --instance-action add`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const defaultKjHome = exists
|
|
539
|
+
? registry.instances[instanceName].kjHome
|
|
540
|
+
: path.join(rootDir, instanceName === "default" ? ".karajan" : `.karajan-${instanceName}`);
|
|
541
|
+
|
|
542
|
+
return { instanceName, action: resolvedAction, defaultKjHome, existing: registry.instances[instanceName] || null };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (existingNames.length === 0) {
|
|
546
|
+
const instanceName = await ask("Nombre de instancia", "default");
|
|
547
|
+
const defaultKjHome = path.join(rootDir, instanceName === "default" ? ".karajan" : `.karajan-${instanceName}`);
|
|
548
|
+
return { instanceName, action: "add", defaultKjHome, existing: null };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
console.log("\nInstancias detectadas:");
|
|
552
|
+
for (const name of existingNames) {
|
|
553
|
+
const item = registry.instances[name];
|
|
554
|
+
console.log(`- ${name} (KJ_HOME: ${item.kjHome})`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const action = await askChoice("\nSe detecta una instalacion previa. ¿Que quieres hacer?", [
|
|
558
|
+
{ value: "update", label: "actualizar (editar configuracion de una instancia existente)" },
|
|
559
|
+
{ value: "replace", label: "reemplazar (eliminar lo que hay y configurarlo todo de nuevo)" },
|
|
560
|
+
{ value: "add", label: "anadir nueva (crear otra instancia mas de KJ)" }
|
|
561
|
+
]);
|
|
562
|
+
|
|
563
|
+
if (action === "add") {
|
|
564
|
+
const suggested = `instance-${existingNames.length + 1}`;
|
|
565
|
+
const instanceName = await ask("Nombre de nueva instancia", suggested);
|
|
566
|
+
if (registry.instances[instanceName]) {
|
|
567
|
+
throw new Error(`La instancia '${instanceName}' ya existe.`);
|
|
568
|
+
}
|
|
569
|
+
const defaultKjHome = path.join(rootDir, `.karajan-${instanceName}`);
|
|
570
|
+
return { instanceName, action, defaultKjHome, existing: null };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const selected = await ask("Instancia objetivo", existingNames[0]);
|
|
574
|
+
if (!registry.instances[selected]) {
|
|
575
|
+
throw new Error(`No existe la instancia '${selected}'.`);
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
instanceName: selected,
|
|
579
|
+
action,
|
|
580
|
+
defaultKjHome: registry.instances[selected].kjHome,
|
|
581
|
+
existing: registry.instances[selected]
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function resolveRecoveryAction(parsedArgs, nonInteractive) {
|
|
586
|
+
const prev = await loadInstallState();
|
|
587
|
+
if (!prev || prev.status !== "in_progress") {
|
|
588
|
+
return { action: "none", previous: null };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (nonInteractive) {
|
|
592
|
+
const recoveryAction = String(
|
|
593
|
+
pickSetting({
|
|
594
|
+
cli: parsedArgs,
|
|
595
|
+
cliKey: "recoveryAction",
|
|
596
|
+
envKey: "KJ_INSTALL_RECOVERY_ACTION",
|
|
597
|
+
fallback: "continue"
|
|
598
|
+
})
|
|
599
|
+
);
|
|
600
|
+
if (!["continue", "restart"].includes(recoveryAction)) {
|
|
601
|
+
throw new Error("--recovery-action must be continue or restart");
|
|
602
|
+
}
|
|
603
|
+
return { action: recoveryAction, previous: prev };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const action = await askChoice(
|
|
607
|
+
`\nSe detecto una instalacion interrumpida (${prev.instanceName || "desconocida"}). ¿Que quieres hacer?`,
|
|
608
|
+
[
|
|
609
|
+
{ value: "continue", label: "continuar (retomar e intentar completar desde el estado actual)" },
|
|
610
|
+
{ value: "restart", label: "comenzar desde el principio (borrando lo generado para esa instancia)" }
|
|
611
|
+
]
|
|
612
|
+
);
|
|
613
|
+
return { action, previous: prev };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async function checkSelectedAgents(settings) {
|
|
617
|
+
const selected = new Set([settings.coder, settings.reviewer, settings.reviewerFallback].filter(Boolean));
|
|
618
|
+
const missing = [];
|
|
619
|
+
|
|
620
|
+
for (const agent of selected) {
|
|
621
|
+
const meta = AGENT_META[agent];
|
|
622
|
+
if (!meta) continue;
|
|
623
|
+
const ok = await ensureCommand(meta.bin);
|
|
624
|
+
if (!ok) {
|
|
625
|
+
missing.push({ agent, ...meta });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return missing;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function collectSettings(rootDir, parsedArgs, instanceContext, nonInteractive) {
|
|
633
|
+
const defaults = {
|
|
634
|
+
linkGlobal: nonInteractive ? false : true,
|
|
635
|
+
kjHome: instanceContext.defaultKjHome,
|
|
636
|
+
sonarHost: instanceContext.existing?.sonarHost || "http://localhost:9000",
|
|
637
|
+
createSonarToken: false,
|
|
638
|
+
sonarUser: "admin",
|
|
639
|
+
sonarPassword: "",
|
|
640
|
+
sonarTokenName: "karajan-cli",
|
|
641
|
+
sonarToken: "",
|
|
642
|
+
coder: instanceContext.existing?.coder || "codex",
|
|
643
|
+
reviewer: instanceContext.existing?.reviewer || "claude",
|
|
644
|
+
reviewerFallback: instanceContext.existing?.reviewerFallback || "codex",
|
|
645
|
+
setupMcpClaude: true,
|
|
646
|
+
setupMcpCodex: true,
|
|
647
|
+
runDoctor: true,
|
|
648
|
+
runTests: true
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
if (nonInteractive) {
|
|
652
|
+
return {
|
|
653
|
+
linkGlobal: parseBool(pickSetting({ cli: parsedArgs, cliKey: "linkGlobal", envKey: "KJ_INSTALL_LINK_GLOBAL", fallback: defaults.linkGlobal }), defaults.linkGlobal),
|
|
654
|
+
kjHome: pickSetting({ cli: parsedArgs, cliKey: "kjHome", envKey: "KJ_INSTALL_KJ_HOME", fallback: defaults.kjHome }),
|
|
655
|
+
sonarHost: pickSetting({ cli: parsedArgs, cliKey: "sonarHost", envKey: "KJ_INSTALL_SONAR_HOST", fallback: defaults.sonarHost }),
|
|
656
|
+
createSonarToken: parseBool(pickSetting({ cli: parsedArgs, cliKey: "generateSonarToken", envKey: "KJ_INSTALL_GENERATE_SONAR_TOKEN", fallback: defaults.createSonarToken }), defaults.createSonarToken),
|
|
657
|
+
sonarUser: pickSetting({ cli: parsedArgs, cliKey: "sonarUser", envKey: "KJ_INSTALL_SONAR_USER", fallback: defaults.sonarUser }),
|
|
658
|
+
sonarPassword: pickSetting({ cli: parsedArgs, cliKey: "sonarPassword", envKey: "KJ_INSTALL_SONAR_PASSWORD", fallback: defaults.sonarPassword }),
|
|
659
|
+
sonarTokenName: pickSetting({ cli: parsedArgs, cliKey: "sonarTokenName", envKey: "KJ_INSTALL_SONAR_TOKEN_NAME", fallback: defaults.sonarTokenName }),
|
|
660
|
+
sonarToken: pickSetting({ cli: parsedArgs, cliKey: "sonarToken", envKey: "KJ_SONAR_TOKEN", fallback: defaults.sonarToken }),
|
|
661
|
+
coder: pickSetting({ cli: parsedArgs, cliKey: "coder", envKey: "KJ_INSTALL_CODER", fallback: defaults.coder }),
|
|
662
|
+
reviewer: pickSetting({ cli: parsedArgs, cliKey: "reviewer", envKey: "KJ_INSTALL_REVIEWER", fallback: defaults.reviewer }),
|
|
663
|
+
reviewerFallback: pickSetting({ cli: parsedArgs, cliKey: "reviewerFallback", envKey: "KJ_INSTALL_REVIEWER_FALLBACK", fallback: defaults.reviewerFallback }),
|
|
664
|
+
setupMcpClaude: parseBool(pickSetting({ cli: parsedArgs, cliKey: "setupMcpClaude", envKey: "KJ_INSTALL_SETUP_MCP_CLAUDE", fallback: defaults.setupMcpClaude }), defaults.setupMcpClaude),
|
|
665
|
+
setupMcpCodex: parseBool(pickSetting({ cli: parsedArgs, cliKey: "setupMcpCodex", envKey: "KJ_INSTALL_SETUP_MCP_CODEX", fallback: defaults.setupMcpCodex }), defaults.setupMcpCodex),
|
|
666
|
+
setupChromeDevtools: parseBool(pickSetting({ cli: parsedArgs, cliKey: "setupChromeDevtools", envKey: "KJ_INSTALL_SETUP_CHROME_DEVTOOLS", fallback: true }), true),
|
|
667
|
+
runDoctor: parseBool(pickSetting({ cli: parsedArgs, cliKey: "runDoctor", envKey: "KJ_INSTALL_RUN_DOCTOR", fallback: defaults.runDoctor }), defaults.runDoctor),
|
|
668
|
+
runTests: parseBool(pickSetting({ cli: parsedArgs, cliKey: "runTests", envKey: "KJ_INSTALL_RUN_TESTS", fallback: defaults.runTests }), defaults.runTests)
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const linkGlobal = await askBool("Link karajan binaries globally with npm link", defaults.linkGlobal);
|
|
673
|
+
const kjHome = await ask("KJ_HOME directory", defaults.kjHome);
|
|
674
|
+
const sonarHost = await ask("SonarQube host", defaults.sonarHost);
|
|
675
|
+
|
|
676
|
+
// Smart SonarQube token flow
|
|
677
|
+
let createSonarToken = false;
|
|
678
|
+
let sonarUser = defaults.sonarUser;
|
|
679
|
+
let sonarPassword = "";
|
|
680
|
+
let sonarTokenName = defaults.sonarTokenName;
|
|
681
|
+
let sonarToken = "";
|
|
682
|
+
|
|
683
|
+
const existingToken = await readExistingSonarToken(kjHome);
|
|
684
|
+
|
|
685
|
+
if (existingToken) {
|
|
686
|
+
const keepSonar = await askBool("Keep current SonarQube token", true);
|
|
687
|
+
if (keepSonar) {
|
|
688
|
+
sonarToken = existingToken;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (!sonarToken) {
|
|
693
|
+
console.log("\n To create a token:");
|
|
694
|
+
console.log(" 1. Open " + sonarHost);
|
|
695
|
+
console.log(" 2. Log in (admin/admin on first access)");
|
|
696
|
+
console.log(" 3. Go to: My Account > Security > Generate Token");
|
|
697
|
+
console.log(" 4. Name: karajan-cli, Type: Global Analysis Token\n");
|
|
698
|
+
|
|
699
|
+
const tokenChoice = await askChoice("SonarQube token configuration:", [
|
|
700
|
+
{ value: "paste", label: "Enter a SonarQube token" },
|
|
701
|
+
{ value: "skip", label: "Skip for now (SonarQube won't work until configured)" }
|
|
702
|
+
]);
|
|
703
|
+
|
|
704
|
+
if (tokenChoice === "paste") {
|
|
705
|
+
sonarToken = await ask("Paste your SonarQube token");
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return {
|
|
710
|
+
linkGlobal,
|
|
711
|
+
kjHome,
|
|
712
|
+
sonarHost,
|
|
713
|
+
createSonarToken,
|
|
714
|
+
sonarUser,
|
|
715
|
+
sonarPassword,
|
|
716
|
+
sonarTokenName,
|
|
717
|
+
sonarToken,
|
|
718
|
+
coder: await ask("Default coder", defaults.coder),
|
|
719
|
+
reviewer: await ask("Default reviewer", defaults.reviewer),
|
|
720
|
+
reviewerFallback: await ask("Reviewer fallback", defaults.reviewerFallback),
|
|
721
|
+
setupMcpClaude: await askBool("Configure Claude MCP automatically", defaults.setupMcpClaude),
|
|
722
|
+
setupMcpCodex: await askBool("Configure Codex MCP automatically", defaults.setupMcpCodex),
|
|
723
|
+
setupChromeDevtools: await askBool("Configure Chrome DevTools MCP", true),
|
|
724
|
+
runDoctor: await askBool("Run kj doctor now", defaults.runDoctor),
|
|
725
|
+
runTests: await askBool("Run tests now", defaults.runTests)
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
async function main() {
|
|
730
|
+
const parsedArgs = parseArgs(process.argv.slice(2));
|
|
731
|
+
if (parsedArgs.help) {
|
|
732
|
+
printHelp();
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
printHeader();
|
|
737
|
+
|
|
738
|
+
const rootDir = process.cwd();
|
|
739
|
+
const nonInteractive = parseBool(
|
|
740
|
+
pickSetting({ cli: parsedArgs, cliKey: "nonInteractive", envKey: "KJ_INSTALL_NON_INTERACTIVE", fallback: false }),
|
|
741
|
+
false
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
console.log("Checking base requirements...");
|
|
745
|
+
const required = ["node", "npm", "git", "docker", "curl"];
|
|
746
|
+
for (const cmd of required) {
|
|
747
|
+
const ok = await ensureCommand(cmd);
|
|
748
|
+
if (!ok) throw new Error(`Missing required command: ${cmd}`);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
console.log("Installing dependencies...");
|
|
752
|
+
let res = await run("npm", ["install"], { cwd: rootDir });
|
|
753
|
+
if (res.exitCode !== 0) throw new Error(res.stderr || res.stdout || "npm install failed");
|
|
754
|
+
|
|
755
|
+
const recovery = await resolveRecoveryAction(parsedArgs, nonInteractive);
|
|
756
|
+
if (recovery.action === "restart" && recovery.previous?.kjHome) {
|
|
757
|
+
console.log(`Reiniciando instalacion previa. Limpiando ${recovery.previous.kjHome} ...`);
|
|
758
|
+
await fs.rm(recovery.previous.kjHome, { recursive: true, force: true });
|
|
759
|
+
await fs.rm(path.join(rootDir, "kj.config.yml"), { force: true });
|
|
760
|
+
await fs.rm(path.join(rootDir, "review-rules.md"), { force: true });
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const registry = await loadRegistry();
|
|
764
|
+
const instance = await resolveInstance(rootDir, parsedArgs, registry, nonInteractive);
|
|
765
|
+
|
|
766
|
+
// Ensure SonarQube Docker is running before asking for token
|
|
767
|
+
await ensureSonarQube(instance.defaultKjHome, nonInteractive);
|
|
768
|
+
|
|
769
|
+
const settings = await collectSettings(rootDir, parsedArgs, instance, nonInteractive);
|
|
770
|
+
|
|
771
|
+
await saveInstallState({
|
|
772
|
+
status: "in_progress",
|
|
773
|
+
startedAt: new Date().toISOString(),
|
|
774
|
+
rootDir,
|
|
775
|
+
instanceName: instance.instanceName,
|
|
776
|
+
action: instance.action,
|
|
777
|
+
kjHome: settings.kjHome
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
if (instance.action === "replace") {
|
|
781
|
+
console.log(`Reemplazando instancia '${instance.instanceName}'...`);
|
|
782
|
+
await fs.rm(settings.kjHome, { recursive: true, force: true });
|
|
783
|
+
await fs.rm(path.join(rootDir, "kj.config.yml"), { force: true });
|
|
784
|
+
await fs.rm(path.join(rootDir, "review-rules.md"), { force: true });
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const missingAgents = await checkSelectedAgents(settings);
|
|
788
|
+
if (missingAgents.length > 0) {
|
|
789
|
+
console.warn("\nWARNING: Faltan CLIs de IA seleccionados. La instalacion continua, pero kj fallara al ejecutar esos agentes.\n");
|
|
790
|
+
for (const item of missingAgents) {
|
|
791
|
+
console.warn(`- ${item.agent}: comando '${item.bin}' no encontrado`);
|
|
792
|
+
console.warn(` Instala aqui: ${item.installUrl}`);
|
|
793
|
+
}
|
|
794
|
+
console.warn("");
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (settings.linkGlobal) {
|
|
798
|
+
res = await run("npm", ["link"], { cwd: rootDir });
|
|
799
|
+
if (res.exitCode !== 0) throw new Error(res.stderr || res.stdout || "npm link failed");
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
let sonarToken = settings.sonarToken || "";
|
|
803
|
+
if (settings.createSonarToken) {
|
|
804
|
+
if (!settings.sonarPassword) {
|
|
805
|
+
if (nonInteractive) {
|
|
806
|
+
throw new Error("Non-interactive mode requires --sonar-password when --generate-sonar-token=true");
|
|
807
|
+
}
|
|
808
|
+
console.log("No Sonar password provided. Skipping automatic token generation.");
|
|
809
|
+
sonarToken = await ask("Paste existing KJ_SONAR_TOKEN (optional)", "");
|
|
810
|
+
} else {
|
|
811
|
+
sonarToken = await generateSonarToken({
|
|
812
|
+
host: settings.sonarHost,
|
|
813
|
+
user: settings.sonarUser,
|
|
814
|
+
password: settings.sonarPassword,
|
|
815
|
+
tokenName: settings.sonarTokenName
|
|
816
|
+
});
|
|
817
|
+
console.log("Sonar token generated.");
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const envPath = await writeKarajanEnv({ kjHome: settings.kjHome, sonarToken, sonarHost: settings.sonarHost });
|
|
822
|
+
const configPath = await bootstrapKjConfig({
|
|
823
|
+
rootDir,
|
|
824
|
+
kjHome: settings.kjHome,
|
|
825
|
+
sonarToken,
|
|
826
|
+
sonarHost: settings.sonarHost,
|
|
827
|
+
coder: settings.coder,
|
|
828
|
+
reviewer: settings.reviewer,
|
|
829
|
+
reviewerFallback: settings.reviewerFallback
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
let claudePath = "";
|
|
833
|
+
if (settings.setupMcpClaude) claudePath = await setupClaudeMcp({ rootDir, kjHome: settings.kjHome });
|
|
834
|
+
|
|
835
|
+
let codexPath = "";
|
|
836
|
+
if (settings.setupMcpCodex) codexPath = await setupCodexMcp({ rootDir, kjHome: settings.kjHome });
|
|
837
|
+
|
|
838
|
+
if (settings.setupChromeDevtools) await setupChromeDevtoolsMcp();
|
|
839
|
+
|
|
840
|
+
if (settings.runDoctor) {
|
|
841
|
+
const env = { ...process.env, KJ_HOME: settings.kjHome };
|
|
842
|
+
if (sonarToken) env.KJ_SONAR_TOKEN = sonarToken;
|
|
843
|
+
const doctor = await run("node", [path.join(rootDir, "src", "cli.js"), "doctor"], { env, cwd: rootDir });
|
|
844
|
+
console.log(doctor.stdout || doctor.stderr);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (settings.runTests) {
|
|
848
|
+
console.log("Running tests...");
|
|
849
|
+
const testRes = await run("npm", ["test"], { cwd: rootDir });
|
|
850
|
+
console.log(testRes.stdout || testRes.stderr);
|
|
851
|
+
if (testRes.exitCode !== 0) {
|
|
852
|
+
console.warn("WARNING: Some tests failed. Review the output above.");
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
registry.instances = registry.instances || {};
|
|
857
|
+
registry.instances[instance.instanceName] = {
|
|
858
|
+
name: instance.instanceName,
|
|
859
|
+
kjHome: settings.kjHome,
|
|
860
|
+
sonarHost: settings.sonarHost,
|
|
861
|
+
coder: settings.coder,
|
|
862
|
+
reviewer: settings.reviewer,
|
|
863
|
+
reviewerFallback: settings.reviewerFallback,
|
|
864
|
+
repoPath: rootDir,
|
|
865
|
+
updatedAt: new Date().toISOString()
|
|
866
|
+
};
|
|
867
|
+
await saveRegistry(registry);
|
|
868
|
+
await clearInstallState();
|
|
869
|
+
|
|
870
|
+
const shellProfiles = await addSourceToShellProfile(envPath);
|
|
871
|
+
|
|
872
|
+
console.log("\nSetup completed.\n");
|
|
873
|
+
console.log(`- Instance: ${instance.instanceName}`);
|
|
874
|
+
console.log(`- Action: ${instance.action}`);
|
|
875
|
+
console.log(`- Env file: ${envPath}`);
|
|
876
|
+
console.log(`- Project config: ${configPath}`);
|
|
877
|
+
if (claudePath) console.log(`- Claude MCP configured: ${claudePath}`);
|
|
878
|
+
if (codexPath) console.log(`- Codex MCP configured: ${codexPath}`);
|
|
879
|
+
console.log(`- Registry: ${REGISTRY_PATH}`);
|
|
880
|
+
if (shellProfiles.length > 0) {
|
|
881
|
+
console.log(`- Shell env auto-loaded in: ${shellProfiles.join(", ")}`);
|
|
882
|
+
console.log("\nReload your shell or run:");
|
|
883
|
+
console.log(` source ${envPath}`);
|
|
884
|
+
} else {
|
|
885
|
+
console.log("\nNo shell profile found. Add this to your shell profile manually:");
|
|
886
|
+
console.log(` source ${envPath}`);
|
|
887
|
+
}
|
|
888
|
+
console.log("\nThen you can ask either assistant to run tasks through MCP tools (kj_run, kj_scan, ...).\n");
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
main()
|
|
892
|
+
.catch((error) => {
|
|
893
|
+
console.error(`\nInstaller failed: ${error.message}`);
|
|
894
|
+
process.exit(1);
|
|
895
|
+
})
|
|
896
|
+
.finally(async () => {
|
|
897
|
+
await rl.close();
|
|
898
|
+
});
|