UNPKG

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

95 lines (94 loc) • 4.45 kB
// 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 crypto from "node:crypto"; import path from "node:path"; import { isBundleSlug } from "./asset/asset-ref.js"; import { UsageError } from "./errors.js"; /** Deterministic, filesystem-safe bundle slug from a source path. */ export function slugForPath(sourcePath) { const resolved = path.resolve(sourcePath); const base = slugify(path.basename(resolved)); if (base.length > 0) return base; return `bundle-${shortHash(resolved)}`; } /** * Bundle slug from the package/repo name a registry install id names * (`npm:@scope/pkg` → `pkg`, `github:owner/repo` → `repo`, * `git:https://host/owner/repo` → `repo`); empty when it names none. */ export function slugForRegistryId(registryId) { const locator = registryId.slice(registryId.indexOf(":") + 1); return slugify(locator.split(/[/:]/).filter(Boolean).pop() ?? ""); } function slugify(name) { return name .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); } /** * Enforce the `--name` contract on every add path (D6): an explicit name is * not a silent hint that may fall back to a derived id — it is a request * that either succeeds exactly or fails loudly, before anything is written. * * `existingKeyForThisInstall` is the bundle key this exact source (path/URL/ * registry id) is already configured under, if any. Re-adding that same * source under a *different* explicit name is rejected in favor of * `akm bundle rename`, which is the one command allowed to change a bundle's * key. A caller with no existing install for this source (the common case) * omits it. */ export function validateExplicitBundleName(bundles, name, existingKeyForThisInstall) { if (!isBundleSlug(name)) { throw new UsageError(`"${name}" is not a legal bundle name: it must not contain whitespace or the characters ":" "." "#" "/".`, "INVALID_FLAG_VALUE"); } if (existingKeyForThisInstall !== undefined && name !== existingKeyForThisInstall) { throw new UsageError(`This source is already installed as bundle "${existingKeyForThisInstall}". Re-adding it under a ` + `different name is not supported — run \`akm bundle rename ${existingKeyForThisInstall} ${name}\` instead.`, "INVALID_FLAG_VALUE"); } if (Object.hasOwn(bundles, name) && name !== existingKeyForThisInstall) { throw new UsageError(`Bundle name "${name}" already exists. Choose another name, or run ` + `\`akm bundle rename ${name} <new-name>\` first.`, "INVALID_FLAG_VALUE"); } } /** Derive one batch-unique bundle id. */ export function deriveBundleId(registryId, sourcePath, usedIds) { const preferred = registryId && registryId.length > 0 && isBundleSlug(registryId) ? registryId : slugForPath(sourcePath); const id = ensureUniqueId(preferred, sourcePath, usedIds); usedIds.add(id); return id; } /** Derive an ordered batch while reserving every explicit configured bundle id. */ export function deriveBundleIds(sources) { const usedIds = new Set(); const reservedIds = new Set(sources.flatMap((source) => (source.registryId && isBundleSlug(source.registryId) ? [source.registryId] : []))); return sources.map((source) => { const id = source.registryId && isBundleSlug(source.registryId) ? deriveBundleId(source.registryId, source.path, usedIds) : deriveBundleId(undefined, source.path, new Set([...usedIds, ...reservedIds])); usedIds.add(id); return id; }); } function ensureUniqueId(preferred, sourcePath, used) { if (!used.has(preferred)) return preferred; const suffixed = `${preferred}-${shortHash(path.resolve(sourcePath))}`; if (!used.has(suffixed)) return suffixed; let n = 2; while (used.has(`${suffixed}-${n}`)) n++; return `${suffixed}-${n}`; } /** * First 8 hex chars of `input`'s sha256 — deterministic and short enough to * suffix a slug or a path key. Exported so callers needing the same * short-hash-of-a-resolved-path primitive (e.g. `getStashStateKey` in * `paths.ts`) don't grow their own duplicate. */ export function shortHash(input) { return crypto.createHash("sha256").update(input).digest("hex").slice(0, 8); }