karajan-code 3.11.0 → 3.12.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "3.11.0",
3
+ "version": "3.12.0",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -226,7 +226,7 @@ export function registerPipeline(program, { pkgVersion }) {
226
226
 
227
227
  program
228
228
  .command("install-tools")
229
- .description("Install external audit tools (semgrep, osv-scanner, lighthouse, docker, sonar) using the package manager available on your system")
229
+ .description("Install the tools kj needs (git, agent CLI: claude+codex) plus the external audit tools (semgrep, osv-scanner, lighthouse, docker, sonar) using the package manager available on your system")
230
230
  .option("--only <tools>", "Comma-separated subset (e.g. \"semgrep,osv-scanner\"). Bypasses stack-gating.")
231
231
  .option("-y, --yes", "Auto-accept all prompts (non-interactive)")
232
232
  .option("--dry-run", "Show what would be installed without running anything")
@@ -18,7 +18,7 @@ import os from "node:os";
18
18
  import path from "node:path";
19
19
  import fs from "node:fs/promises";
20
20
  import { checkBinary } from "../utils/agent-detect.js";
21
- import { getInstallHint, detectPackageManagers, appliesToStack } from "../utils/install-hints.js";
21
+ import { getInstallHint, detectPackageManagers, appliesToStack, gitInstallPlan } from "../utils/install-hints.js";
22
22
  import { detectProjectStack } from "../utils/stack-detect.js";
23
23
  import { resolveStandalone } from "../utils/binary-sources.js";
24
24
  import { downloadBinary, binDir, runInstallCommand } from "../utils/tool-installer.js";
@@ -26,7 +26,17 @@ import { dockerInstallPlan } from "../utils/docker-install.js";
26
26
 
27
27
  const execFileAsync = promisify(execFile);
28
28
 
29
- const ALL_TOOLS = ["semgrep", "osv-scanner", "lighthouse", "docker", "sonar"];
29
+ // git and the agent CLI lead the list: both are `kj doctor` *required* tools,
30
+ // so a blank machine wants them before the optional audit tools.
31
+ const ALL_TOOLS = ["git", "agent-cli", "semgrep", "osv-scanner", "lighthouse", "docker", "sonar"];
32
+
33
+ // The default pipeline is coder=claude, reviewer=codex, so `agent-cli` installs
34
+ // exactly those two. gemini stays out of the default: it is a supported
35
+ // reviewer, not a required one. Both ship as global npm packages.
36
+ const AGENT_CLIS = [
37
+ { bin: "claude", pkg: "@anthropic-ai/claude-code", url: "https://docs.anthropic.com/en/docs/claude-code" },
38
+ { bin: "codex", pkg: "@openai/codex", url: "https://github.com/openai/codex" },
39
+ ];
30
40
 
31
41
  function parseOnlyList(raw) {
32
42
  if (!raw) return null;
@@ -56,11 +66,100 @@ async function isInstalled(tool) {
56
66
  // sonar is a Docker container, not a binary. We check it separately
57
67
  // by looking at running containers via `docker ps`.
58
68
  if (tool === "sonar") return checkSonarRunning();
69
+ // agent-cli is "installed" only when BOTH default-pipeline CLIs are present;
70
+ // a partial install (one of two) still routes to handleAgentCli.
71
+ if (tool === "agent-cli") {
72
+ const present = await Promise.all(AGENT_CLIS.map(async (c) => (await checkBinary(c.bin)).ok));
73
+ return present.every(Boolean);
74
+ }
59
75
  if (tool === "lighthouse") return (await checkBinary("lighthouse")).ok;
60
76
  if (tool === "docker") return (await checkBinary("docker")).ok;
61
77
  return (await checkBinary(tool)).ok;
62
78
  }
63
79
 
80
+ /**
81
+ * Agent-CLI handler. Installs whichever of the default-pipeline CLIs
82
+ * (coder=claude, reviewer=codex) are missing, via global npm. Opt-in with the
83
+ * exact command shown first. When npm itself is absent we surface the manual
84
+ * commands + URLs rather than failing silently.
85
+ */
86
+ async function handleAgentCli({ available, dryRun, yes, logger }) {
87
+ const missing = [];
88
+ for (const cli of AGENT_CLIS) {
89
+ if (!(await checkBinary(cli.bin)).ok) missing.push(cli);
90
+ }
91
+ if (missing.length === 0) return { tool: "agent-cli", action: "already-installed" };
92
+
93
+ if (!available.npm) {
94
+ const commands = missing.map((c) => `npm install -g ${c.pkg}`);
95
+ logger.warn?.(`✗ agent-cli: npm not found — install manually: ${commands.join(" ; ")}`);
96
+ return { tool: "agent-cli", action: "manual", reason: "npm not available", missing: missing.map((c) => c.bin), commands, manualUrls: missing.map((c) => c.url) };
97
+ }
98
+
99
+ const installed = [];
100
+ for (const cli of missing) {
101
+ const command = `npm install -g ${cli.pkg}`;
102
+ if (dryRun) {
103
+ logger.info?.(`▸ ${cli.bin}: would run \`${command}\``);
104
+ installed.push({ bin: cli.bin, action: "dry-run", command });
105
+ continue;
106
+ }
107
+ const proceed = yes || await promptYesNo(`Install ${cli.bin} with: ${command}?`);
108
+ if (!proceed) {
109
+ logger.info?.(`⊘ ${cli.bin}: declined`);
110
+ installed.push({ bin: cli.bin, action: "declined", command });
111
+ continue;
112
+ }
113
+ logger.info?.(`▸ ${cli.bin}: running \`${command}\`...`);
114
+ const r = await runInstallCommand(command, { interactive: false });
115
+ if (r.ok) { logger.info?.(`✓ ${cli.bin}: installed`); installed.push({ bin: cli.bin, action: "installed", command }); }
116
+ else { const err = r.error || `exit ${r.code}`; logger.warn?.(`✗ ${cli.bin}: install failed (${err})`); installed.push({ bin: cli.bin, action: "failed", command, error: err }); }
117
+ }
118
+
119
+ const action = dryRun ? "dry-run"
120
+ : installed.some((r) => r.action === "failed") ? "failed"
121
+ : installed.some((r) => r.action === "installed") ? "installed"
122
+ : "declined";
123
+ return { tool: "agent-cli", action, installed };
124
+ }
125
+
126
+ /**
127
+ * git handler. git installs through the OS package manager; on Linux that
128
+ * needs root, so those runs go through the interactive (tty-inherited) path
129
+ * where sudo prompts the user directly — kj never captures the password. The
130
+ * exact command is shown first and the install is opt-in, defaulting to NO for
131
+ * the privileged case. When no package manager matches, we surface the manual
132
+ * download URL instead of failing silently.
133
+ */
134
+ async function handleGit({ available, dryRun, yes, logger }) {
135
+ const plan = gitInstallPlan(available);
136
+ if (!plan.command) {
137
+ logger.warn?.(`✗ git: no supported package manager here — install manually: ${plan.manualUrl}`);
138
+ return { tool: "git", action: "manual", reason: "no supported package manager found", manualUrl: plan.manualUrl };
139
+ }
140
+ if (dryRun) {
141
+ logger.info?.(`▸ git: would run \`${plan.command}\` (manager: ${plan.manager}${plan.needsSudo ? ", needs sudo" : ""})`);
142
+ return { tool: "git", action: "dry-run", command: plan.command, manager: plan.manager };
143
+ }
144
+ if (plan.needsSudo) {
145
+ logger.warn?.("git: this step needs sudo — you'll be prompted for your password on your own terminal (kj never sees it).");
146
+ }
147
+ const proceed = yes || await promptYesNo(`Install git with: ${plan.command}?`, !plan.needsSudo);
148
+ if (!proceed) {
149
+ logger.info?.("⊘ git: declined");
150
+ return { tool: "git", action: "declined", command: plan.command };
151
+ }
152
+ logger.info?.(`▸ git: running \`${plan.command}\`...`);
153
+ const r = await runInstallCommand(plan.command, { interactive: plan.needsSudo });
154
+ if (r.ok) {
155
+ logger.info?.(`✓ git: installed via ${plan.manager}`);
156
+ return { tool: "git", action: "installed", manager: plan.manager };
157
+ }
158
+ const err = r.error || `exit ${r.code}`;
159
+ logger.warn?.(`✗ git: install failed (${err})`);
160
+ return { tool: "git", action: "failed", command: plan.command, error: err };
161
+ }
162
+
64
163
  async function checkSonarRunning() {
65
164
  try {
66
165
  const { stdout } = await execFileAsync("docker", ["ps", "--format", "{{.Names}}", "--filter", "name=sonarqube"], { timeout: 5000 });
@@ -236,6 +335,18 @@ export async function installToolsCommand(opts = {}) {
236
335
  continue;
237
336
  }
238
337
 
338
+ if (tool === "git") {
339
+ const result = await handleGit({ available, dryRun, yes, logger });
340
+ results.push(result);
341
+ continue;
342
+ }
343
+
344
+ if (tool === "agent-cli") {
345
+ const result = await handleAgentCli({ available, dryRun, yes, logger });
346
+ results.push(result);
347
+ continue;
348
+ }
349
+
239
350
  if (tool === "docker") {
240
351
  const result = await handleDocker({ available, dryRun, yes, logger });
241
352
  results.push(result);
@@ -116,8 +116,36 @@ const MANUAL_URLS = {
116
116
  "osv-scanner": "https://google.github.io/osv-scanner/installation/",
117
117
  lighthouse: "https://github.com/GoogleChrome/lighthouse#using-the-cli",
118
118
  docker: "https://docs.docker.com/get-docker/",
119
+ git: "https://git-scm.com/downloads",
119
120
  };
120
121
 
122
+ // git is a `kj doctor` *required* tool. Unlike the audit tools it is installed
123
+ // through the OS package manager, and on Linux that needs root — so the plan
124
+ // carries a `needsSudo` flag and the caller runs those through the interactive
125
+ // (tty-inherited) path, where sudo prompts the user directly and kj never sees
126
+ // the password. brew/scoop/choco install at user scope, no sudo.
127
+ const GIT_CANDIDATES = [
128
+ { manager: "brew", command: "brew install git", needsSudo: false },
129
+ { manager: "apt", command: "sudo apt-get install -y git", needsSudo: true },
130
+ { manager: "dnf", command: "sudo dnf install -y git", needsSudo: true },
131
+ { manager: "choco", command: "choco install git -y", needsSudo: false },
132
+ { manager: "scoop", command: "scoop install git", needsSudo: false },
133
+ ];
134
+
135
+ /**
136
+ * Pick a git install plan for the current machine. Returns a manual plan
137
+ * (command null + URL) when no supported package manager is present.
138
+ *
139
+ * @param {Record<PackageManager, boolean>} available
140
+ * @returns {{ command: string|null, manager: PackageManager|null, needsSudo: boolean, manualUrl: string }}
141
+ */
142
+ export function gitInstallPlan(available = {}) {
143
+ for (const { manager, command, needsSudo } of GIT_CANDIDATES) {
144
+ if (available[manager]) return { command, manager, needsSudo, manualUrl: MANUAL_URLS.git };
145
+ }
146
+ return { command: null, manager: null, needsSudo: false, manualUrl: MANUAL_URLS.git };
147
+ }
148
+
121
149
  /**
122
150
  * Tools whose hint depends on the project stack. lighthouse is only
123
151
  * relevant for frontend projects; suppressing the hint elsewhere keeps