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
119 lines (118 loc) • 4.23 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/.
/**
* Flexible asset resolution system.
*
* Provides a rich FileContext built once per file during walking, plus a
* matcher/renderer registry that decouples asset classification from rendering.
*/
import fs from "node:fs";
import path from "node:path";
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
import { toPosix } from "../../core/common.js";
/**
* Build a FileContext from a stash root and an absolute file path.
*
* Path-derived fields are computed eagerly. The content, frontmatter, and
* stat getters use lazy caching so the file is only read from disk when
* (and if) a matcher or renderer actually needs it.
*/
export function buildFileContext(stashRoot, absPath) {
const relPath = toPosix(path.relative(stashRoot, absPath));
const ext = path.extname(absPath).toLowerCase();
const fileName = path.basename(absPath);
const parentDirAbs = path.dirname(absPath);
const parentDir = path.basename(parentDirAbs);
// Compute ancestor directory segments from the POSIX relPath's directory portion.
// For "scripts/azure/deploy/run.sh" the dir portion is "scripts/azure/deploy"
// which splits into ["scripts", "azure", "deploy"].
const relDir = toPosix(path.dirname(relPath));
const ancestorDirs = relDir === "." ? [] : relDir.split("/").filter((seg) => seg.length > 0);
// Lazy caches
let cachedContent;
let cachedFrontmatter;
let frontmatterComputed = false;
let cachedStat;
return {
absPath,
relPath,
ext,
fileName,
parentDir,
parentDirAbs,
ancestorDirs,
stashRoot,
content() {
if (cachedContent === undefined) {
cachedContent = fs.readFileSync(absPath, "utf8");
}
return cachedContent;
},
frontmatter() {
if (!frontmatterComputed) {
const raw = this.content();
const parsed = parseFrontmatter(raw);
cachedFrontmatter = Object.keys(parsed.data).length > 0 ? parsed.data : null;
frontmatterComputed = true;
}
return cachedFrontmatter ?? null;
},
stat() {
if (cachedStat === undefined) {
cachedStat = fs.statSync(absPath);
}
return cachedStat;
},
};
}
// ── Registry ─────────────────────────────────────────────────────────────────
/** Renderer lookup by name. */
const renderers = new Map();
/**
* Ensure that all built-in indexer contributors are registered.
*
* Delegates to the single `initIndexer()` composition root (see
* `src/indexer/init.ts`). Imported dynamically to keep this a lazy gate and to
* avoid a static import cycle (init -> renderers -> file-context). Called on
* first use of getRenderer/getAllRenderers; idempotent.
*/
async function ensureBuiltinsRegistered() {
const { initIndexer } = await import("../init.js");
await initIndexer();
}
/**
* Register an AssetRenderer.
*
* If a renderer with the same name already exists it is silently replaced.
*/
export function registerRenderer(renderer) {
renderers.set(renderer.name, renderer);
}
/**
* Look up a renderer by name.
*/
export async function getRenderer(name) {
await ensureBuiltinsRegistered();
return renderers.get(name);
}
/**
* Return all registered renderers (snapshot, safe to iterate).
*/
export async function getAllRenderers() {
await ensureBuiltinsRegistered();
return Array.from(renderers.values());
}
/**
* Build a RenderContext by merging a FileContext with its winning MatchResult
* and the list of stash search paths.
*/
export function buildRenderContext(ctx, match, stashDirs, origin, defaultBundle) {
return {
...ctx,
matchResult: match,
stashDirs,
origin,
defaultBundle,
};
}