akm-cli
Version:
akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur
602 lines (601 loc) • 26.1 kB
JavaScript
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
import * as childProcess from "node:child_process";
import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { fetchWithRetry, IS_WINDOWS, ResponseTooLargeError, readBodyWithByteCap, readChunkWithDeadline, } from "../../core/common.js";
import { ConfigError } from "../../core/errors.js";
import { warn } from "../../core/warn.js";
import { githubHeaders } from "../../integrations/github.js";
import { getDirname, mainPath, semverOrder } from "../../runtime.js";
import { resolveNpmGlobalRoot } from "../../tasks/resolve-akm-bin.js";
import { runMigrationTool } from "../migration-tool.js";
const REPO = "itlackey/akm";
const DEFAULT_PACKAGE_NAME = "akm-cli";
const NODE_MODULES_SEGMENT = "/node_modules/";
const BUN_GLOBAL_INSTALL_PATTERN = /(^|\/)\.bun\/(?:[^/]+\/)+node_modules\//;
const PNPM_GLOBAL_INSTALL_PATTERN = /(^|\/)(?:pnpm\/global|\.pnpm-global)(?:\/\d+)?\/node_modules\//;
const MAX_BINARY_DOWNLOAD_BYTES = 256 * 1024 * 1024;
const MAX_CHECKSUM_METADATA_BYTES = 1024 * 1024;
/**
* Bounds on the binary body read. `fetchWithTimeout`'s timer only covers
* time-to-HEADERS — it is cleared the moment `fetch` resolves — so without
* these a compromised or misconfigured endpoint could dribble bytes forever
* and hang `akm upgrade` with no output and no way out but Ctrl-C. Two
* bounds, because either alone is evadable: a per-chunk STALL deadline (a
* connection making no progress dies quickly, while a slow-but-progressing
* one is left alone) and an OVERALL deadline (a trickle that sends one byte
* per stall-window can't stretch the download indefinitely).
*/
const BINARY_STALL_TIMEOUT_MS = 60_000;
const BINARY_TOTAL_TIMEOUT_MS = 30 * 60_000;
export async function streamResponseToFile(response, destination, maxBytes, limits) {
const declaredText = response.headers.get("content-length");
if (declaredText) {
const declared = Number(declaredText);
if (Number.isFinite(declared) && declared > maxBytes) {
await response.body?.cancel().catch(() => undefined);
throw new ResponseTooLargeError(response.url || "binary download", maxBytes, declared);
}
}
if (!response.body)
throw new Error("Binary download response did not provide a streaming body.");
const url = response.url || "binary download";
const stallTimeoutMs = limits?.stallTimeoutMs ?? BINARY_STALL_TIMEOUT_MS;
const totalTimeoutMs = limits?.totalTimeoutMs ?? BINARY_TOTAL_TIMEOUT_MS;
const overallDeadlineAt = Date.now() + totalTimeoutMs;
const fd = fs.openSync(destination, "w", 0o600);
const reader = response.body.getReader();
const hash = createHash("sha256");
let byteSize = 0;
try {
while (true) {
// Whichever bound bites first wins, and the error reports THAT bound —
// "no data for 60s" and "download exceeded 30m" are different diagnoses.
const stallDeadlineAt = Date.now() + stallTimeoutMs;
const deadlineAt = Math.min(stallDeadlineAt, overallDeadlineAt);
const reportedTimeoutMs = deadlineAt === overallDeadlineAt ? totalTimeoutMs : stallTimeoutMs;
const { done, value } = await readChunkWithDeadline(reader, deadlineAt, undefined, url, reportedTimeoutMs);
if (done)
break;
if (!value)
continue;
byteSize += value.byteLength;
// The catch block cancels the reader for every throw path, this included.
if (byteSize > maxBytes)
throw new ResponseTooLargeError(url, maxBytes, byteSize);
hash.update(value);
let written = 0;
while (written < value.byteLength)
written += fs.writeSync(fd, value, written, value.byteLength - written);
}
fs.fdatasyncSync(fd);
return { byteSize, sha256: hash.digest("hex") };
}
catch (error) {
// Cancel so a timed-out (still-open) connection releases its socket rather
// than lingering until process exit. Already-cancelled readers no-op.
await reader.cancel().catch(() => undefined);
removeFileBestEffort(destination);
throw error;
}
finally {
reader.releaseLock();
fs.closeSync(fd);
}
}
/** Read live runtime signals. */
export function getInstallSignals() {
return {
bunMain: mainPath,
importMetaDir: getDirname(import.meta.url),
hasAkmVersion: typeof AKM_VERSION !== "undefined",
npmGlobalRoot: resolveNpmGlobalRootForThisProcess(),
};
}
function resolveNpmGlobalRootForThisProcess() {
const nodePath = process.env.AKM_LAUNCHER_NODE?.trim() || (process.versions.bun ? undefined : process.execPath);
if (!nodePath)
return undefined;
try {
return resolveNpmGlobalRoot(nodePath, process.env);
}
catch {
return undefined;
}
}
function isUnderDirectory(dir, root) {
const real = (value) => {
try {
return fs.realpathSync(value);
}
catch {
return path.resolve(value);
}
};
const relative = path.relative(real(root), real(dir));
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
/** The package that depends on this akm: the directory holding the `node_modules` it lives in. */
function packageLocalRoot(importMetaDir) {
const index = normalizePathSeparators(importMetaDir).lastIndexOf(NODE_MODULES_SEGMENT);
return index < 0 ? importMetaDir : importMetaDir.slice(0, index);
}
// AKM_VERSION ambient type is declared in globals.d.ts
export function detectInstallMethod(signals) {
const s = signals ?? getInstallSignals();
const normalizedImportMetaDir = normalizePathSeparators(s.importMetaDir);
if (normalizedImportMetaDir.includes(NODE_MODULES_SEGMENT)) {
if (BUN_GLOBAL_INSTALL_PATTERN.test(normalizedImportMetaDir)) {
return "bun";
}
if (PNPM_GLOBAL_INSTALL_PATTERN.test(normalizedImportMetaDir)) {
return "pnpm";
}
// A node_modules install outside the npm global root is a DEPENDENCY of
// some other package (an image's tools dir, a plugin's node_modules):
// it moves when that package does, and an `npm install -g` here would
// "succeed" while the parent kept executing its own copy. Only a proven
// global root can make that call; without one this stays "npm".
if (s.npmGlobalRoot && s.importMetaDir && !isUnderDirectory(s.importMetaDir, s.npmGlobalRoot)) {
return "package-local";
}
return "npm";
}
// Bun-compiled binaries: mainPath points to a virtual /$bunfs/
// path, NOT process.execPath. The old check (mainPath === process.execPath) was
// always false for compiled binaries, causing "unknown" for every binary user.
if (s.bunMain !== undefined) {
// Primary check: compiled binaries embed sources under /$bunfs/
if (s.bunMain.startsWith("/$bunfs/")) {
return "binary";
}
// Secondary check: AKM_VERSION is defined only in compiled builds (via --define)
if (s.hasAkmVersion) {
return "binary";
}
}
return "unknown";
}
export function getAkmBinaryName() {
const platform = process.platform;
const arch = process.arch;
if (platform === "linux" && arch === "x64")
return "akm-linux-x64";
if (platform === "linux" && arch === "arm64")
return "akm-linux-arm64";
if (platform === "darwin" && arch === "x64")
return "akm-darwin-x64";
if (platform === "darwin" && arch === "arm64")
return "akm-darwin-arm64";
if (platform === "win32" && arch === "x64")
return "akm-windows-x64.exe";
throw new ConfigError(`Unsupported platform for binary upgrade: ${platform}/${arch}`, "UNSUPPORTED_PLATFORM");
}
export async function checkForUpdate(currentVersion, fetchOptions) {
const installMethod = detectInstallMethod();
const url = `https://api.github.com/repos/${REPO}/releases/latest`;
const response = await fetchWithRetry(url, { headers: githubHeaders() }, fetchOptions);
if (!response.ok) {
throw new Error(`Failed to check for updates: ${response.status} ${response.statusText}`);
}
const release = JSON.parse(await readBodyWithByteCap(response, MAX_CHECKSUM_METADATA_BYTES));
const latestTag = release.tag_name ?? "";
const latestVersion = latestTag.replace(/^v/, "");
return {
currentVersion,
latestVersion,
updateAvailable: latestVersion !== "" && semverOrder(currentVersion, latestVersion) < 0,
installMethod,
};
}
/**
* Whether the operator has asked to bypass upgrade checksum verification.
*
* Deliberately NOT a CLI flag (C6): STABILITY.md states checksum verification
* is not optional, so the recovery hatch for a genuinely broken `checksums.txt`
* is this internal, undocumented-on-purpose env var — not something
* discoverable in `--help` or offered by tab completion.
*
* Extracted rather than read inline so `performUpgrade` stays under the
* function-size bar (`scripts/lint-src-fn-size.ts`), whose baseline only
* shrinks.
*/
function checksumBypassRequested() {
return process.env.AKM_UPGRADE_SKIP_CHECKSUM === "1";
}
export async function performUpgrade(check, opts, dependencies) {
const { currentVersion, latestVersion, installMethod } = check;
const force = opts?.force === true;
const skipPostUpgrade = opts?.skipPostUpgrade === true;
const runTool = dependencies?.runMigrationTool ?? runMigrationTool;
// Every `akm upgrade` ends by running `akm-migrate apply`, install or no
// install: the migrator on disk after the install step is the one whose
// migrations the installed akm needs, and an image that ships akm has
// nothing to install and nobody to run a migration by hand (#895). The two
// no-install cases return here.
if (installMethod === "package-local") {
const parent = packageLocalRoot(getInstallSignals().importMetaDir ?? "");
return {
currentVersion,
newVersion: latestVersion,
upgraded: false,
installMethod,
message: `akm runs as a dependency of the package at ${parent}; upgrade that package to move akm.`,
migration: await runMigrationStep(runTool),
};
}
if (!check.updateAvailable && !force) {
return {
currentVersion,
newVersion: latestVersion,
upgraded: false,
installMethod,
message: `akm v${currentVersion} is already the latest version`,
migration: await runMigrationStep(runTool),
};
}
const packageManagerCommand = getPackageManagerUpgradeCommand(installMethod);
if (packageManagerCommand) {
return runPackageManagerUpgrade({
packageManagerCommand,
currentVersion,
latestVersion,
installMethod,
skipPostUpgrade,
runTool,
});
}
if (installMethod === "unknown") {
return {
currentVersion,
newVersion: latestVersion,
upgraded: false,
installMethod,
message: `Unable to detect install method. Upgrade manually from https://github.com/${REPO}/releases`,
migration: await runMigrationStep(runTool),
};
}
// Binary install
if (!latestVersion) {
throw new Error("Unable to determine latest version from GitHub releases. Check https://github.com/itlackey/akm/releases");
}
const tag = `v${latestVersion}`;
const binaryName = getAkmBinaryName();
const binaryUrl = `https://github.com/${REPO}/releases/download/${tag}/${binaryName}`;
const checksumsUrl = `https://github.com/${REPO}/releases/download/${tag}/checksums.txt`;
const execPath = dependencies?.execPath ?? process.execPath;
const execDir = path.dirname(execPath);
const execName = path.basename(execPath);
const stagedPath = IS_WINDOWS
? path.join(execDir, `.${execName}.new.${process.pid}`)
: path.join(execDir, `.${execName}.tmp.${process.pid}`);
const backupPath = `${execPath}${IS_WINDOWS ? ".old" : ".bak"}`;
// Download binary
const binaryResponse = await fetchWithRetry(binaryUrl);
if (!binaryResponse.ok) {
throw new Error(`Failed to download binary: ${binaryResponse.status} ${binaryResponse.statusText}`);
}
let downloaded;
try {
downloaded = await streamResponseToFile(binaryResponse, stagedPath, MAX_BINARY_DOWNLOAD_BYTES);
if (!IS_WINDOWS)
fs.chmodSync(stagedPath, 0o755);
}
catch (err) {
removeFileBestEffort(stagedPath);
const code = err.code;
if (code === "EACCES" || code === "EPERM") {
throw new ConfigError(`Permission denied writing to ${execDir}.\n` +
`${IS_WINDOWS ? "Try running as Administrator." : "Run: sudo akm upgrade"}\n` +
`Or re-run the install script: curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | bash`, "UPGRADE_BLOCKED");
}
throw err;
}
// Download and verify checksum (mandatory — upgrade is blocked if checksums cannot be fetched).
let checksumVerified = false;
const skipChecksum = checksumBypassRequested();
try {
const checksumsResponse = await fetchWithRetry(checksumsUrl);
if (!checksumsResponse.ok) {
if (skipChecksum) {
warn(`WARNING: checksums.txt fetch failed (HTTP ${checksumsResponse.status}). Proceeding without verification because AKM_UPGRADE_SKIP_CHECKSUM=1 was set.`);
}
else {
throw new Error(`Checksum verification failed: could not fetch ${checksumsUrl} (HTTP ${checksumsResponse.status}). ` +
`Set AKM_UPGRADE_SKIP_CHECKSUM=1 to bypass (not recommended).`);
}
}
else {
const checksumsText = await readBodyWithByteCap(checksumsResponse, MAX_CHECKSUM_METADATA_BYTES);
const expectedHash = parseChecksumForFile(checksumsText, binaryName);
if (expectedHash) {
const actualHash = downloaded.sha256;
if (actualHash !== expectedHash) {
throw new Error(`Checksum mismatch for ${binaryName}.\n` + `Expected: ${expectedHash}\n` + `Got: ${actualHash}`);
}
checksumVerified = true;
}
else {
if (skipChecksum) {
warn(`WARNING: ${binaryName} not found in checksums.txt. Proceeding without verification because AKM_UPGRADE_SKIP_CHECKSUM=1 was set.`);
}
else {
throw new Error(`Checksum verification failed: ${binaryName} not listed in checksums.txt. ` +
`Set AKM_UPGRADE_SKIP_CHECKSUM=1 to bypass (not recommended).`);
}
}
}
}
catch (err) {
if (err instanceof Error &&
(err.message.includes("Checksum mismatch") || err.message.includes("Checksum verification failed"))) {
removeFileBestEffort(stagedPath);
throw err;
}
// Network or parse failure
if (skipChecksum) {
warn(`WARNING: Could not fetch or parse checksums: ${err instanceof Error ? err.message : String(err)}. Proceeding because AKM_UPGRADE_SKIP_CHECKSUM=1 was set.`);
}
else {
removeFileBestEffort(stagedPath);
throw new Error(`Checksum verification failed: ${err instanceof Error ? err.message : String(err)}. ` +
`Set AKM_UPGRADE_SKIP_CHECKSUM=1 to bypass (not recommended).`);
}
}
if (fs.existsSync(backupPath)) {
warn(`A previous upgrade left a stale backup at ${backupPath}; overwriting it.`);
removeFileBestEffort(backupPath);
}
try {
if (IS_WINDOWS)
fs.renameSync(execPath, backupPath);
else
fs.copyFileSync(execPath, backupPath);
fs.renameSync(stagedPath, execPath);
}
catch (err) {
removeFileBestEffort(stagedPath);
if (!fs.existsSync(execPath) && fs.existsSync(backupPath)) {
fs.renameSync(backupPath, execPath);
}
throw err;
}
// The replacement completed; the temporary rollback copy is no longer needed.
removeFileBestEffort(backupPath);
// The new binary is at execPath now, so this re-execs the NEW migrator.
const migration = await runMigrationStep(runTool);
return {
currentVersion,
newVersion: latestVersion,
upgraded: true,
installMethod,
binaryPath: execPath,
checksumVerified,
migration,
postUpgrade: runPostUpgradeTasks(execPath, { skip: skipPostUpgrade }),
};
}
/**
* `akm-migrate apply`, spawned so it is whichever migrator is on disk NOW:
* after a successful install, the new one. Its JSON plan becomes the
* response's `migration`. A migrator that could not run or print a plan
* reports `failed` with its error text instead of throwing, so the install
* outcome the caller is about to report is never lost behind it.
*/
async function runMigrationStep(runTool) {
let result;
try {
result = await runTool(["apply"]);
}
catch (error) {
return { status: "failed", error: error instanceof Error ? error.message : String(error) };
}
const line = result.stdout.trim();
try {
const plan = JSON.parse(line);
if (plan.status === "current" || plan.status === "ready" || plan.status === "blocked") {
return plan;
}
}
catch {
// Not a plan; reported below with whatever the migrator did say.
}
return { status: "failed", error: result.stderr.trim() || line || `akm-migrate exited ${result.status}` };
}
/**
* Rebuild the derived index after a successful upgrade.
*/
function runPostUpgradeTasks(akmBin, opts) {
if (opts.skip) {
return {
ok: true,
skipped: true,
message: "Upgrade completed. Skipped the index rebuild. Run `akm index` manually to rebuild the index.",
};
}
try {
const result = childProcess.spawnSync(akmBin, ["index"], {
encoding: "utf8",
env: process.env,
stdio: "pipe",
});
if (result.error) {
return {
ok: false,
skipped: false,
message: `Upgrade completed. The index rebuild could not start: ${result.error.message}. Run \`akm index\` manually.`,
};
}
if (result.status !== 0) {
const detail = (result.stderr ?? "").trim() || (result.stdout ?? "").trim() || `exit code ${result.status}`;
return {
ok: false,
skipped: false,
exitCode: result.status,
message: `Upgrade completed. Post-upgrade \`akm index\` failed (${detail}). Run \`akm index\` manually.`,
};
}
return {
ok: true,
skipped: false,
exitCode: 0,
message: "Upgrade completed and the index was rebuilt against the new binary.",
};
}
catch (err) {
const detail = err instanceof Error ? err.message : String(err);
return {
ok: false,
skipped: false,
message: `Upgrade completed. The index rebuild failed: ${detail}. Run \`akm index\` manually.`,
};
}
}
/**
* The package-manager arm of {@link performUpgrade}: install → post-install
* version verification → post-upgrade tasks.
* Extracted whole so performUpgrade stays under its fn-size baseline.
*/
async function runPackageManagerUpgrade(input) {
const { packageManagerCommand, currentVersion, latestVersion, installMethod, skipPostUpgrade, runTool } = input;
if (!latestVersion) {
throw new Error("Unable to determine latest version from GitHub releases. Check https://github.com/itlackey/akm/releases");
}
const result = childProcess.spawnSync(packageManagerCommand.command, packageManagerCommand.args, {
encoding: "utf8",
env: process.env,
stdio: "pipe",
});
if (result.error) {
throw new Error(`Failed to run '${packageManagerCommand.displayCommand}': ${result.error.message}`);
}
if (result.status !== 0) {
const details = (result.stderr ?? "").trim() || (result.stdout ?? "").trim() || `exit code ${result.status}`;
// The install could not change what runs, so the migrator on disk is
// still the right one: run it, then say so, or an operator reading the
// EACCES will assume the migration is stuck behind it (#895).
const migration = await runMigrationStep(runTool);
throw new Error(`Failed to upgrade akm via ${installMethod}: ${details}\nRun manually: ${packageManagerCommand.displayCommand}\n` +
`Pending migrations ran anyway (status: ${migration.status}).`);
}
// The package manager exiting 0 does not prove it delivered
// `latestVersion`: a lagging `@latest` dist-tag (partial publish,
// registry mirror lag) "succeeds" while leaving the old version on PATH.
// Re-read the version the shim actually reports before claiming an
// upgrade, so stop before claiming success.
const installedVersion = readInstalledCliVersion("akm");
if (installedVersion !== undefined && installedVersion !== latestVersion) {
return {
currentVersion,
newVersion: latestVersion,
upgraded: false,
installMethod,
message: `\`${packageManagerCommand.displayCommand}\` succeeded, but \`akm --version\` still reports ` +
`v${installedVersion} (expected v${latestVersion}). The ${installMethod} registry's @latest tag ` +
`may be lagging the GitHub release — try again shortly, or install the exact version: ` +
`${packageManagerCommand.displayCommand.replace(/@latest\b/, `@${latestVersion}`)}`,
migration: await runMigrationStep(runTool),
};
}
return {
currentVersion,
newVersion: latestVersion,
upgraded: true,
installMethod,
message: installedVersion === latestVersion
? `akm upgraded via ${installMethod} (verified: akm --version reports v${installedVersion})`
: `akm upgraded via ${installMethod} (installed version could not be verified)`,
migration: await runMigrationStep(runTool),
postUpgrade: runPostUpgradeTasks("akm", { skip: skipPostUpgrade }),
};
}
/**
* Read the version the on-PATH `akm` actually reports, or `undefined` when
* that cannot be determined (no shim on PATH in a sandbox, unparseable
* output). Verification is fail-open on its own infrastructure — only a
* CONFIRMED mismatch downgrades an upgrade result (§24.2 Package/release
* gate).
*/
function readInstalledCliVersion(akmBin) {
const result = childProcess.spawnSync(akmBin, ["--version"], {
encoding: "utf8",
env: process.env,
stdio: "pipe",
});
if (result.error || result.status !== 0)
return undefined;
const match = (result.stdout ?? "").match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/);
return match?.[0];
}
function removeFileBestEffort(filePath) {
try {
fs.unlinkSync(filePath);
}
catch {
// Cleanup is best-effort; the primary operation determines success.
}
}
function parseChecksumForFile(checksumsText, filename) {
for (const line of checksumsText.split("\n")) {
const trimmed = line.trim();
if (!trimmed)
continue;
// Format: <hash> <filename>
const match = trimmed.match(/^([0-9a-f]{64})\s+(.+)$/);
if (match && match[2] === filename) {
return match[1];
}
}
return undefined;
}
function normalizePathSeparators(value) {
return (value ?? "").replaceAll("\\", "/");
}
function getInstalledPackageName() {
try {
const pkgPath = path.resolve(getDirname(import.meta.url), "../../../package.json");
if (fs.existsSync(pkgPath)) {
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
if (typeof pkg.name === "string" && pkg.name.trim()) {
return pkg.name.trim();
}
}
}
catch {
// Swallow and fall back to default package name.
}
return DEFAULT_PACKAGE_NAME;
}
function resolveNodePackageManagerCommand(name) {
const extension = IS_WINDOWS ? ".cmd" : "";
const adjacent = path.join(path.dirname(process.execPath), `${name}${extension}`);
return fs.existsSync(adjacent) ? adjacent : name;
}
export function getPackageManagerUpgradeCommand(installMethod, packageName = getInstalledPackageName()) {
const pkgRef = `${packageName}@latest`;
if (installMethod === "bun") {
return {
command: "bun",
args: ["install", "-g", pkgRef],
displayCommand: `bun install -g ${pkgRef}`,
};
}
if (installMethod === "pnpm") {
return {
command: resolveNodePackageManagerCommand("pnpm"),
args: ["add", "-g", pkgRef],
displayCommand: `pnpm add -g ${pkgRef}`,
};
}
if (installMethod === "npm") {
return {
command: resolveNodePackageManagerCommand("npm"),
args: ["install", "-g", pkgRef],
displayCommand: `npm install -g ${pkgRef}`,
};
}
return undefined;
}