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
346 lines (345 loc) • 14.9 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/.
/**
* Runtime helpers that derive provider-ready and {@link ConfiguredSource}
* values from the current bundle map in an {@link AkmConfig}.
*/
import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { ConfigError } from "../errors.js";
/** The current one-component-per-bundle configuration entry, if present. */
export function bundleComponentConfig(bundle) {
if (!bundle?.components)
return undefined;
const components = Object.values(bundle.components);
if (components.length !== 1) {
throw new ConfigError("A bundle components map must contain exactly one component.", "INVALID_CONFIG_FILE");
}
return components[0];
}
/**
* Convert the current `bundles` map into its ordered provider projection:
* `defaultBundle` first, then map insertion order. Each entry's `name` is its
* bundle key. Returns `undefined` when no bundles map is configured.
*/
/**
* A bundle's true identity: its configured `path` plus its component's
* `root` (default `"."`), fully resolved. Two bundle entries whose bare
* `path` differs (relative vs. absolute, trailing slash, `~` vs. expanded)
* can still resolve to this same directory — this is the identity akm
* compares before registering or reconciling a bundle (issue #870).
*/
export function bundleContentRoot(entryPath, componentRoot) {
return path.resolve(entryPath, componentRoot ?? ".");
}
/**
* Physical identity for an already materialized filesystem bundle.
*
* Configuration may spell the same directory through relative paths or
* symbolic links. Those spellings are not distinct sources: treating them as
* distinct would make ownership, default-source trust, and scheduler grants
* depend on syntax instead of the directory the OS will actually read.
*/
export function bundlePhysicalContentRoot(entryPath, componentRoot) {
const resolved = bundleContentRoot(entryPath, componentRoot);
try {
return fs.realpathSync.native(resolved);
}
catch (cause) {
// Cache-backed sources can be configured before they are materialized.
// Their source descriptor remains the identity until a real path exists.
if (cause.code === "ENOENT")
return resolved;
throw new ConfigError(`Unable to resolve physical bundle root ${JSON.stringify(resolved)}: ${cause instanceof Error ? cause.message : String(cause)}`, "INVALID_CONFIG_FILE");
}
}
/** Whether a configured bundle participates in reads, writes, and execution. */
export function isBundleEnabled(config, bundleId) {
const bundle = config.bundles?.[bundleId];
return bundle !== undefined && bundle.enabled !== false;
}
/**
* Stable identity of the origin currently installed under a bundle id.
*
* This deliberately excludes mutable policy (`enabled`, `writable`,
* credentials, adapter selection) and bundle contents. Ordinary updates and
* adapter auto-detection for the same origin keep their grant; changing the
* locator or component root does not.
*/
export function bundleSourceId(config, bundleId) {
const bundle = config.bundles?.[bundleId];
if (!bundle) {
throw new ConfigError(`Bundle ${JSON.stringify(bundleId)} is not configured.`, "INVALID_CONFIG_FILE");
}
const component = bundleComponentConfig(bundle);
let source;
if (bundle.path !== undefined) {
source = { kind: "filesystem", locator: bundlePhysicalContentRoot(bundle.path, component?.root) };
}
else if (bundle.git !== undefined) {
source = { kind: "git", locator: normalizeInstalledGitRef("git", bundle.git) };
}
else if (bundle.website !== undefined) {
// Crawl/refresh settings are mutable fetch policy, not source identity.
// Changing them must not silently revoke a user's scheduling decision for
// the same website origin.
source = { kind: "website", url: bundle.website.url };
}
else if (bundle.npm !== undefined) {
source = { kind: "npm", locator: bundle.npm };
}
else {
throw new ConfigError(`Bundle ${JSON.stringify(bundleId)} has no source descriptor.`, "INVALID_CONFIG_FILE");
}
return hashBundleSourceIdentity({
version: 1,
source,
registryId: bundle.registryId ?? null,
componentRoot: component?.root ?? ".",
});
}
/** Identity for an environment-only filesystem bundle not persisted in config. */
export function filesystemBundleSourceId(contentRoot) {
return hashBundleSourceIdentity({
version: 1,
source: { kind: "filesystem", locator: bundlePhysicalContentRoot(contentRoot) },
registryId: null,
componentRoot: ".",
});
}
function hashBundleSourceIdentity(identity) {
return `sha256:${createHash("sha256")
.update("akm.bundle-source\0v1\0")
.update(JSON.stringify(identity))
.digest("hex")}`;
}
/**
* The resolved primary stash path — the `defaultBundle`'s filesystem `path`
* (spec §10.1) — or `undefined` when no filesystem primary is configured.
*/
export function primaryBundlePath(config) {
const bundles = config.bundles;
const key = config.defaultBundle;
if (!bundles || !key)
return undefined;
const entry = bundles[key];
if (!entry || typeof entry.path !== "string" || entry.path.length === 0)
return undefined;
const componentRoot = bundleComponentConfig(entry)?.root;
const bundleRoot = path.resolve(entry.path);
if (!componentRoot || componentRoot === ".")
return bundleRoot;
const resolved = path.resolve(bundleRoot, componentRoot);
const relative = path.relative(bundleRoot, resolved);
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
throw new ConfigError(`Component root "${componentRoot}" escapes bundle "${key}".`, "INVALID_CONFIG_FILE");
}
return resolved;
}
/**
* Every configured filesystem bundle's id paired with its resolved content
* root. Used to detect two bundle ids that resolve to the same directory
* (issue #870) and to find the id that already owns a given root before a
* new one is registered.
*/
export function bundleContentRoots(config) {
const bundles = config.bundles ?? {};
const out = [];
for (const [id, entry] of Object.entries(bundles)) {
if (typeof entry.path !== "string" || entry.path.length === 0)
continue;
out.push({ id, contentRoot: bundlePhysicalContentRoot(entry.path, bundleComponentConfig(entry)?.root) });
}
return out;
}
/** The bundle id whose resolved content root already matches `resolvedContentRoot`, if any. */
export function bundleKeyForContentRoot(config, resolvedContentRoot) {
let physical = path.resolve(resolvedContentRoot);
try {
physical = fs.realpathSync.native(physical);
}
catch {
// Compare the unresolved absolute locator when the candidate is not yet materialized.
}
return bundleContentRoots(config).find((entry) => entry.contentRoot === physical)?.id;
}
export function bundlesToSourceEntries(config) {
const bundles = config.bundles;
if (!bundles)
return undefined;
const keys = Object.keys(bundles);
const defaultKey = config.defaultBundle && config.defaultBundle in bundles ? config.defaultBundle : undefined;
const ordered = defaultKey ? [defaultKey, ...keys.filter((k) => k !== defaultKey)] : keys;
const entries = [];
for (const key of ordered) {
const entry = bundleEntryToSourceEntry(key, bundles[key], key === defaultKey);
if (entry)
entries.push(entry);
}
return entries;
}
/** Map one `bundles.<key>` entry to a runtime {@link SourceConfigEntry}. */
export function bundleEntryToSourceEntry(key, bundle, isPrimary = false) {
const base = {
name: key,
...(bundle.writable !== undefined ? { writable: bundle.writable } : {}),
...(bundle.credential !== undefined ? { credential: bundle.credential } : {}),
...(bundle.enabled !== undefined ? { enabled: bundle.enabled } : {}),
...(isPrimary ? { primary: true } : {}),
};
if (typeof bundle.path === "string" && bundle.path.length > 0) {
return { type: "filesystem", path: bundle.path, ...base };
}
if (typeof bundle.git === "string" && bundle.git.length > 0) {
return { type: "git", url: normalizeInstalledGitRef("", bundle.git), ...base };
}
if (bundle.website && typeof bundle.website.url === "string") {
// All non-`url` website-descriptor keys become provider options.
const { url, ...rest } = bundle.website;
return {
type: "website",
url,
...(Object.keys(rest).length > 0 ? { options: rest } : {}),
...base,
};
}
if (typeof bundle.npm === "string" && bundle.npm.length > 0) {
// Today's npm source carries the package spec in `path` (see parseSourceSpec).
return { type: "npm", path: bundle.npm, ...base };
}
return undefined;
}
/**
* Desired 0.9.0 bundle descriptor for a registry-installed source (spec §10.1).
* Maps the install source kind onto the ONE source descriptor a bundle entry
* carries: git/github → a provider-ready clone URL, npm → `{ npm: ref }`,
* everything else (local/filesystem) → `{ path: stashRoot }`.
*
* CRITICAL (spec §10.2:453): the materialized cache root NEVER appears in the
* descriptor for a git/npm bundle — the desired config carries only the source
* descriptor; the install locator stays in `registryId` and the resolved root
* belongs exclusively in the lock's `localRoot`. Callers layer
* `registryId`/`writable` onto the result.
*/
export function installedSourceDescriptor(source, ref, stashRoot) {
switch (source) {
case "git":
case "github":
if (ref)
return { git: normalizeInstalledGitRef(source, ref) };
break;
case "npm":
if (ref)
return { npm: ref };
break;
default:
break;
}
// local/filesystem installs reference a real on-disk path (no package-manager
// cache to re-materialize), so the resolved root IS the desired path.
return { path: stashRoot };
}
function normalizeInstalledGitRef(source, ref) {
if (ref.startsWith("git+"))
return ref.slice(4);
const isGithubShorthand = ref.startsWith("github:") || (source === "github" && /^[^/:#]+\/[^/#]+(?:#.+)?$/.test(ref));
if (!isGithubShorthand)
return ref;
const body = ref.startsWith("github:") ? ref.slice("github:".length) : ref;
const fragmentAt = body.indexOf("#");
const repository = fragmentAt >= 0 ? body.slice(0, fragmentAt) : body;
const requestedRef = fragmentAt >= 0 ? body.slice(fragmentAt + 1) : "";
const cloneUrl = `https://github.com/${repository.replace(/\.git$/i, "")}`;
return requestedRef ? `${cloneUrl}/tree/${requestedRef}` : cloneUrl;
}
/**
* Produce a stable internal identifier for a provider projection. Current
* bundle-derived entries always carry `name`; the hash protects manually
* constructed internal values.
*/
function deriveBundleName(entry) {
if (entry.name)
return entry.name;
const seed = JSON.stringify({
type: entry.type,
path: entry.path ?? null,
url: entry.url ?? null,
});
const hash = createHash("sha256").update(seed).digest("hex").slice(0, 8);
return `${entry.type}-${hash}`;
}
/**
* Convert a provider-ready {@link SourceConfigEntry} into the runtime
* {@link SourceSpec} discriminated union. Returns `undefined` when the entry
* is missing the fields its provider type requires (e.g. a `filesystem`
* entry with no `path`); callers should drop or warn for those.
*/
export function parseSourceSpec(entry) {
switch (entry.type) {
case "filesystem":
return entry.path ? { type: "filesystem", path: entry.path } : undefined;
case "git":
return entry.url ? { type: "git", url: entry.url } : undefined;
case "website":
return entry.url
? {
type: "website",
url: entry.url,
...(typeof entry.options?.maxPages === "number" ? { maxPages: entry.options.maxPages } : {}),
}
: undefined;
case "npm":
return entry.path ? { type: "npm", package: entry.path } : undefined;
default:
return undefined;
}
}
/**
* Build the full ordered list of runtime {@link ConfiguredSource} values from a
* loaded {@link AkmConfig}, resolved from `bundles` + `defaultBundle` (spec
* §10.1): the `defaultBundle` (primary) first, then map insertion order. The
* retired `stashDir`/`sources[]`/`installed[]` trio is never read or normalized.
*
* Entries with `enabled: false` are still emitted — callers decide whether to
* honour the flag. Entries that fail {@link parseSourceSpec} drop silently.
* Returns `[]` when no bundles are configured.
*/
export function resolveConfiguredSources(config) {
const bundleEntries = bundlesToSourceEntries(config);
if (!bundleEntries)
return [];
const out = [];
for (const persisted of bundleEntries) {
const runtime = toConfiguredSource(persisted, persisted.primary === true);
if (runtime) {
const component = bundleComponentConfig(config.bundles?.[persisted.name ?? ""]);
out.push({
...runtime,
...(component?.adapter ? { adapterId: component.adapter } : {}),
...(component?.root ? { componentRoot: component.root } : {}),
...(component?.writable !== undefined ? { writable: component.writable } : {}),
});
}
}
return out;
}
/** Active sources in the same deterministic order as `resolveConfiguredSources`. */
export function resolveActiveConfiguredSources(config) {
return resolveConfiguredSources(config).filter((source) => source.enabled !== false);
}
function toConfiguredSource(persisted, isPrimary) {
const source = parseSourceSpec(persisted);
if (!source)
return undefined;
return {
name: deriveBundleName(persisted),
type: persisted.type,
source,
...(persisted.enabled !== undefined ? { enabled: persisted.enabled } : {}),
...(persisted.writable !== undefined ? { writable: persisted.writable } : {}),
...(isPrimary || persisted.primary ? { primary: true } : {}),
...(persisted.options ? { options: persisted.options } : {}),
};
}