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
|
@@ -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
|
-
|
|
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
|