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

200 lines (193 loc) • 7.53 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 fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { placementTypes } from "../core/asset/asset-placement.js"; const FLAG_VALUES = { "--format": [{ values: ["json", "jsonl", "yaml", "text", "md", "html"] }], "--detail": [{ values: ["brief", "normal", "full"] }], "--shape": [{ values: ["human", "agent", "summary"] }], "--type": [{ values: () => [...placementTypes(), "any"] }], "--shell": [{ values: ["bash"] }], "--from": [{ paths: ["akm search", "akm curate"], values: ["local", "registry", "all"] }], }; function resolveRuleValues(rule) { return typeof rule.values === "function" ? rule.values() : rule.values; } /** * Build the `${prev}`-case body for one flag. When the flag has no * path-scoped rules, every command shares one value set (unchanged from * before R-052a). When it does (currently only `--from`), nest a * `${cmd_path}` match inside the flag's case so the suggestion depends on * which command is being completed — with a `*)` fallback to any * unscoped/global rule declared alongside the scoped ones, or no suggestion * at all when every rule for that flag is scoped. */ function buildFlagValueCaseBody(rules) { const scoped = rules.filter((rule) => rule.paths && rule.paths.length > 0); const global = rules.find((rule) => !rule.paths || rule.paths.length === 0); if (scoped.length === 0) { return ` COMPREPLY=( $(compgen -W "${resolveRuleValues(global).join(" ")}" -- "\${cur}") )`; } const branches = scoped .map((rule) => ` ${rule.paths.map((p) => `"${p}"`).join("|")}) COMPREPLY=( $(compgen -W "${resolveRuleValues(rule).join(" ")}" -- "\${cur}") ) ;;`) .join("\n"); const fallback = global ? ` *) COMPREPLY=( $(compgen -W "${resolveRuleValues(global).join(" ")}" -- "\${cur}") ) ;;` : ""; return ` case "\${cmd_path}" in ${branches} ${fallback} esac`; } function walkCommandTree(cmd, parentPath = "") { const name = cmd.meta?.name ?? ""; const currentPath = parentPath ? `${parentPath} ${name}` : name; const result = []; // `meta.hidden` (e.g. `migrate`, S11) is excluded from completion the same // way citty's own `renderUsage` excludes it from a rendered COMMANDS list — // a hidden command still runs, it just isn't suggested. const visibleSubEntries = Object.entries((cmd.subCommands ?? {})).filter(([, sub]) => !sub.meta?.hidden); const subcommands = visibleSubEntries.map(([key]) => key); const flags = []; const valueFlags = []; if (cmd.args) { for (const [flagName, arg] of Object.entries(cmd.args)) { if (arg.type === "positional") continue; flags.push(`--${flagName}`); if (arg.type === "boolean" && arg.default === true) flags.push(`--no-${flagName}`); if (arg.type !== "boolean") valueFlags.push(`--${flagName}`); } } result.push({ path: currentPath, subcommands, flags, valueFlags }); for (const [, sub] of visibleSubEntries) { result.push(...walkCommandTree(sub, currentPath)); } return result; } // ── Bash completion script generator ───────────────────────────────────────── export function generateBashCompletions(cmd) { const commands = walkCommandTree(cmd); const rootName = cmd.meta?.name ?? "akm"; // Collect global flags from root command const rootInfo = commands.find((c) => c.path === rootName); const globalFlags = rootInfo?.flags ?? []; const valueFlags = [...new Set(commands.flatMap((info) => info.valueFlags))]; // Build the case blocks for subcommand completion const caseBlocks = []; for (const info of commands) { const allFlags = [...new Set([...info.flags, ...globalFlags])]; if (info.subcommands.length > 0 || allFlags.length > 0) { const matchPath = info.path; const subcmdStr = info.subcommands.join(" "); const flagStr = allFlags.join(" "); caseBlocks.push(` "${matchPath}") if [[ "\${cur}" == -* ]]; then COMPREPLY=( $(compgen -W "${flagStr}" -- "\${cur}") ) else COMPREPLY=( $(compgen -W "${subcmdStr}" -- "\${cur}") ) fi return 0 ;;`); } } // Build flag-value completion cases const valueCases = []; for (const [flag, rules] of Object.entries(FLAG_VALUES)) { valueCases.push(` ${flag}) ${buildFlagValueCaseBody(rules)} return 0 ;;`); } const script = `#!/bin/bash # Bash completion for ${rootName} # Generated by ${rootName} completions _${rootName}() { local cur prev words cword if type _init_completion &>/dev/null; then _init_completion || return else cur="\${COMP_WORDS[COMP_CWORD]}" prev="\${COMP_WORDS[COMP_CWORD-1]}" words=("\${COMP_WORDS[@]}") cword=\${COMP_CWORD} fi # Build the command path from recognized subcommand transitions only. Positional # arguments and option values cannot extend the path, so completion remains # command-aware after either one. local cmd_path="${rootName}" local skip_value=0 for (( i=1; i < cword; i++ )); do if (( skip_value )); then skip_value=0 continue fi case "\${words[i]}" in --) break ;; --*=*) continue ;; ${valueFlags.join("|")}) skip_value=1; continue ;; -*) continue ;; *) case "\${cmd_path}:\${words[i]}" in ${commands .filter((info) => info.subcommands.length > 0) .flatMap((info) => info.subcommands.map((subcommand) => ` "${info.path}:${subcommand}") cmd_path="${info.path} ${subcommand}" ;;`)) .join("\n")} esac ;; esac done # Complete flag values case "\${prev}" in ${valueCases.join("\n")} esac # Complete based on current command path case "\${cmd_path}" in ${caseBlocks.join("\n")} esac return 0 } complete -F _${rootName} ${rootName} `; return script; } // ── Install ────────────────────────────────────────────────────────────────── export function installBashCompletions(script) { const dest = resolveInstallPath(); const dir = path.dirname(dest); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(dest, script, "utf8"); return dest; } function resolveInstallPath() { const xdgData = process.env.XDG_DATA_HOME?.trim(); if (xdgData) { return path.join(xdgData, "bash-completion", "completions", "akm"); } const home = os.homedir(); // Default XDG location const defaultXdg = path.join(home, ".local", "share", "bash-completion", "completions", "akm"); const defaultXdgDir = path.dirname(defaultXdg); if (isDir(defaultXdgDir) || isDir(path.dirname(defaultXdgDir))) { return defaultXdg; } // Fallback return path.join(home, ".bash_completion.d", "akm"); } function isDir(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }