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
131 lines (130 loc) • 4.62 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 { spawnSync } from "node:child_process";
import fs from "node:fs";
import { ConfigError } from "../../core/errors.js";
/**
* Default exec strategy: run commands synchronously via {@link spawnCommand}.
*
* This is the shared default for every task backend's `exec` seam. Backends
* that need extra fields (e.g. launchd's `uid()`) spread this and add them.
*/
export function nodeExec() {
return {
run(args) {
return spawnCommand(args);
},
};
}
/**
* Default filesystem strategy for the shared subset of the backend `fs` seam
* (`writeFile` + `ensureDir`), backed by `node:fs`.
*
* Backends spread this and add their own members. Note `removeFile` is NOT
* shared here: the launchd and schtasks defaults differ observably (schtasks
* swallows errors; launchd does not), so each keeps its own.
*/
export function nodeFs() {
return {
writeFile(file, content) {
fs.writeFileSync(file, content, { encoding: "utf8" });
},
ensureDir(dir) {
fs.mkdirSync(dir, { recursive: true });
},
};
}
/**
* Run a command synchronously, normalizing null results to safe defaults.
* args[0] is the binary; args[1..] are its arguments.
*/
export function spawnCommand(args) {
const [bin, ...rest] = args;
const r = spawnSync(bin, rest);
return {
status: r.status ?? 1,
stdout: decodeCommandOutput(r.stdout),
stderr: decodeCommandOutput(r.stderr),
};
}
/**
* Throw a `ConfigError` (code `INVALID_CONFIG_FILE`) when `result` doesn't
* satisfy `isOk` (default: `status === 0`); otherwise return `result`
* unchanged: the "run a native scheduler command, throw a formatted error on
* a bad exit" idiom shared by the cron/launchd/schtasks backends.
*/
export function throwIfNotOk(result, options) {
const ok = options.isOk ? options.isOk(result) : result.status === 0;
if (!ok) {
throw new ConfigError(options.message(result), "INVALID_CONFIG_FILE", options.hint);
}
return result;
}
/** {@link throwIfNotOk} for the common case of running `args` via `exec` first. */
export function runOrThrow(exec, args, options) {
return throwIfNotOk(exec.run(args), options);
}
/** Decode native command output, including the UTF-16 XML emitted by schtasks. */
export function decodeCommandOutput(output) {
if (output === null || output === undefined)
return "";
if (typeof output === "string")
return output.replace(/^\uFEFF/, "");
if (output.length === 0)
return "";
if (output[0] === 0xef && output[1] === 0xbb && output[2] === 0xbf) {
return output.toString("utf8", 3);
}
if (output[0] === 0xff && output[1] === 0xfe) {
return output.subarray(2).toString("utf16le");
}
if (output[0] === 0xfe && output[1] === 0xff) {
return decodeUtf16Be(output.subarray(2));
}
const pairs = Math.floor(output.length / 2);
if (pairs > 1) {
let evenNuls = 0;
let oddNuls = 0;
for (let i = 0; i < pairs * 2; i += 2) {
if (output[i] === 0)
evenNuls += 1;
if (output[i + 1] === 0)
oddNuls += 1;
}
if (oddNuls / pairs > 0.6)
return output.toString("utf16le");
if (evenNuls / pairs > 0.6)
return decodeUtf16Be(output);
}
return output.toString("utf8");
}
/** Return XML with the declaration required by Task Scheduler's UTF-16 input. */
export function normalizeXmlForUtf16File(xml) {
const source = xml.replace(/^\uFEFF/, "");
const declaration = '<?xml version="1.0" encoding="UTF-16"?>';
if (/^<\?xml\b[^?]*\?>/i.test(source)) {
return source.replace(/^<\?xml\b[^?]*\?>/i, declaration);
}
return `${declaration}\n${source}`;
}
function decodeUtf16Be(output) {
const evenLength = output.length - (output.length % 2);
const swapped = Buffer.allocUnsafe(evenLength);
for (let i = 0; i < evenLength; i += 2) {
swapped[i] = output[i + 1];
swapped[i + 1] = output[i];
}
return swapped.toString("utf16le");
}
/**
* Escape a string for safe embedding in an XML attribute or text node.
*/
export function escapeXml(s) {
return s
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}