karajan-code 3.12.2 → 3.12.3
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/README.md +1 -1
- package/package.json +1 -1
- package/scripts/build-sea.mjs +23 -1
- package/scripts/esbuild-sea.config.mjs +22 -1
- package/src/audit/osv-findings.js +1 -1
- package/src/commands/init.js +4 -3
- package/src/commands/onboard.js +2 -1
- package/src/orchestrator/config-init.js +2 -1
- package/src/prompts/prompt-resolver.js +2 -9
- package/src/utils/install-hints.js +5 -1
- package/src/utils/templates-root.js +61 -0
- package/src/utils/update-check.js +78 -15
package/README.md
CHANGED
|
@@ -190,7 +190,7 @@ None of these are required. Karajan auto-skips any scanner that isn't installed,
|
|
|
190
190
|
| Tool | Scope | Install | Installable from `kj init`? | What you get |
|
|
191
191
|
|------|-------|---------|-----------------------------|--------------|
|
|
192
192
|
| **SonarQube** | Any stack | `docker compose -f ~/sonarqube/docker-compose.yml up -d` | ✅ Yes (wizard configures Docker container + token) | Code quality + security rules with line-precision in `kj audit` |
|
|
193
|
-
| **OSV-Scanner** | Any stack | `go install github.com/google/osv-scanner@latest` | ❌ No (install manually) | Dependency CVE coverage broader than `npm audit` |
|
|
193
|
+
| **OSV-Scanner** | Any stack | `go install github.com/google/osv-scanner/v2/cmd/osv-scanner@latest` | ❌ No (install manually) | Dependency CVE coverage broader than `npm audit` |
|
|
194
194
|
| **Semgrep** | Any stack | `pipx install semgrep` | ❌ No (install manually) | SAST: XSS, SQLi, taint flow, secrets — equivalent to `snyk code`, free for OSS |
|
|
195
195
|
| **Lighthouse** | **Frontend only** | `npm install -g lighthouse` | ❌ No (install manually) | Core Web Vitals + opportunities for `kj webperf` (auto-feeds `kj audit`) |
|
|
196
196
|
|
package/package.json
CHANGED
package/scripts/build-sea.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { execSync } from "node:child_process";
|
|
22
|
-
import { cpSync, mkdirSync, writeFileSync, statSync } from "node:fs";
|
|
22
|
+
import { cpSync, mkdirSync, writeFileSync, statSync, readFileSync } from "node:fs";
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
25
|
|
|
@@ -59,10 +59,32 @@ async function main() {
|
|
|
59
59
|
|
|
60
60
|
// ── Step 2: Generate SEA blob ───────────────────────────────────
|
|
61
61
|
console.log("[2/5] Generating SEA preparation blob...");
|
|
62
|
+
// Templates travel as SEA assets (KJC-BUG-0104): the binary ships no
|
|
63
|
+
// templates/ directory, so kj init / role prompts / onboard resolved
|
|
64
|
+
// paths like $HOME/templates/skills and hit ENOENT. The runtime
|
|
65
|
+
// (src/utils/templates-root.js) reads the index asset and extracts
|
|
66
|
+
// the files to ~/.karajan/embedded-templates/<version>/ on first use.
|
|
67
|
+
const { collectTemplateFiles } = await import("./esbuild-sea.config.mjs");
|
|
68
|
+
const pkgVersion = JSON.parse(
|
|
69
|
+
readFileSync(path.join(ROOT, "package.json"), "utf8"),
|
|
70
|
+
).version;
|
|
71
|
+
const templateFiles = collectTemplateFiles();
|
|
72
|
+
writeFileSync(
|
|
73
|
+
path.join(DIST, "templates-index.json"),
|
|
74
|
+
JSON.stringify({ version: pkgVersion, files: templateFiles }),
|
|
75
|
+
);
|
|
76
|
+
// Absolute paths: node resolves asset paths against the CWD (not the
|
|
77
|
+
// config file), so anything relative is ambiguous.
|
|
78
|
+
const assets = { "templates-index.json": path.join(DIST, "templates-index.json") };
|
|
79
|
+
for (const rel of templateFiles) {
|
|
80
|
+
assets[`templates/${rel}`] = path.join(ROOT, "templates", rel);
|
|
81
|
+
}
|
|
82
|
+
console.log(` -> ${templateFiles.length} template assets embedded.`);
|
|
62
83
|
const seaConfig = {
|
|
63
84
|
main: "dist/kj-bundle.cjs",
|
|
64
85
|
output: "dist/sea-prep.blob",
|
|
65
86
|
disableExperimentalSEAWarning: true,
|
|
87
|
+
assets,
|
|
66
88
|
};
|
|
67
89
|
writeFileSync(
|
|
68
90
|
path.join(DIST, "sea-config.json"),
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import fs from "node:fs/promises";
|
|
13
|
-
import { readFileSync } from "node:fs";
|
|
13
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
|
|
16
16
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -213,6 +213,27 @@ const auditHistoryStubPlugin = {
|
|
|
213
213
|
};
|
|
214
214
|
|
|
215
215
|
/** @type {import('esbuild').BuildOptions} */
|
|
216
|
+
/**
|
|
217
|
+
* List every built-in template file, as paths relative to templates/
|
|
218
|
+
* (KJC-BUG-0104). The SEA binary ships them as SEA assets — build-sea.mjs
|
|
219
|
+
* turns this list into the sea-config `assets` map plus an index the
|
|
220
|
+
* runtime (src/utils/templates-root.js) uses to extract them on first use.
|
|
221
|
+
* Exported (instead of living in build-sea.mjs, which runs main() on
|
|
222
|
+
* import) so tests can exercise it.
|
|
223
|
+
*/
|
|
224
|
+
export function collectTemplateFiles(rootDir = path.join(ROOT, "templates")) {
|
|
225
|
+
const files = [];
|
|
226
|
+
const walk = (dir) => {
|
|
227
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
228
|
+
const p = path.join(dir, entry.name);
|
|
229
|
+
if (entry.isDirectory()) walk(p);
|
|
230
|
+
else files.push(path.relative(rootDir, p));
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
walk(rootDir);
|
|
234
|
+
return files.sort();
|
|
235
|
+
}
|
|
236
|
+
|
|
216
237
|
export const seaBuildOptions = {
|
|
217
238
|
entryPoints: ["src/cli.js"],
|
|
218
239
|
outfile: "dist/kj-bundle.cjs",
|
|
@@ -48,7 +48,7 @@ export async function collectOsvFindings(projectDir, logger = null) {
|
|
|
48
48
|
// err.code 1 is NORMAL — we still want the stdout. Real failures are
|
|
49
49
|
// ENOENT (binary missing) or stdout empty.
|
|
50
50
|
if (err.code === "ENOENT") {
|
|
51
|
-
if (logger?.warn) logger.warn("osv-scanner not found in PATH — install via 'go install github.com/google/osv-scanner@latest'");
|
|
51
|
+
if (logger?.warn) logger.warn("osv-scanner not found in PATH — install via 'go install github.com/google/osv-scanner/v2/cmd/osv-scanner@latest'");
|
|
52
52
|
return { available: false, reason: "osv-scanner not installed" };
|
|
53
53
|
}
|
|
54
54
|
if (err.killed) {
|
package/src/commands/init.js
CHANGED
|
@@ -7,6 +7,7 @@ import { ollamaUp, waitForOllamaReady, normalizeOllamaConfig } from "../rag/olla
|
|
|
7
7
|
import { checkOllamaCapability, pullOllamaModel } from "../rag/ollama-capability.js";
|
|
8
8
|
import { exists, ensureDir } from "../utils/fs.js";
|
|
9
9
|
import { getKarajanHome } from "../utils/paths.js";
|
|
10
|
+
import { getTemplatesRoot } from "../utils/templates-root.js";
|
|
10
11
|
import { detectAvailableAgents } from "../utils/agent-detect.js";
|
|
11
12
|
import { createWizard, isTTY } from "../utils/wizard.js";
|
|
12
13
|
import { runCommand } from "../utils/process.js";
|
|
@@ -441,7 +442,7 @@ async function ensureReviewRules(reviewRulesPath, logger) {
|
|
|
441
442
|
|
|
442
443
|
async function ensureCoderRules(coderRulesPath, logger) {
|
|
443
444
|
if (await exists(coderRulesPath)) return;
|
|
444
|
-
const templatePath = path.
|
|
445
|
+
const templatePath = path.join(getTemplatesRoot(), "coder-rules.md");
|
|
445
446
|
let content;
|
|
446
447
|
try {
|
|
447
448
|
content = await fs.readFile(templatePath, "utf8");
|
|
@@ -537,7 +538,7 @@ async function scaffoldCiGateway(config, flags, logger) {
|
|
|
537
538
|
const workflowDir = path.join(projectDir, ".github", "workflows");
|
|
538
539
|
await ensureDir(workflowDir);
|
|
539
540
|
|
|
540
|
-
const templatesDir = path.
|
|
541
|
+
const templatesDir = path.join(getTemplatesRoot(), "workflows");
|
|
541
542
|
const workflows = ["kj-ci-gateway.yml", "automerge.yml", "houston-override.yml"];
|
|
542
543
|
|
|
543
544
|
for (const wf of workflows) {
|
|
@@ -611,7 +612,7 @@ async function bootstrapOllama({ flags, config, logger, interactive }) {
|
|
|
611
612
|
async function installSkills(logger, interactive) {
|
|
612
613
|
const projectDir = process.cwd();
|
|
613
614
|
const commandsDir = path.join(projectDir, ".claude", "commands");
|
|
614
|
-
const skillsTemplateDir = path.
|
|
615
|
+
const skillsTemplateDir = path.join(getTemplatesRoot(), "skills");
|
|
615
616
|
|
|
616
617
|
let doInstall = true;
|
|
617
618
|
if (interactive) {
|
package/src/commands/onboard.js
CHANGED
|
@@ -6,6 +6,7 @@ import { collectAll } from "../onboarder/collectors/index.js";
|
|
|
6
6
|
import { OnboarderRole } from "../roles/onboarder-role.js";
|
|
7
7
|
import { resolveRole } from "../config.js";
|
|
8
8
|
import { getKarajanHome } from "../utils/paths.js";
|
|
9
|
+
import { getTemplatesRoot } from "../utils/templates-root.js";
|
|
9
10
|
|
|
10
11
|
export function briefPath(projectDir) {
|
|
11
12
|
const slug = path.basename(projectDir).replace(/[^a-zA-Z0-9._-]/g, "-").toLowerCase() || "project";
|
|
@@ -28,7 +29,7 @@ export async function onboardCommand({ config, logger, flags = {} }) {
|
|
|
28
29
|
return { path: out, bundle, brief: null };
|
|
29
30
|
}
|
|
30
31
|
const roleCfg = resolveRole(config, "onboarder") || resolveRole(config, "architect");
|
|
31
|
-
const templatePath =
|
|
32
|
+
const templatePath = path.join(getTemplatesRoot(), "roles", "onboarder.md");
|
|
32
33
|
const instructions = existsSync(templatePath) ? await readFile(templatePath, "utf8") : "";
|
|
33
34
|
const role = new OnboarderRole({ instructions, config, ...roleCfg });
|
|
34
35
|
// KJC-BUG-0061 Bug B: BaseRole.run() refuses to execute unless init()
|
|
@@ -10,6 +10,7 @@ import { buildCoderPrompt } from "../prompts/coder.js";
|
|
|
10
10
|
import { buildReviewerPrompt } from "../prompts/reviewer.js";
|
|
11
11
|
import { resolveRole } from "../config.js";
|
|
12
12
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
13
|
+
import { getTemplatesRoot } from "../utils/templates-root.js";
|
|
13
14
|
import { BudgetTracker, extractUsageMetrics } from "../utils/budget.js";
|
|
14
15
|
import { computeKjComparison } from "../budget/comparison.js";
|
|
15
16
|
import { resolveRoleMdPath, loadFirstExisting } from "../roles/base-role.js";
|
|
@@ -76,7 +77,7 @@ export async function autoInit(projectDir, logger) {
|
|
|
76
77
|
logger.info("No .karajan/ found — auto-initializing project scaffolding");
|
|
77
78
|
await ensureDir(karajanDir);
|
|
78
79
|
|
|
79
|
-
const templatesDir =
|
|
80
|
+
const templatesDir = getTemplatesRoot();
|
|
80
81
|
|
|
81
82
|
const filesToCopy = [
|
|
82
83
|
{ src: "coder-rules.md", dest: "coder-rules.md" },
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
|
|
27
27
|
import fs from "node:fs/promises";
|
|
28
28
|
import path from "node:path";
|
|
29
|
-
import { fileURLToPath } from "node:url";
|
|
30
29
|
import { getKarajanHome } from "../utils/paths.js";
|
|
30
|
+
import { getTemplatesRoot } from "../utils/templates-root.js";
|
|
31
31
|
import { normalizeProvider } from "../utils/provider-env.js";
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -50,14 +50,7 @@ export function resolveRolePromptPaths(role, provider, projectDir) {
|
|
|
50
50
|
if (projectDir) homes.push(path.join(projectDir, ".karajan", "roles"));
|
|
51
51
|
homes.push(path.join(getKarajanHome(), "roles"));
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
55
|
-
"..",
|
|
56
|
-
"..",
|
|
57
|
-
"templates",
|
|
58
|
-
"roles"
|
|
59
|
-
);
|
|
60
|
-
homes.push(builtInRoot);
|
|
53
|
+
homes.push(path.join(getTemplatesRoot(), "roles"));
|
|
61
54
|
|
|
62
55
|
const perProvider = [];
|
|
63
56
|
const defaults = [];
|
|
@@ -97,7 +97,11 @@ const INSTALL_CANDIDATES = {
|
|
|
97
97
|
{ manager: "pip", command: "pip install semgrep" },
|
|
98
98
|
],
|
|
99
99
|
"osv-scanner": [
|
|
100
|
-
|
|
100
|
+
// The module root has no main package — the binary lives in the cmd/
|
|
101
|
+
// subpackage, and since v2 the module path carries the /v2 suffix.
|
|
102
|
+
// `go install github.com/google/osv-scanner@latest` resolves the module
|
|
103
|
+
// (v1.x) but fails with "does not contain package" (KJC-BUG-0105).
|
|
104
|
+
{ manager: "go", command: "go install github.com/google/osv-scanner/v2/cmd/osv-scanner@latest" },
|
|
101
105
|
{ manager: "brew", command: "brew install osv-scanner" },
|
|
102
106
|
],
|
|
103
107
|
lighthouse: [
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { isSea, getAsset } from "node:sea";
|
|
5
|
+
import { getKarajanHome } from "./paths.js";
|
|
6
|
+
|
|
7
|
+
// npm install / source tree: the real templates/ directory next to src/.
|
|
8
|
+
const SOURCE_ROOT = path.resolve(
|
|
9
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
10
|
+
"..",
|
|
11
|
+
"..",
|
|
12
|
+
"templates",
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const INDEX_ASSET = "templates-index.json";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Root directory of the built-in templates (KJC-BUG-0104).
|
|
19
|
+
*
|
|
20
|
+
* The SEA binary ships only the JS bundle — templates/ never travels as
|
|
21
|
+
* files, and `import.meta.dirname`-relative resolution lands on the
|
|
22
|
+
* binary's own directory (e.g. `$HOME/templates/skills` → ENOENT). In SEA
|
|
23
|
+
* builds the templates travel as SEA assets (see scripts/build-sea.mjs);
|
|
24
|
+
* they are extracted once per version into ~/.karajan/embedded-templates/
|
|
25
|
+
* and served from there, so every fs-based consumer keeps working.
|
|
26
|
+
*/
|
|
27
|
+
export function getTemplatesRoot() {
|
|
28
|
+
if (!isSeaRuntime()) return SOURCE_ROOT;
|
|
29
|
+
const index = JSON.parse(getAsset(INDEX_ASSET, "utf8"));
|
|
30
|
+
const destRoot = path.join(getKarajanHome(), "embedded-templates", index.version);
|
|
31
|
+
return extractEmbeddedTemplates({
|
|
32
|
+
destRoot,
|
|
33
|
+
files: index.files,
|
|
34
|
+
readAsset: (rel) => getAsset(`templates/${rel}`, "utf8"),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isSeaRuntime() {
|
|
39
|
+
try {
|
|
40
|
+
return isSea();
|
|
41
|
+
} catch {
|
|
42
|
+
// isSea() unexpectedly threw ⇒ treat as npm / source tree, never block.
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract the embedded template files into destRoot (idempotent: a
|
|
49
|
+
* .complete marker skips re-extraction). Exported for unit testing.
|
|
50
|
+
*/
|
|
51
|
+
export function extractEmbeddedTemplates({ destRoot, files, readAsset }) {
|
|
52
|
+
const marker = path.join(destRoot, ".complete");
|
|
53
|
+
if (fs.existsSync(marker)) return destRoot;
|
|
54
|
+
for (const rel of files) {
|
|
55
|
+
const dest = path.join(destRoot, rel);
|
|
56
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
57
|
+
fs.writeFileSync(dest, readAsset(rel));
|
|
58
|
+
}
|
|
59
|
+
fs.writeFileSync(marker, "");
|
|
60
|
+
return destRoot;
|
|
61
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { isSea } from "node:sea";
|
|
4
5
|
import { getKarajanHome } from "./paths.js";
|
|
@@ -104,30 +105,46 @@ export async function printUpdateNotice(currentVersion) {
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
/**
|
|
107
|
-
* Run the
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* the
|
|
112
|
-
*
|
|
108
|
+
* Run the self-update (`kj update`), channel-aware (KJC-BUG-0106).
|
|
109
|
+
*
|
|
110
|
+
* npm channel: `npm install -g` with CAPTURED output — success shows only
|
|
111
|
+
* progress + result; npm's deprecation / allow-scripts / funding noise never
|
|
112
|
+
* reaches the user. On failure the captured output IS surfaced.
|
|
113
|
+
*
|
|
114
|
+
* sea channel (standalone binary): npm-installing would update a copy the
|
|
115
|
+
* PATH never resolves — the running binary stays old while `kj update`
|
|
116
|
+
* reports success. Instead the binary installer is re-run (downloaded to a
|
|
117
|
+
* temp file first, never piped from the network straight into a shell).
|
|
118
|
+
*
|
|
119
|
+
* Both channels end with a PATH probe: `kj --version` must report the new
|
|
120
|
+
* version, otherwise a stale copy shadows the update and the command fails
|
|
121
|
+
* LOUDLY naming the likely cause — never a silent "updated" that isn't.
|
|
113
122
|
*
|
|
114
123
|
* @param {Object} opts
|
|
115
124
|
* @param {string} opts.currentVersion - version of the running kj
|
|
116
125
|
* @param {(cmd: string, args: string[]) => Promise<{stdout: string, stderr: string}>} [opts.exec] - injectable runner (defaults to execa)
|
|
117
126
|
* @param {Console} [opts.logger]
|
|
118
|
-
* @
|
|
127
|
+
* @param {"npm"|"sea"} [opts.channel] - injectable channel (defaults to detectInstallChannel())
|
|
128
|
+
* @param {typeof fetch} [opts.fetchFn] - injectable fetch (registry lookup + installer download)
|
|
129
|
+
* @returns {Promise<{ ok: boolean, alreadyLatest?: boolean, latest?: string, manual?: boolean }>}
|
|
119
130
|
*/
|
|
120
|
-
export async function performSelfUpdate({ currentVersion, exec, logger = console } = {}) {
|
|
131
|
+
export async function performSelfUpdate({ currentVersion, exec, logger = console, channel, fetchFn = fetch } = {}) {
|
|
121
132
|
const run = exec || (async (cmd, args) => (await import("execa")).execa(cmd, args));
|
|
133
|
+
const ch = channel || detectInstallChannel();
|
|
122
134
|
logger.log(`Current version: ${currentVersion}`);
|
|
123
135
|
logger.log("Checking for updates...");
|
|
124
136
|
|
|
125
137
|
let latest;
|
|
126
138
|
try {
|
|
127
|
-
|
|
128
|
-
|
|
139
|
+
// Registry over HTTP, not `npm view` — standalone-binary machines may
|
|
140
|
+
// not have npm at all.
|
|
141
|
+
const res = await fetchFn(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
142
|
+
headers: { Accept: "application/json" },
|
|
143
|
+
});
|
|
144
|
+
if (!res.ok) throw new Error(`registry responded ${res.status}`);
|
|
145
|
+
latest = (await res.json()).version;
|
|
129
146
|
} catch (err) {
|
|
130
|
-
logger.error(`Update failed: ${err.
|
|
147
|
+
logger.error(`Update failed: could not check the registry (${err.message})`);
|
|
131
148
|
return { ok: false };
|
|
132
149
|
}
|
|
133
150
|
|
|
@@ -136,18 +153,64 @@ export async function performSelfUpdate({ currentVersion, exec, logger = console
|
|
|
136
153
|
return { ok: true, alreadyLatest: true, latest };
|
|
137
154
|
}
|
|
138
155
|
|
|
156
|
+
if (ch === "sea" && process.platform === "win32") {
|
|
157
|
+
// Executing a downloaded .ps1 from inside the binary is not supported
|
|
158
|
+
// yet — hand the user the exact command instead of half-updating.
|
|
159
|
+
logger.log(`Update available: ${currentVersion} → ${latest} (standalone binary).`);
|
|
160
|
+
logger.log(`${updateInstruction({ channel: "sea" })}`);
|
|
161
|
+
return { ok: true, latest, manual: true };
|
|
162
|
+
}
|
|
163
|
+
|
|
139
164
|
logger.log(`Updating ${currentVersion} → ${latest}... (this can take a few minutes)`);
|
|
140
165
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
166
|
+
if (ch === "sea") {
|
|
167
|
+
// Re-run the binary installer: download to a temp file, then execute —
|
|
168
|
+
// never pipe the network straight into a shell.
|
|
169
|
+
const res = await fetchFn(INSTALL_SH_URL);
|
|
170
|
+
if (!res.ok) throw new Error(`installer download failed (${res.status})`);
|
|
171
|
+
const tmpScript = path.join(os.tmpdir(), `kj-install-${process.pid}.sh`);
|
|
172
|
+
await fs.writeFile(tmpScript, await res.text(), { mode: 0o700 });
|
|
173
|
+
try {
|
|
174
|
+
await run("sh", [tmpScript]);
|
|
175
|
+
} finally {
|
|
176
|
+
await fs.rm(tmpScript, { force: true });
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
// No stdio:inherit — capture and drop npm's warnings on the success path.
|
|
180
|
+
await run("npm", ["install", "-g", `${PACKAGE_NAME}@latest`]);
|
|
181
|
+
}
|
|
145
182
|
} catch (err) {
|
|
146
183
|
if (err.stdout) logger.error(err.stdout);
|
|
147
184
|
if (err.stderr) logger.error(err.stderr);
|
|
148
185
|
logger.error(`Update failed: ${err.shortMessage || err.message}`);
|
|
149
186
|
return { ok: false };
|
|
150
187
|
}
|
|
188
|
+
|
|
189
|
+
// The install succeeded — but is the kj the USER runs actually the new one?
|
|
190
|
+
const onPath = await verifyKjOnPath({ run, latest, logger });
|
|
191
|
+
if (!onPath) return { ok: false };
|
|
192
|
+
logger.log(`Updated to ${latest}. Restart Claude to pick up the new MCP server.`);
|
|
193
|
+
return { ok: true, latest };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Probe `kj --version` through the PATH and confirm it reports `latest`.
|
|
198
|
+
* A mismatch means a stale copy shadows the freshly installed one (e.g. a
|
|
199
|
+
* standalone binary in ~/.local/bin in front of the npm global prefix).
|
|
200
|
+
*/
|
|
201
|
+
async function verifyKjOnPath({ run, latest, logger }) {
|
|
202
|
+
let reported;
|
|
203
|
+
try {
|
|
204
|
+
const { stdout } = await run("kj", ["--version"]);
|
|
205
|
+
reported = String(stdout).trim();
|
|
206
|
+
} catch (err) {
|
|
207
|
+
logger.error(`Update installed ${latest}, but running \`kj --version\` failed: ${err.shortMessage || err.message}`);
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
if (reported.includes(latest)) return true;
|
|
211
|
+
logger.error(`Update installed ${latest}, but the \`kj\` on your PATH still reports ${reported}.`);
|
|
212
|
+
logger.error("A stale copy is shadowing the update. Check `which -a kj` and remove the old one, or update through the channel that owns the first entry.");
|
|
213
|
+
return false;
|
|
151
214
|
}
|
|
152
215
|
|
|
153
216
|
/** Simple semver compare: returns >0 if a > b, <0 if a < b, 0 if equal */
|