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
151 lines (150 loc) • 6.38 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/.
/**
* Npm-source stash provider.
*
* `sync()` resolves the npm package tarball, downloads it, verifies its
* integrity, extracts it securely (via `extractTarGzSecure`), detects the
* stash root inside the package, and applies any nested `akm.include`
* (package.json) configuration. Cache hits short-circuit the fetch.
*/
import fs from "node:fs";
import path from "node:path";
import { ConfigError, UsageError } from "../../core/errors.js";
import { getRegistryCacheDir } from "../../core/paths.js";
import { parseRegistryRef, resolveRegistryArtifact } from "../../registry/resolve.js";
import { applyAkmIncludeConfig, buildInstallCacheDir, computeFileHash, detectStashRoot, downloadArchive, isDirectory, } from "./provider-utils.js";
import { extractTarGzSecure, verifyArchiveIntegrity } from "./tar-utils.js";
/**
* NPM source provider — fetches a tarball from the npm registry and extracts
* it into a local cache. Implements the {@link SourceProvider} interface.
*
* The install-time pipeline (`syncNpmRef`) lives below as a standalone
* function used by `akm add` / `akm update`; that path produces a
* {@link SourceLockData} record for lockfile bookkeeping. The provider's own
* `sync()` is a void refresh (delegates to the install pipeline but discards
* the lock data, which is owned by `lockfile.ts`).
*/
class NpmSourceProvider {
kind = "npm";
name;
#config;
constructor(config) {
this.#config = config;
this.name = config.name ?? config.url ?? "npm";
}
path() {
if (this.#config.path)
return this.#config.path;
throw new ConfigError(`npm source "${this.name}" has no resolved content path — run \`akm bundle update\` to sync it before indexing.`);
}
async sync(options) {
const ref = npmRefFromConfig(this.#config);
await syncNpmRef(ref, { force: options?.force });
}
}
function npmRefFromConfig(config) {
const candidate = config.path;
if (typeof candidate !== "string" || !candidate) {
throw new UsageError('npm source entry must include a package ref (e.g. "npm:my-pkg@1.2.3")');
}
return candidate.startsWith("npm:") ? candidate : `npm:${candidate}`;
}
/**
* Fetch and extract an npm tarball, returning a populated `SourceLockData`.
*
* Materializes npm sources by:
* - resolving the artifact URL and integrity from the npm registry
* - reuse cached extraction when present
* - download, verify, extract securely, then detect the stash root
* - honour `akm.include` (package.json) filters
*/
export async function syncNpmRef(ref, options) {
const parsed = parseRegistryRef(ref);
if (parsed.source !== "npm") {
throw new UsageError(`syncNpmRef requires an npm: ref, got "${ref}"`);
}
return doSyncNpm(parsed, options);
}
async function doSyncNpm(parsed, options) {
const resolved = await resolveRegistryArtifact(parsed);
const syncedAt = (options?.now ?? new Date()).toISOString();
const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheDir();
const cacheDir = buildInstallCacheDir(cacheRootDir, resolved.source, resolved.id, resolved.resolvedVersion ?? resolved.resolvedRevision);
const archivePath = path.join(cacheDir, "artifact.tar.gz");
const extractedDir = path.join(cacheDir, "extracted");
// Cache hit: extracted dir already valid → reuse it
if (!options?.force && isDirectory(extractedDir)) {
try {
const cachedStashRoot = detectStashRoot(extractedDir);
if (cachedStashRoot) {
const integrity = fs.existsSync(archivePath) ? await computeFileHash(archivePath) : undefined;
return {
id: resolved.id,
source: resolved.source,
ref: resolved.ref,
artifactUrl: resolved.artifactUrl,
resolvedVersion: resolved.resolvedVersion,
resolvedRevision: resolved.resolvedRevision,
contentDir: cachedStashRoot,
cacheDir,
extractedDir,
integrity,
writable: options?.writable,
syncedAt,
};
}
}
catch {
// Cache invalid, re-download
}
}
fs.mkdirSync(cacheDir, { recursive: true });
let integrity;
let provisionalBundleRoot;
let installRoot;
let stashRoot;
try {
await downloadArchive(resolved.artifactUrl, archivePath);
verifyArchiveIntegrity(archivePath, resolved.resolvedRevision, resolved.source);
integrity = await computeFileHash(archivePath);
extractTarGzSecure(archivePath, extractedDir);
const detectedProvisionalBundleRoot = detectStashRoot(extractedDir);
if (!detectedProvisionalBundleRoot) {
throw new UsageError(`Unable to detect a stash root in extracted npm package: ${resolved.ref}`);
}
provisionalBundleRoot = detectedProvisionalBundleRoot;
installRoot = applyAkmIncludeConfig(provisionalBundleRoot, cacheDir, extractedDir) ?? provisionalBundleRoot;
const detectedStashRoot = detectStashRoot(installRoot);
if (!detectedStashRoot) {
throw new UsageError(`Unable to detect a stash root after applying akm.include (package.json) configuration for npm package: ${resolved.ref}`);
}
stashRoot = detectedStashRoot;
}
catch (err) {
// Clean up so stale or partial extractions don't cause false cache hits.
try {
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,
integrity,
writable: options?.writable,
syncedAt,
};
}
export { NpmSourceProvider };