karajan-code 3.11.0 → 3.12.1
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
package/src/cli/register-meta.js
CHANGED
|
@@ -304,26 +304,11 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
304
304
|
.command("update")
|
|
305
305
|
.description("Update karajan-code to the latest version from npm")
|
|
306
306
|
.action(async () => {
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
console.log("Checking for updates...");
|
|
313
|
-
try {
|
|
314
|
-
const { stdout } = await execa("npm", ["view", "karajan-code", "version"]);
|
|
315
|
-
const latest = stdout.trim();
|
|
316
|
-
if (latest === pkgVersion) {
|
|
317
|
-
console.log(`Already on the latest version (${pkgVersion}).`);
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
console.log(`Updating ${pkgVersion} → ${latest}...`);
|
|
321
|
-
await execa("npm", ["install", "-g", "karajan-code@latest"], { stdio: "inherit" });
|
|
322
|
-
console.log(`Updated to ${latest}. Restart Claude to pick up the new MCP server.`);
|
|
323
|
-
} catch (err) {
|
|
324
|
-
console.error(`Update failed: ${err.message}`);
|
|
325
|
-
process.exit(1);
|
|
326
|
-
}
|
|
307
|
+
// Output-capturing self-update: npm's deprecation/allow-scripts/funding
|
|
308
|
+
// warnings are hidden on success and only surfaced when the install fails.
|
|
309
|
+
const { performSelfUpdate } = await import("../utils/update-check.js");
|
|
310
|
+
const result = await performSelfUpdate({ currentVersion: pkgVersion });
|
|
311
|
+
if (!result.ok) process.exit(1);
|
|
327
312
|
});
|
|
328
313
|
|
|
329
314
|
program
|
|
@@ -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
|
|
@@ -103,6 +103,53 @@ export async function printUpdateNotice(currentVersion) {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Run the npm-channel self-update (`kj update`). Captures npm's output instead
|
|
108
|
+
* of streaming it: a successful update shows only the progress + result line,
|
|
109
|
+
* so npm's deprecation / allow-scripts / funding noise — build plumbing, not
|
|
110
|
+
* actionable for whoever runs the command — never reaches the user. On failure
|
|
111
|
+
* the captured stdout/stderr IS surfaced, so real errors (native build,
|
|
112
|
+
* permissions) stay diagnosable — never a silent failure.
|
|
113
|
+
*
|
|
114
|
+
* @param {Object} opts
|
|
115
|
+
* @param {string} opts.currentVersion - version of the running kj
|
|
116
|
+
* @param {(cmd: string, args: string[]) => Promise<{stdout: string, stderr: string}>} [opts.exec] - injectable runner (defaults to execa)
|
|
117
|
+
* @param {Console} [opts.logger]
|
|
118
|
+
* @returns {Promise<{ ok: boolean, alreadyLatest?: boolean, latest?: string }>}
|
|
119
|
+
*/
|
|
120
|
+
export async function performSelfUpdate({ currentVersion, exec, logger = console } = {}) {
|
|
121
|
+
const run = exec || (async (cmd, args) => (await import("execa")).execa(cmd, args));
|
|
122
|
+
logger.log(`Current version: ${currentVersion}`);
|
|
123
|
+
logger.log("Checking for updates...");
|
|
124
|
+
|
|
125
|
+
let latest;
|
|
126
|
+
try {
|
|
127
|
+
const { stdout } = await run("npm", ["view", PACKAGE_NAME, "version"]);
|
|
128
|
+
latest = stdout.trim();
|
|
129
|
+
} catch (err) {
|
|
130
|
+
logger.error(`Update failed: ${err.shortMessage || err.message}`);
|
|
131
|
+
return { ok: false };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (latest === currentVersion) {
|
|
135
|
+
logger.log(`Already on the latest version (${currentVersion}).`);
|
|
136
|
+
return { ok: true, alreadyLatest: true, latest };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
logger.log(`Updating ${currentVersion} → ${latest}... (this can take a few minutes)`);
|
|
140
|
+
try {
|
|
141
|
+
// No stdio:inherit — capture and drop npm's warnings on the success path.
|
|
142
|
+
await run("npm", ["install", "-g", `${PACKAGE_NAME}@latest`]);
|
|
143
|
+
logger.log(`Updated to ${latest}. Restart Claude to pick up the new MCP server.`);
|
|
144
|
+
return { ok: true, latest };
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if (err.stdout) logger.error(err.stdout);
|
|
147
|
+
if (err.stderr) logger.error(err.stderr);
|
|
148
|
+
logger.error(`Update failed: ${err.shortMessage || err.message}`);
|
|
149
|
+
return { ok: false };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
106
153
|
/** Simple semver compare: returns >0 if a > b, <0 if a < b, 0 if equal */
|
|
107
154
|
function compareVersions(a, b) {
|
|
108
155
|
const pa = a.split(".").map(Number);
|