@catladder/cli
Version:
Panter cli tool for cloud CI/CD and DevOps
225 lines (224 loc) • 8.19 kB
JavaScript
;
var __createBinding = this && this.__createBinding || (Object.create ? function (o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = {
enumerable: true,
get: function () {
return m[k];
}
};
}
Object.defineProperty(o, k2, desc);
} : function (o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
});
var __setModuleDefault = this && this.__setModuleDefault || (Object.create ? function (o, v) {
Object.defineProperty(o, "default", {
enumerable: true,
value: v
});
} : function (o, v) {
o["default"] = v;
});
var __importStar = this && this.__importStar || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = this && this.__importDefault || function (mod) {
return mod && mod.__esModule ? mod : {
"default": mod
};
};
Object.defineProperty(exports, "__esModule", {
value: true
});
const commander_1 = require("commander");
const terminal_1 = require("./adapters/terminal");
const completion_1 = require("./completion");
const getProjectConfig_1 = require("./config/getProjectConfig");
const packageInfos_1 = __importDefault(require("./packageInfos"));
const portForwards_1 = require("./utils/portForwards");
// Import all commands
const commands = __importStar(require("./commands"));
const program = new commander_1.Command();
program.name("catladder").description("Panter CLI tool for cloud CI/CD and DevOps").version(packageInfos_1.default.version).option("-y, --yes", "skip all confirmation prompts");
// Collect all command defs for completion
const allCommandDefs = [];
// Cache for intermediate subcommand groups (e.g., "project", "project cloudsql")
const subcommandCache = new Map();
/**
* Get or create a chain of nested subcommands.
* E.g., for ["project", "cloudsql"], ensures program.command("project").command("cloudsql") exists.
*/
function getOrCreateParent(parts) {
if (parts.length === 0) return program;
const key = parts.join(" ");
if (subcommandCache.has(key)) return subcommandCache.get(key);
const parent = getOrCreateParent(parts.slice(0, -1));
const cmd = parent.command(parts[parts.length - 1]);
subcommandCache.set(key, cmd);
return cmd;
}
/**
* Register a CommandDef with Commander.js.
* - Command name can contain spaces for nesting (e.g., "project cloudsql proxy")
* - Inputs with `positional: true` become Commander positional args
* - Other inputs become optional --flags
* - --inputs accepts JSON for programmatic use
*/
function registerCommand(def) {
allCommandDefs.push(def);
// Separate positional inputs from flag inputs
const positionalInputs = [];
const flagInputs = [];
for (const [name, inputDef] of Object.entries(def.inputs)) {
if (inputDef.positional) {
positionalInputs.push([name, inputDef]);
} else {
flagInputs.push([name, inputDef]);
}
}
// Split name into parent groups + leaf command
const nameParts = def.name.split(" ");
const parentParts = nameParts.slice(0, -1);
const leafName = nameParts[nameParts.length - 1];
// Build leaf command string with positional args
let cmdStr = leafName;
for (const [name] of positionalInputs) {
cmdStr += ` [${name}]`;
}
const parent = getOrCreateParent(parentParts);
const cmd = parent.command(cmdStr).description(def.description);
// Add --flag for each non-positional input
for (const [name, inputDef] of flagInputs) {
const flagName = name.replace(/([A-Z])/g, "-$1").toLowerCase();
const desc = inputDef.message.replace(/\s*🤔\s*$/, "").trim();
if (inputDef.type === "boolean") {
cmd.option(`--${flagName}`, desc);
cmd.option(`--no-${flagName}`, `negate: ${desc}`);
} else {
cmd.option(`--${flagName} <value>`, desc);
}
}
// Add --inputs for JSON input
cmd.option("--inputs <json>", "JSON object with input values");
cmd.action(async (...rawArgs) => {
// Commander passes positional args first, then options object, then the Command
const opts = rawArgs[rawArgs.length - 2];
// Parse positional args into cliOptions
const cliOptions = {};
positionalInputs.forEach(([name], i) => {
if (rawArgs[i] !== undefined) {
cliOptions[name] = rawArgs[i];
}
});
// Parse CLI flag overrides
for (const [name, inputDef] of flagInputs) {
const flagName = name.replace(/([A-Z])/g, "-$1").toLowerCase();
const camelName = flagName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
if (opts[camelName] !== undefined) {
let value = opts[camelName];
if (inputDef.type === "number" && typeof value === "string") {
value = Number(value);
}
cliOptions[name] = value;
}
}
// Parse --inputs JSON
let jsonInputs = {};
if (opts.inputs) {
try {
jsonInputs = JSON.parse(opts.inputs);
} catch (_a) {
console.error("Error: --inputs must be valid JSON");
process.exit(1);
}
}
const ctx = (0, terminal_1.createTerminalContext)(def, {
cliOptions,
jsonInputs,
yes: program.opts().yes
});
try {
await def.execute(ctx);
} catch (err) {
if (err.name === "MissingInputError") {
console.error(`\n${err.message}\n`);
process.exit(1);
}
throw err;
}
});
}
// Register all commands (async to support isAvailable checks)
async function registerAllCommands() {
// Fetch contexts once for all isAvailable checks
let contexts = [];
try {
contexts = await (0, getProjectConfig_1.getAllPipelineContexts)();
} catch (_a) {
// No config available (e.g. outside a project) — contexts stays empty
}
for (const cmd of Object.values(commands)) {
if (cmd && typeof cmd === "object" && "name" in cmd && "execute" in cmd) {
const def = cmd;
if (def.isAvailable) {
if (!def.isAvailable(contexts)) continue;
}
registerCommand(def);
}
}
}
// Hidden __complete command for shell completion
program.command("__complete", {
hidden: true
}).argument("[words]", "words typed so far").argument("[cursor]", "word at cursor").action(async (wordsStr, cursor) => {
const words = wordsStr ? wordsStr.split(" ").filter(Boolean) : [];
const cursorWord = cursor !== null && cursor !== void 0 ? cursor : "";
try {
const completions = await (0, completion_1.getCompletions)(allCommandDefs, words, cursorWord);
for (const c of completions) {
console.log(c);
}
} catch (_a) {
// Silently fail -- completion should never break the shell
}
process.exit(0);
});
// completion commands
const completionCmd = program.command("completion").description("shell completion setup");
completionCmd.command("zsh").description("output zsh completion script").action(() => {
console.log((0, completion_1.generateZshCompletionScript)());
});
completionCmd.command("install").description("install shell completions into your .zshrc").action(async () => {
await (0, completion_1.installCompletions)();
});
completionCmd.command("uninstall").description("remove shell completions from your .zshrc").action(async () => {
await (0, completion_1.uninstallCompletions)();
});
// Cleanup on exit
process.on("exit", () => {
(0, portForwards_1.stopAllPortForwards)();
});
// Prevent Node.js from printing the (minified) source line on uncaught errors
process.on("uncaughtException", err => {
var _a;
console.error((_a = err.stack) !== null && _a !== void 0 ? _a : `${err.name}: ${err.message}`);
process.exit(1);
});
process.on("unhandledRejection", reason => {
var _a;
if (reason instanceof Error) {
console.error((_a = reason.stack) !== null && _a !== void 0 ? _a : `${reason.name}: ${reason.message}`);
} else {
console.error(reason);
}
process.exit(1);
});
registerAllCommands().then(() => program.parse());