@catladder/cli
Version:
Panter cli tool for cloud CI/CD and DevOps
240 lines (232 loc) • 8.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.uninstallCompletions = exports.installCompletions = exports.generateZshCompletionScript = exports.getCompletions = void 0;
const fs_1 = require("fs");
const path_1 = require("path");
const os_1 = require("os");
/**
* Generate completions for the current command line.
*
* @param allCommands - All registered command definitions
* @param words - The words typed so far (argv without the binary name)
* @param cursorWord - The word currently being completed (may be empty)
* @returns Array of completion strings
*/
async function getCompletions(allCommands, words, cursorWord) {
// Try to find a command by matching the longest prefix of words.
// E.g., for words ["project", "cloudsql", "restore-db", "--source"],
// we match "project cloudsql restore-db" and treat ["--source"] as args.
const matched = findCommand(allCommands, words);
if (matched) {
const {
command,
argsAfterCommand
} = matched;
// Check if cursor is a --flag name
if (cursorWord.startsWith("--")) {
return getFlagCompletions(command, cursorWord);
}
// Check if previous word was a --flag (complete its value)
const prevWord = argsAfterCommand[argsAfterCommand.length - 1];
if (prevWord === null || prevWord === void 0 ? void 0 : prevWord.startsWith("--")) {
const flagName = prevWord.replace(/^--/, "");
const inputName = flagNameToInputName(flagName);
const inputDef = command.inputs[inputName];
if (inputDef && "choices" in inputDef && inputDef.choices) {
return resolveChoicesForCompletion(inputDef, cursorWord);
}
return [];
}
// Complete positional inputs
const positionalInputs = getPositionalInputs(command);
const positionalArgs = argsAfterCommand.filter(w => !w.startsWith("--"));
const positionalIndex = positionalArgs.length;
const targetInput = positionalInputs[positionalIndex];
if (targetInput) {
const [, inputDef] = targetInput;
if ("choices" in inputDef && inputDef.choices) {
return resolveChoicesForCompletion(inputDef, cursorWord);
}
}
return [];
}
// No command matched. Complete subcommand names at the current level.
const typed = words.join(" ");
const prefix = typed ? typed + " " : "";
const nextParts = new Set();
for (const cmd of allCommands) {
if (cmd.name.startsWith(prefix)) {
const rest = cmd.name.slice(prefix.length);
const nextPart = rest.split(" ")[0];
if (nextPart && nextPart.startsWith(cursorWord)) {
nextParts.add(nextPart);
}
}
}
return [...nextParts].sort();
}
exports.getCompletions = getCompletions;
/**
* Find a command by matching the longest prefix of words against command names.
* Returns the matched command and the remaining words (args/flags after the command name).
*/
function findCommand(allCommands, words) {
// Try longest match first
for (let i = words.length; i >= 1; i--) {
const candidate = words.slice(0, i).join(" ");
const command = allCommands.find(c => c.name === candidate);
if (command) {
return {
command,
argsAfterCommand: words.slice(i)
};
}
}
return null;
}
function getPositionalInputs(command) {
return Object.entries(command.inputs).filter(([, def]) => def.positional);
}
function getFlagCompletions(command, cursorWord) {
const flags = [];
for (const [name, inputDef] of Object.entries(command.inputs)) {
if (inputDef.positional) continue;
const flagName = `--${name.replace(/([A-Z])/g, "-$1").toLowerCase()}`;
flags.push(flagName);
}
flags.push("--inputs");
flags.push("--yes");
return flags.filter(f => f.startsWith(cursorWord));
}
function flagNameToInputName(flagName) {
return flagName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
}
async function resolveChoicesForCompletion(inputDef, cursorWord) {
if (!("choices" in inputDef) || !inputDef.choices) return [];
try {
// Create a minimal ctx that returns empty for any get() call.
// This works for choices functions that don't depend on other inputs
// (like envAndComponents). For dependent choices, completion won't work
// but that's acceptable -- those are typically non-positional.
const noopCtx = {
get: () => Promise.reject(new Error("not available during completion")),
log: () => {},
confirm: () => Promise.resolve(true),
promptDirect: () => Promise.reject(new Error("not available during completion"))
};
const choices = await inputDef.choices(noopCtx);
return choices.map(c => typeof c === "string" ? c : c.value).filter(v => v.startsWith(cursorWord));
} catch (_a) {
// If choices function fails (e.g., needs other inputs), return nothing
return [];
}
}
/**
* Generate the zsh completion script.
* The script discovers the local catladder binary at completion time,
* so it works without a global install.
*/
function generateZshCompletionScript() {
return `
###-begin-catladder-completions-###
# Find catladder binary by walking up from cwd
_catladder_find_binary() {
local dir="$PWD"
while [[ "$dir" != "/" ]]; do
if [[ -x "$dir/node_modules/.bin/catladder" ]]; then
echo "$dir/node_modules/.bin/catladder"
return
fi
# Also check for catladder-dev (monorepo development)
if [[ -x "$dir/cli/bin/catladder-dev" ]]; then
echo "$dir/cli/bin/catladder-dev"
return
fi
dir="\${dir:h}"
done
# Fall back to global
if command -v catladder &>/dev/null; then
echo "catladder"
elif command -v catladder-dev &>/dev/null; then
echo "catladder-dev"
fi
}
# Run completion with given offset (number of words to skip before CLI args)
_catladder_do_complete() {
local skip=$1
local catladder_bin=$(_catladder_find_binary)
[[ -z "$catladder_bin" ]] && return
local -a completions
local current_word="\${words[$CURRENT]}"
local -a cmd_words=(\${words:$skip:$((CURRENT-skip-1))})
completions=(\${(f)"$($catladder_bin __complete -- "\${cmd_words[*]}" "$current_word" 2>/dev/null)"})
if [[ \${#completions[@]} -gt 0 ]]; then
compadd -a completions
fi
}
# catladder <TAB>
_catladder_completions() { _catladder_do_complete 1; }
# yarn catladder <TAB>
_yarn_catladder_completions() {
[[ $CURRENT -le 2 || "\${words[2]}" != "catladder" ]] && return
_catladder_do_complete 2
}
compdef _catladder_completions catladder
compdef _catladder_completions catladder-dev
compdef _yarn_catladder_completions yarn
###-end-catladder-completions-###
`.trim();
}
exports.generateZshCompletionScript = generateZshCompletionScript;
const BEGIN_MARKER = "###-begin-catladder-completions-###";
const END_MARKER = "###-end-catladder-completions-###";
function getZshrcPath() {
return (0, path_1.join)((0, os_1.homedir)(), ".zshrc");
}
/**
* Install completions by writing the script directly into .zshrc.
*/
async function installCompletions() {
const zshrcPath = getZshrcPath();
let content = "";
if ((0, fs_1.existsSync)(zshrcPath)) {
content = (0, fs_1.readFileSync)(zshrcPath, "utf-8");
}
if (content.includes(BEGIN_MARKER)) {
// Replace existing
const startIdx = content.indexOf(BEGIN_MARKER);
const endIdx = content.indexOf(END_MARKER) + END_MARKER.length;
content = content.slice(0, startIdx) + generateZshCompletionScript() + content.slice(endIdx);
(0, fs_1.writeFileSync)(zshrcPath, content);
console.log(`Updated catladder completions in ${zshrcPath}`);
} else {
(0, fs_1.writeFileSync)(zshrcPath, content + "\n" + generateZshCompletionScript() + "\n");
console.log(`Installed catladder completions in ${zshrcPath}`);
}
console.log(`Restart your shell or run: source ${zshrcPath}`);
}
exports.installCompletions = installCompletions;
/**
* Remove completions from .zshrc.
*/
async function uninstallCompletions() {
const zshrcPath = getZshrcPath();
if (!(0, fs_1.existsSync)(zshrcPath)) {
console.log("No .zshrc found");
return;
}
const content = (0, fs_1.readFileSync)(zshrcPath, "utf-8");
if (!content.includes(BEGIN_MARKER)) {
console.log("No catladder completions found in " + zshrcPath);
return;
}
const startIdx = content.indexOf(BEGIN_MARKER);
const endIdx = content.indexOf(END_MARKER) + END_MARKER.length;
const cleaned = (content.slice(0, startIdx) + content.slice(endIdx)).replace(/\n{3,}/g, "\n\n");
(0, fs_1.writeFileSync)(zshrcPath, cleaned);
console.log(`Removed catladder completions from ${zshrcPath}`);
console.log(`Restart your shell or run: source ${zshrcPath}`);
}
exports.uninstallCompletions = uninstallCompletions;