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
132 lines (129 loc) • 5.67 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/.
import fs from "node:fs";
import path from "node:path";
import { getDirname } from "../../runtime.js";
const SKELETON_DIR = path.join(getDirname(import.meta.url), "../../assets/stash-skeleton");
/**
* Copy the default stash skeleton into a stash directory.
*
* The skeleton tree under src/assets/stash-skeleton/ is mirrored **recursively**
* into the stash root, preserving relative subpaths (e.g.
* `facts/conventions/assets/skill.md` lands at the matching stash subpath).
* Each file is written only if the destination does not already exist — existing
* (possibly user-edited) files are never overwritten. Intermediate directories
* are created as needed.
*
* Idempotent and absent-only: running it again on an existing stash backfills
* any skeleton files that are missing without clobbering present ones. Non-fatal:
* if the skeleton directory is missing or a copy fails the caller continues.
*/
export function copyStashSkeleton(stashDir) {
copySkeletonDir(SKELETON_DIR, stashDir);
}
/** Recursively mirror `srcDir` into `destDir`, writing files only when absent. */
function copySkeletonDir(srcDir, destDir) {
let entries;
try {
entries = fs.readdirSync(srcDir, { withFileTypes: true });
}
catch {
return;
}
for (const entry of entries) {
const src = path.join(srcDir, entry.name);
const dest = path.join(destDir, entry.name);
if (entry.isDirectory()) {
copySkeletonDir(src, dest);
continue;
}
if (fs.existsSync(dest))
continue;
try {
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.copyFileSync(src, dest);
}
catch {
// Non-fatal — stash is usable without skeleton files
}
}
}
/**
* Scaffold the optional `.meta/index.md` orientation doc for the stash
* `.meta/` convention. Written only when absent — an existing `.meta/index.md`
* is never overwritten. Non-fatal: a stash works fine without it.
*
* `.meta/` is a dot-directory, so the indexer skips it; the template is
* written here (rather than shipped under `src/assets/`) because the
* build-time asset glob excludes dotfiles.
*/
export function scaffoldStashMeta(stashDir) {
const metaDir = path.join(stashDir, ".meta");
const indexPath = path.join(metaDir, "index.md");
if (fs.existsSync(indexPath))
return;
try {
fs.mkdirSync(metaDir, { recursive: true });
fs.writeFileSync(indexPath, STASH_META_INDEX_TEMPLATE);
}
catch {
// Non-fatal — stash is usable without the .meta orientation doc
}
}
/** Marks the akm-authored block in a stash `.gitignore` (idempotency anchor). */
const STASH_GITIGNORE_MARKER = "# akm: keep secret material out of git by default";
/** Documented opt-out for private bundles that deliberately version env/ and secrets/. */
export const STASH_GITIGNORE_TRACK_SECRETS_MARKER = "# akm: intentionally track env and secrets";
const STASH_GITIGNORE_BLOCK = [
STASH_GITIGNORE_MARKER,
"# env/ and secrets/ assets hold tokens and keys. They are ignored by default",
"# so `git push` can never leak them. To version a specific one (e.g. for a",
"# private-remote backup), un-ignore its path below once you accept the risk.",
"env/",
"secrets/",
"",
].join("\n");
/**
* Ensure the stash `.gitignore` keeps `env/` and `secrets/` out of git by
* default (08-F1: the v0.8.0 `vaults/` → `env/`+`secrets/` migration never
* carried the ignore rules forward, and init scaffolded none).
*
* Idempotent + non-clobbering: creates the file when absent, appends the akm
* block when the file exists but lacks it (preserving the user's own rules),
* and no-ops once the marker is present. A private bundle that deliberately
* versions all secret material can add `# akm: intentionally track env and
* secrets`; that explicit marker suppresses this helper entirely (#981).
*/
export function ensureStashGitignore(stashDir) {
try {
const gitignorePath = path.join(stashDir, ".gitignore");
const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
if (existing.includes(STASH_GITIGNORE_MARKER) || existing.includes(STASH_GITIGNORE_TRACK_SECRETS_MARKER))
return;
const gap = existing.length === 0 ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
fs.writeFileSync(gitignorePath, `${existing}${gap}${STASH_GITIGNORE_BLOCK}`);
}
catch {
// Non-fatal — the stash is usable without the ignore scaffold.
}
}
const STASH_META_INDEX_TEMPLATE = `---
# Optional, human-authored orientation for this stash. Not indexed; surfaced
# on demand via \`akm show meta\` (this file) or \`akm show <stash>//meta\`.
# Every field is optional — delete what you don't need.
purpose:
- Describe what this stash is for.
entry_points:
# Refs an agent should start from, e.g. skills/code-review, workflows/ship-release
conventions:
# House rules an agent should follow when working in this stash.
maintainer:
---
# About this stash
Replace this with a short orientation for agents and humans: what lives here,
where to start, and the conventions to follow.
Extend the \`.meta/\` directory with more docs as needed — \`.meta/about.md\`,
\`.meta/conventions.md\`, \`.meta/license\` — and read any of them with
\`akm show meta:<name>\` (or \`akm show <stash>//meta:<name>\`).
`;