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

151 lines (150 loc) • 6.38 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/. /** * 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 };