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
108 lines (107 loc) • 4.57 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/.
/**
* Tell "this path is not there" apart from "I am not allowed to look at it"
* (issue #791).
*
* # Why this exists
*
* `fs.existsSync()` answers `false` for BOTH cases: it swallows every error,
* so `ENOENT` (genuinely absent) and `EACCES` (present, unreadable) are
* indistinguishable. akm used `existsSync` as its "is there an index?" gate on
* the read path, so an index the caller could not read was reported as an index
* that did not exist — `akm search` and `akm curate` returned
* `hits: []` with the tip *"No search index available. Run 'akm index' to build
* one."* at exit 0, for a populated index sitting right there on disk.
*
* That is the worst possible shape for a failure. A non-zero exit is a problem
* the operator can see; an empty-but-successful result is a lie that a
* consuming agent will confidently relay to its user. In the report that
* prompted this module, exactly that happened: an agent told its user akm's
* "vector service is unavailable and its maintenance lock is read-only" — a
* story it invented to explain results that claimed to be fine.
*
* # Contract
*
* `absent` means the path (or a parent component) genuinely is not there, which
* is a legitimate first-run state every caller already handles. `inaccessible`
* means the path may well exist and this process cannot determine that or read
* it — a caller must NEVER degrade that to an empty-but-successful result.
*/
import fs from "node:fs";
function errnoOf(error) {
const code = error?.code;
return typeof code === "string" ? code : undefined;
}
/**
* Classify `target` as {@link PathAccess}.
*
* `ENOENT`/`ENOTDIR` are `absent` — the path cannot exist as named. Everything
* else that fails is `inaccessible`, carrying its errno: a permission error is
* the common case, but a symlink loop or an I/O error are equally "present as
* far as anyone knows, and unusable", and silently treating them as "no index"
* is the bug this module exists to prevent.
*
* Statting is not enough — a file can be `stat`-able through a searchable
* parent while being unreadable itself — so a successful stat is confirmed with
* an `R_OK` access check.
*/
export function classifyPathAccess(target) {
try {
fs.statSync(target);
}
catch (error) {
const code = errnoOf(error);
if (code === "ENOENT" || code === "ENOTDIR")
return { access: "absent" };
return { access: "inaccessible", ...(code ? { code } : {}) };
}
try {
fs.accessSync(target, fs.constants.R_OK);
}
catch (error) {
const code = errnoOf(error);
return { access: "inaccessible", ...(code ? { code } : {}) };
}
return { access: "present" };
}
/** True when the path is genuinely absent — the ordinary "not built yet" state. */
export function isPathAbsent(target) {
return classifyPathAccess(target).access === "absent";
}
/**
* A diagnostic line naming everything an operator needs to fix a permission
* problem without a second round trip: the path, the errno, the mode and owner
* of whatever akm *could* stat along the way, and the uid actually running.
*
* Deliberately best-effort — this runs on an error path, so a failure to gather
* detail must never mask the error being described.
*/
export function describeInaccessiblePath(target, code) {
const parts = [target];
if (code)
parts.push(`(${code})`);
try {
const stat = fs.statSync(target);
parts.push(`mode ${(stat.mode & 0o777).toString(8).padStart(3, "0")}, owner uid ${stat.uid}`);
}
catch {
// Cannot stat the file itself — describe the closest parent we CAN see,
// which is usually where the missing permission actually is.
const parent = target.slice(0, Math.max(0, target.lastIndexOf("/")));
if (parent) {
try {
const stat = fs.statSync(parent);
parts.push(`parent ${parent} is mode ${(stat.mode & 0o777).toString(8).padStart(3, "0")}, owner uid ${stat.uid}`);
}
catch {
// Nothing further to say.
}
}
}
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
if (uid !== undefined)
parts.push(`running as uid ${uid}`);
return parts.join("; ");
}