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
226 lines (225 loc) • 9.82 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/.
/**
* `akm bundle` command group — 0.9 CLI overhaul (S7).
*
* Consolidates the five former top-level bundle-lifecycle commands under one
* group, hard break, no alias kept at the top level:
* - `create` (= old top-level `init`, src/commands/sources/init.ts)
* - `add` (= old top-level `add`, src/commands/sources/add-cli.ts — the
* command definition moved here; `add-cli.ts` keeps its exported helpers,
* including `auditInstalledStashForDangerousKeys`, which a test imports
* directly)
* - `list` (= old top-level `list`)
* - `remove` (= old top-level `remove`)
* - `update` (= old top-level `update`)
* - `show <name>` — NEW: a single-bundle detail view of the `list` payload.
*
* `add`/`list`/`remove`/`update` are pure relocations (payload and output
* shape unchanged: `SourceEntry`/`RemoveResponse`/`UpdateResponse` keep their
* bare `add`/`list`/`remove`/`update` output shape). `create` is a genuine
* verb rename (was `init`), so its output shape is renamed to `bundle-create`
* to match (the same convention S8 used for `propose` -> `proposal new`'s
* `proposal-new` shape). `show` is a new shape, `bundle-show`.
*/
import { defineGroupCommand, defineJsonCommand, output } from "../../cli/shared.js";
import { renameBundle } from "../../core/bundle-rename.js";
import { NotFoundError, TransientError, UsageError } from "../../core/errors.js";
import { appendEvent } from "../../core/events.js";
import { warn } from "../../core/warn.js";
import { akmTasksSync } from "../tasks/tasks.js";
import { addCommand } from "./add-cli.js";
import { akmInit } from "./init.js";
import { akmListSources, akmRemove, akmUpdate } from "./installed-stashes.js";
const VALID_SOURCE_KINDS = new Set(["filesystem", "git", "npm", "website"]);
function parseKindFilter(raw) {
if (!raw)
return undefined;
const kinds = raw.split(",").map((s) => s.trim());
for (const k of kinds) {
if (!VALID_SOURCE_KINDS.has(k)) {
throw new UsageError(`Invalid --kind value: "${k}". Expected one of: filesystem, git, npm, website`);
}
}
return kinds;
}
const createCommand = defineJsonCommand({
meta: {
name: "create",
description: "Initialize akm's working bundle directory and persist it in config",
},
args: {
dir: { type: "string", description: "Custom bundle directory path (default: ~/akm)" },
"set-default": {
type: "boolean",
description: "Make --dir the default bundle. Without this, `akm bundle create --dir X` scaffolds X but leaves your existing default bundle unchanged.",
default: false,
},
},
async run({ args }) {
const result = await akmInit({
dir: args.dir,
setDefault: args["set-default"],
});
output("bundle-create", result);
},
});
const listCommand = defineJsonCommand({
meta: { name: "list", description: "List configured bundles and their resolved source state" },
args: {
kind: {
type: "string",
description: "Filter by source provider (filesystem, git, npm, website). Comma-separated.",
},
},
async run({ args }) {
const kind = parseKindFilter(args.kind);
const result = await akmListSources({ kind });
output("list", result);
},
});
const showCommand = defineJsonCommand({
meta: { name: "show", description: "Show detail for a single configured bundle" },
args: {
name: { type: "positional", description: "Bundle name (as reported by `akm bundle list`)", required: true },
},
async run({ args }) {
const { sources } = await akmListSources();
const entry = sources.find((s) => s.name === args.name);
if (!entry) {
throw new NotFoundError(`No configured bundle named "${args.name}"`, "SOURCE_NOT_FOUND");
}
output("bundle-show", entry);
},
});
const removeCommand = defineJsonCommand({
meta: { name: "remove", description: "Remove a bundle by id, ref, path, URL, or name" },
args: {
target: { type: "positional", description: "Bundle to remove (id, ref, path, URL, or name)", required: true },
yes: { type: "boolean", alias: "y", description: "Skip confirmation prompt", default: false },
},
async run({ args }) {
const { confirmDestructive } = await import("../../cli/confirm.js");
const confirmed = await confirmDestructive(`Remove bundle "${args.target}"? This cannot be undone.`, {
yes: args.yes === true,
});
if (!confirmed) {
process.stderr.write("Aborted.\n");
return;
}
const result = await akmRemove({ target: args.target });
appendEvent({
eventType: "remove",
metadata: {
target: args.target,
ref: typeof result.removed?.ref === "string" ? result.removed.ref : null,
id: typeof result.removed?.id === "string" ? result.removed.id : null,
},
});
output("remove", result);
},
});
const updateCommand = defineJsonCommand({
meta: {
name: "update",
description: "Refresh one or all configured bundles and reconcile their index. Git refresh is explicit; schedule this command for automatic updates.",
},
args: {
target: { type: "positional", description: "Bundle to update (id or ref)", required: false },
all: { type: "boolean", description: "Update all configured bundles and report each outcome", default: false },
force: { type: "boolean", description: "Force fresh download even if version is unchanged", default: false },
"skip-if-locked": {
type: "boolean",
description: "Exit successfully when index/state database contention shows another akm operation is active",
default: false,
},
"allow-dangerous-env-keys": {
type: "boolean",
description: "Allow an update containing dangerous env keys (e.g. LD_PRELOAD, PATH). Use only after explicitly reviewing the staged bundle.",
default: false,
},
// F1/R-058: gates ONLY the rare branch where the resolved content
// directory moves and the previous `localRoot` is deleted — a normal
// refresh (the overwhelming majority of updates) deletes nothing and
// never consults this flag. Mirrors `remove`'s `-y/--yes`.
yes: {
type: "boolean",
alias: "y",
description: "Skip the confirmation prompt when an update needs to delete a previous install directory (because the resolved content location moved). No effect on a normal refresh, which deletes nothing.",
default: false,
},
},
async run({ args }) {
let result;
try {
result = await akmUpdate({
target: args.target,
all: args.all,
force: args.force,
yes: args.yes,
allowDangerousEnvKeys: args["allow-dangerous-env-keys"],
});
}
catch (error) {
const skippable = isSkippableBundleUpdateLock(error);
if (!args["skip-if-locked"] || !skippable)
throw error;
warn(`[bundle update] ${error.message}; skipping (--skip-if-locked)`);
output("update", { ok: true, skipped: { reason: "lock-held", code: error.code } });
return;
}
appendEvent({
eventType: "update",
metadata: {
target: args.target ?? null,
all: args.all === true,
force: args.force === true,
allowDangerousEnvKeys: args["allow-dangerous-env-keys"] === true,
processed: Array.isArray(result.processed)
? result.processed.length
: 0,
},
});
output("update", result);
},
});
const renameCommand = defineJsonCommand({
meta: {
name: "rename",
description: "Rename a configured bundle's key everywhere akm persists it (config, lock, index, and its own proposals/task history). Content inside the bundle that still spells the old ref is reported, not rewritten.",
},
args: {
old: { type: "positional", description: "Current bundle name", required: true },
new: { type: "positional", description: "New bundle name (must be a legal, unused slug)", required: true },
"dry-run": {
type: "boolean",
description: "Show the rename plan without writing anything",
default: false,
},
},
async run({ args }) {
const result = await renameBundle(args.old, args.new, { dryRun: args["dry-run"] }, {
syncTasks: (newId, backend) => akmTasksSync({ backend }, newId),
});
output("bundle-rename", result);
},
});
export function isSkippableBundleUpdateLock(error) {
return error instanceof TransientError && ["INDEX_DB_CONTENDED", "STATE_DB_CONTENDED"].includes(error.code);
}
export const bundleCommand = defineGroupCommand({
meta: {
name: "bundle",
description: "Create, add to, inspect, and manage your bundles (working bundle + configured sources)",
},
subCommands: {
create: createCommand,
add: addCommand,
list: listCommand,
show: showCommand,
remove: removeCommand,
update: updateCommand,
rename: renameCommand,
},
});