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

131 lines (130 loc) • 4.62 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/. 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, "&amp;") .replace(/</g, "&lt;") .replace(/>/g, "&gt;") .replace(/"/g, "&quot;") .replace(/'/g, "&apos;"); }