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
497 lines (496 loc) • 25.2 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 { spawnSync } from "node:child_process";
import { randomBytes } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { isWithin } from "../../core/common.js";
import { UsageError } from "../../core/errors.js";
import { getRegistryCacheDir } from "../../core/paths.js";
import { gitCredentialEnvironment, parseRegistryRef, resolveRegistryArtifact, validateGitRef, validateGitUrl, } from "../../registry/resolve.js";
import { applyAkmIncludeConfig, buildInstallCacheDir, detectStashRoot, isDirectory } from "./provider-utils.js";
/**
* Shared subprocess wrapper for `git` invocations. Disables git's interactive
* terminal prompt so a missing credential never hangs the process.
*/
export function runGit(args, options) {
return spawnSync("git", args, {
encoding: "utf8",
...options,
env: { ...process.env, ...options?.env, GIT_TERMINAL_PROMPT: "0" },
});
}
/** Fetch and classify the current branch against its upstream without changing the worktree. */
export function inspectGitUpstream(repoDir, credential) {
const remotes = runGit(["-C", repoDir, "remote"]);
if (remotes.status !== 0)
throw new UsageError(`Cannot inspect Git remotes at ${repoDir}: ${remotes.stderr.trim()}`);
if (!remotes.stdout.trim())
return { hasRemote: false, ahead: 0, behind: 0 };
const fetch = runGit(["-C", repoDir, "fetch", "--prune"], {
timeout: 120_000,
env: gitCredentialEnvironment(credential),
});
if (fetch.status !== 0)
throw new UsageError(`Cannot refresh Git target at ${repoDir}: ${fetch.stderr.trim()}`);
const upstream = runGit(["-C", repoDir, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
if (upstream.status !== 0 || !upstream.stdout.trim()) {
throw new UsageError(`Git target at ${repoDir} has a remote but no upstream branch; configure one before writing.`);
}
const relation = gitRelation(repoDir, upstream.stdout.trim());
return { hasRemote: true, upstream: upstream.stdout.trim(), ...relation };
}
/**
* Verify the actually-cloned HEAD matches the revision resolved before the
* clone ran (R-011). This is a presence/identity check only — full
* content-digest verification across install/update is separate, larger
* scope work. `expectedRevision` may be an annotated tag's OBJECT id (which
* `git ls-remote` reports, distinct from the commit it points to), so the
* comparison peels it (`^{commit}`) before comparing against HEAD; a plain
* commit SHA peels to itself.
*/
export function verifyClonedRevision(cloneDir, url, expectedRevision) {
if (!expectedRevision) {
// `resolveGitArtifact` (registry/resolve.ts) throws when it cannot resolve
// a revision via `git ls-remote`, so a plain git install never reaches
// this call with `undefined` — treat it as a bug, not a silently-skipped
// check that would defeat the R-011 post-clone integrity verification.
throw new UsageError(`No revision was resolved from ${url}; refusing to install without a verifiable checkout.`);
}
const head = runGit(["-C", cloneDir, "rev-parse", "HEAD"]);
if (head.status !== 0 || !head.stdout.trim()) {
throw new UsageError(`Failed to read cloned HEAD at ${cloneDir}: ${head.stderr.trim() || "rev-parse failed"}`);
}
const actual = head.stdout.trim();
const peeled = runGit(["-C", cloneDir, "rev-parse", `${expectedRevision}^{commit}`]);
const expectedCommit = peeled.status === 0 ? peeled.stdout.trim() : expectedRevision;
if (actual !== expectedCommit) {
throw new UsageError(`Cloned HEAD ${actual} at ${cloneDir} does not match the revision resolved from ${url} (${expectedRevision}); refusing to install a mismatched checkout.`);
}
}
function gitRelation(repoDir, target) {
const result = runGit(["-C", repoDir, "rev-list", "--left-right", "--count", `HEAD...${target}`]);
const match = result.stdout.trim().match(/^(\d+)\s+(\d+)$/);
if (result.status !== 0 || !match) {
throw new UsageError(`Cannot compare Git history at ${repoDir}: ${result.stderr.trim() || "unknown relation"}`);
}
return { ahead: Number(match[1]), behind: Number(match[2]) };
}
/** Reject a fast-forward that would replace an ignored, untracked local path. */
export function assertNoIgnoredPathOverwrite(repoDir, targetRevision) {
const ignored = runGit(["-C", repoDir, "ls-files", "--others", "--ignored", "--exclude-standard", "-z"]);
if (ignored.status !== 0) {
throw new UsageError(`Cannot inspect ignored files at ${repoDir}: ${ignored.stderr.trim()}`);
}
const ignoredPaths = ignored.stdout.split("\0").filter(Boolean);
if (ignoredPaths.length === 0)
return;
const changed = runGit(["-C", repoDir, "diff", "--name-only", "--no-renames", "-z", "HEAD", targetRevision]);
if (changed.status !== 0) {
throw new UsageError(`Cannot inspect incoming Git paths at ${repoDir}: ${changed.stderr.trim()}`);
}
const changedPaths = changed.stdout.split("\0").filter(Boolean);
const conflict = ignoredPaths.find((ignoredPath) => changedPaths.some((changedPath) => changedPath === ignoredPath ||
changedPath.startsWith(`${ignoredPath}/`) ||
ignoredPath.startsWith(`${changedPath}/`)));
if (conflict) {
throw new UsageError(`Git update would overwrite ignored local path ${path.join(repoDir, conflict)}; move or remove it before update.`);
}
}
/**
* Whether a requested ref is a full commit hash rather than a branch or tag.
*
* Only the unambiguous 40-hex (SHA-1) and 64-hex (SHA-256) forms count. An
* abbreviated hash is indistinguishable from a legal branch name, so it stays
* on the `--branch` path where git itself reports the mismatch.
*/
function isCommitSha(ref) {
return /^[0-9a-f]{40}$/i.test(ref) || /^[0-9a-f]{64}$/i.test(ref);
}
function normalizeRemoteUrl(value) {
return value
.trim()
.replace(/\/+$/, "")
.replace(/\.git$/i, "");
}
/**
* Move a fully materialized directory into place. POSIX cannot rename over a
* non-empty directory, so the old one is renamed aside first and restored if
* the second rename fails; the destination is missing for one syscall at most.
*/
export function replaceDirectory(stagedDir, destination) {
const backup = `${destination}.backup-${randomBytes(4).toString("hex")}`;
fs.mkdirSync(path.dirname(destination), { recursive: true });
const hadDestination = fs.existsSync(destination);
if (hadDestination)
fs.renameSync(destination, backup);
try {
fs.renameSync(stagedDir, destination);
}
catch (error) {
if (hadDestination && fs.existsSync(backup))
fs.renameSync(backup, destination);
throw error;
}
if (hadDestination) {
try {
fs.rmSync(backup, { recursive: true, force: true });
}
catch {
// The new destination is live; a stale backup is safer than reporting a
// failed replacement after the swap already succeeded.
}
}
}
/**
* Materialize a Git install ref (`akm bundle add github:owner/repo` or
* `akm bundle add git:url`) through the clone, strip, and include-filter pipeline.
*/
export async function syncGitRef(ref, options) {
const parsed = parseRegistryRef(ref);
if (parsed.source === "github") {
const githubRef = {
source: "git",
ref: parsed.ref,
id: parsed.id,
url: `https://github.com/${parsed.owner}/${parsed.repo}.git`,
requestedRef: parsed.requestedRef,
};
const result = await doSyncGit(githubRef, options);
return { ...result, source: "github" };
}
if (parsed.source !== "git") {
throw new UsageError(`syncGitRef requires a git: or github: ref, got "${ref}"`);
}
return doSyncGit(parsed, options);
}
async function doSyncGit(parsed, options) {
validateGitUrl(parsed.url);
if (parsed.requestedRef)
validateGitRef(parsed.requestedRef);
const resolved = await resolveRegistryArtifact(parsed, { gitCredential: options?.credential });
const syncedAt = (options?.now ?? new Date()).toISOString();
if (options?.writable && options.writableRoot) {
return syncExistingWritableCheckout(parsed, resolved, options.writableRoot, syncedAt, options.writableRequiredRoots, options.credential);
}
const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheDir();
const cacheDir = buildInstallCacheDir(cacheRootDir, parsed.source, parsed.id, options?.writable ? "writable" : resolved.resolvedRevision);
const cloneDir = path.join(cacheDir, "clone");
const extractedDir = path.join(cacheDir, "extracted");
// Cache hit. Writable installs must remain real checkouts so every mutation
// can be committed; an older extracted snapshot is not eligible.
if (isDirectory(extractedDir) && (!options?.writable || isDirectory(path.join(extractedDir, ".git")))) {
if (options?.writable) {
const provisionalBundleRoot = detectStashRoot(extractedDir);
const installRoot = applyAkmIncludeConfig(provisionalBundleRoot, cacheDir, extractedDir) ?? provisionalBundleRoot;
if (installRoot !== provisionalBundleRoot) {
throw new UsageError("Writable Git installs do not support akm.include (package.json) filtered snapshots.");
}
return syncExistingWritableCheckout(parsed, resolved, detectStashRoot(installRoot), syncedAt, options.writableRequiredRoots, options.credential);
}
try {
if (options?.force) {
throw new Error("refresh read-only snapshot");
}
const provisionalBundleRoot = detectStashRoot(extractedDir);
const installRoot = applyAkmIncludeConfig(provisionalBundleRoot, cacheDir, extractedDir) ?? provisionalBundleRoot;
const stashRoot = detectStashRoot(installRoot);
if (stashRoot) {
return {
id: resolved.id,
source: resolved.source,
ref: resolved.ref,
artifactUrl: resolved.artifactUrl,
resolvedVersion: resolved.resolvedVersion,
resolvedRevision: resolved.resolvedRevision,
contentDir: stashRoot,
cacheDir,
extractedDir,
writable: options?.writable,
syncedAt,
};
}
}
catch {
// Cache invalid, re-clone
}
}
const cacheExisted = fs.existsSync(cacheDir);
fs.mkdirSync(cacheDir, { recursive: true });
fs.rmSync(cloneDir, { recursive: true, force: true });
let provisionalBundleRoot;
let installRoot;
let stashRoot;
try {
// `git clone --branch` accepts only a branch or tag name, never a raw commit
// hash — so pinning an install to a commit (`#<40-hex-sha>`, which
// parseGithubShorthand/parseGitUrl accept and validateGitRef allows) made the
// clone fail outright. A commit pin needs a full clone followed by a
// checkout of that revision; `--depth 1` cannot fetch an arbitrary commit
// either, so the read-only shallow optimization is skipped in that case.
const pinnedCommit = parsed.requestedRef !== undefined && isCommitSha(parsed.requestedRef);
const cloneArgs = ["clone"];
if (!options?.writable && !pinnedCommit)
cloneArgs.push("--depth", "1");
if (parsed.requestedRef && !pinnedCommit) {
cloneArgs.push("--branch", parsed.requestedRef);
}
cloneArgs.push(parsed.url, cloneDir);
const cloneResult = runGit(cloneArgs, {
timeout: 120_000,
env: gitCredentialEnvironment(options?.credential),
});
if (cloneResult.status !== 0) {
throw new Error(classifyCloneFailure(parsed.url, cloneResult.stderr, cloneResult.error));
}
if (pinnedCommit) {
const checkout = runGit(["-C", cloneDir, "checkout", "--detach", parsed.requestedRef], { timeout: 120_000 });
if (checkout.status !== 0) {
throw new Error(`Could not check out commit ${parsed.requestedRef} from ${parsed.url}: ${checkout.stderr.trim() || "unknown git error"}`);
}
}
// R-011: `resolved.resolvedRevision` was resolved via a SEPARATE
// `git ls-remote` round-trip before this clone ran (resolveGitArtifact /
// resolveGithubArtifact in registry/resolve.ts) and was never checked
// against what actually got cloned. Verify it now, while `.git` still
// exists (the read-only branch below strips it).
verifyClonedRevision(cloneDir, parsed.url, resolved.resolvedRevision);
if (options?.writable) {
const branch = runGit(["-C", cloneDir, "branch", "--show-current"]);
if (branch.status !== 0 || !branch.stdout.trim()) {
throw new UsageError("Writable Git installs require a branch ref; tags and detached revisions are read-only.");
}
const stagedRoot = detectStashRoot(cloneDir);
if (applyAkmIncludeConfig(stagedRoot, cacheDir, cloneDir)) {
throw new UsageError("Writable Git installs do not support akm.include (package.json) filtered snapshots.");
}
replaceDirectory(cloneDir, extractedDir);
}
else {
// Read-only installs are immutable snapshots and do not retain Git metadata.
fs.rmSync(path.join(cloneDir, ".git"), { recursive: true, force: true });
replaceDirectory(cloneDir, extractedDir);
}
provisionalBundleRoot = detectStashRoot(extractedDir);
installRoot = applyAkmIncludeConfig(provisionalBundleRoot, cacheDir, extractedDir) ?? provisionalBundleRoot;
if (options?.writable && installRoot !== provisionalBundleRoot) {
throw new UsageError("Writable Git installs do not support akm.include (package.json) filtered snapshots.");
}
stashRoot = detectStashRoot(installRoot);
}
catch (err) {
fs.rmSync(cloneDir, { recursive: true, force: true });
try {
if (!cacheExisted)
fs.rmSync(cacheDir, { recursive: true, force: true });
}
catch {
/* best-effort */
}
throw err;
}
return {
id: resolved.id,
source: resolved.source,
ref: resolved.ref,
artifactUrl: resolved.artifactUrl,
resolvedVersion: resolved.resolvedVersion,
resolvedRevision: resolved.resolvedRevision,
contentDir: stashRoot,
cacheDir,
extractedDir,
writable: options?.writable,
syncedAt,
};
}
export function syncExistingWritableCheckout(parsed, resolved, contentRoot, syncedAt, requiredRoots = [], credential) {
const root = path.resolve(contentRoot);
const repoResult = runGit(["-C", root, "rev-parse", "--show-toplevel"]);
if (repoResult.status !== 0 || !repoResult.stdout.trim()) {
throw new UsageError(`Writable Git install at ${root} is not a checkout; refusing to replace it because it may contain local work.`);
}
const repoDir = path.resolve(repoResult.stdout.trim());
if (!isWithin(root, repoDir)) {
throw new UsageError(`Writable Git content root ${root} resolves outside its checkout at ${repoDir}.`);
}
const remote = runGit(["-C", repoDir, "remote", "get-url", "origin"]);
if (remote.status !== 0 || normalizeRemoteUrl(remote.stdout) !== normalizeRemoteUrl(parsed.url)) {
throw new UsageError(`Writable Git install at ${root} points at a different origin; refusing to update or replace the checkout.`);
}
const status = runGit(["-C", repoDir, "status", "--porcelain"]);
if (status.status !== 0 || status.stdout.trim()) {
throw new UsageError(`Writable Git install at ${root} has uncommitted changes; commit or discard them before update.`);
}
if (parsed.requestedRef) {
const branch = runGit(["-C", repoDir, "branch", "--show-current"]);
const expectedBranch = parsed.requestedRef.replace(/^refs\/heads\//, "");
if (branch.status !== 0 || !branch.stdout.trim() || branch.stdout.trim() !== expectedBranch) {
throw new UsageError(`Writable Git install at ${root} is checked out on a different branch than requested ref "${parsed.requestedRef}".`);
}
}
const shallow = runGit(["-C", repoDir, "rev-parse", "--is-shallow-repository"]);
const fetchArgs = ["-C", repoDir, "fetch", "--prune", "--tags"];
if (shallow.status === 0 && shallow.stdout.trim() === "true")
fetchArgs.push("--unshallow");
fetchArgs.push("origin");
if (parsed.requestedRef)
fetchArgs.push(parsed.requestedRef);
const fetch = runGit(fetchArgs, { timeout: 120_000, env: gitCredentialEnvironment(credential) });
if (fetch.status !== 0) {
throw new UsageError(`Writable Git install at ${root} could not fetch its expected origin; local work was preserved. ${fetch.stderr.trim()}`);
}
let targetRevision = resolved.resolvedRevision;
if (targetRevision) {
const resolvedTarget = runGit(["-C", repoDir, "rev-parse", "--verify", `${targetRevision}^{commit}`]);
if (resolvedTarget.status === 0)
targetRevision = resolvedTarget.stdout.trim();
else
targetRevision = undefined;
}
if (!targetRevision) {
const fallbackTarget = runGit([
"-C",
repoDir,
"rev-parse",
"--verify",
parsed.requestedRef ? "FETCH_HEAD" : "@{u}",
]);
if (fallbackTarget.status !== 0 || !fallbackTarget.stdout.trim()) {
throw new UsageError(`Writable Git install at ${root} has no verifiable upstream revision.`);
}
targetRevision = fallbackTarget.stdout.trim();
}
const relation = gitRelation(repoDir, targetRevision);
const rootsToPreserve = [...new Set([root, ...requiredRoots.map((candidate) => path.resolve(candidate))])];
for (const requiredRoot of rootsToPreserve) {
if (!isWithin(requiredRoot, repoDir)) {
throw new UsageError(`Configured Git component root ${requiredRoot} resolves outside ${repoDir}.`);
}
}
if (relation.behind > 0) {
if (relation.ahead > 0) {
throw new UsageError(`Writable Git install at ${root} has local commits that are not in the requested upstream revision; push or reconcile them before update.`);
}
assertNoIgnoredPathOverwrite(repoDir, targetRevision);
for (const requiredRoot of rootsToPreserve) {
const relative = path.relative(repoDir, requiredRoot).replaceAll(path.sep, "/");
if (!relative)
continue;
const tree = runGit(["-C", repoDir, "ls-tree", "-d", "-z", "--name-only", targetRevision, "--", relative]);
const names = tree.stdout.split("\0").filter(Boolean);
if (tree.status !== 0 || !names.includes(relative)) {
throw new UsageError(`Writable Git update would remove configured content root ${requiredRoot}; the existing checkout was left unchanged.`);
}
}
const statusBeforeMerge = runGit(["-C", repoDir, "status", "--porcelain"]);
if (statusBeforeMerge.status !== 0 || statusBeforeMerge.stdout.trim()) {
throw new UsageError(`Writable Git install at ${root} changed while its update was prepared; local work was preserved.`);
}
assertNoIgnoredPathOverwrite(repoDir, targetRevision);
const merge = runGit(["-C", repoDir, "merge", "--ff-only", "--no-overwrite-ignore", targetRevision], {
timeout: 120_000,
});
if (merge.status !== 0) {
throw new UsageError(`Writable Git install at ${root} cannot fast-forward; local commits were preserved. ${merge.stderr.trim()}`);
}
}
for (const requiredRoot of rootsToPreserve) {
if (!isDirectory(requiredRoot)) {
throw new UsageError(`Writable Git update did not preserve configured content root ${requiredRoot}.`);
}
}
const head = runGit(["-C", repoDir, "rev-parse", "HEAD"]);
return {
id: resolved.id,
source: resolved.source,
ref: resolved.ref,
artifactUrl: resolved.artifactUrl,
resolvedVersion: resolved.resolvedVersion,
resolvedRevision: head.status === 0 ? head.stdout.trim() : targetRevision,
contentDir: root,
cacheDir: repoDir,
extractedDir: repoDir,
writable: true,
syncedAt,
};
}
export function cloneRepo(cloneUrl, ref, destDir, writable = false, credential) {
// Stage the clone into a sibling temp dir so that a failed clone never
// destroys a previously-valid destDir (e.g. when the remote is temporarily
// unreachable and we have a valid cached copy).
const tmpDir = `${destDir}.tmp-${randomBytes(4).toString("hex")}`;
const args = ["clone"];
if (!writable)
args.push("--depth", "1");
if (ref)
args.push("--branch", ref);
args.push(cloneUrl, tmpDir);
const result = runGit(args, { timeout: 120_000, env: gitCredentialEnvironment(credential) });
if (result.status !== 0) {
// Clean up the (possibly partial) temp dir but leave destDir untouched.
fs.rmSync(tmpDir, { recursive: true, force: true });
throw new Error(classifyCloneFailure(cloneUrl, result.stderr, result.error));
}
try {
if (!writable) {
// Remove .git directory — we only need the working tree for read-only stashes
const gitDir = path.join(tmpDir, ".git");
if (fs.existsSync(gitDir))
fs.rmSync(gitDir, { recursive: true, force: true });
}
replaceDirectory(tmpDir, destDir);
}
catch (err) {
// Post-clone steps failed — clean up the temp dir to avoid orphaned dirs.
fs.rmSync(tmpDir, { recursive: true, force: true });
throw err;
}
}
// ── Clone-failure classification (#487) ─────────────────────────────────────
/**
* Translate git's stderr into an actionable message. Without this, a user
* who passes a nonexistent or private repo to `akm bundle add` sees:
*
* "could not read Username for 'https://github.com': No such device or
* address"
*
* That is git falling through to its auth-prompt path — the actual cause
* is "repo doesn't exist (or is private)". We classify the common patterns
* and emit a message that names the cause and the fix.
*/
export function classifyCloneFailure(url, stderr, spawnError) {
const safeUrl = redactUrlUserinfo(url);
const raw = (stderr ?? "").trim();
const spawnMsg = spawnError?.message ?? "";
// `git` binary not on PATH.
if (spawnError?.code === "ENOENT") {
return `Failed to clone ${safeUrl}: 'git' is not installed or not on PATH. Install git, then re-run.`;
}
// Auth-prompt fall-through (the headline #487 case).
if (/could not read Username|terminal prompts disabled|Authentication failed|fatal: Authentication/i.test(raw)) {
return (`Failed to clone ${safeUrl}: repository not found or private. ` +
`If the repository is public, double-check the URL and try again. ` +
`If it is private, set GH_TOKEN (or configure a git credential helper) before re-running.`);
}
// 404-style messages from git http.
if (/repository '.*' not found|HTTP 404|fatal: remote error|not found:|Not Found/i.test(raw)) {
return (`Failed to clone ${safeUrl}: repository not found. ` +
`Check the URL — for GitHub, the form is 'owner/repo' or 'github:owner/repo'.`);
}
// SSH connection issues.
if (/Permission denied \(publickey\)|kex_exchange_identification|Connection refused|Connection timed out/i.test(raw)) {
return (`Failed to clone ${safeUrl}: network or SSH failure. ` +
`Check connectivity, your SSH agent, and the remote host's availability.`);
}
// Branch / ref-specific failures.
if (/Remote branch .* not found in upstream origin|couldn't find remote ref/i.test(raw)) {
return (`Failed to clone ${safeUrl}: the requested branch/tag does not exist on the remote. ` +
`Verify the ref name and re-run.`);
}
const detail = raw || spawnMsg || "unknown error";
return `Failed to clone ${safeUrl}: ${redactUrlUserinfo(detail)}`;
}
function redactUrlUserinfo(text) {
return text.replace(/\b([A-Za-z][A-Za-z0-9+.-]*:\/\/)([^\s/@]+)@/g, "$1[REDACTED]@");
}