UNPKG

@lenne.tech/cli

Version:

lenne.Tech CLI: lt

371 lines (370 loc) 15.4 kB
"use strict"; 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 () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const ejs = __importStar(require("ejs")); const fs_1 = require("fs"); const os_1 = require("os"); const path_1 = require("path"); /** * Detect user's shell */ function detectShell() { const shell = process.env.SHELL || ''; if (shell.includes('zsh')) return 'zsh'; if (shell.includes('fish')) return 'fish'; return 'bash'; } /** * Discover commands recursively as a tree structure * Supports arbitrary nesting depth */ function discoverCommandTree(dir = __dirname, parentPath = '') { const nodes = []; if (!(0, fs_1.existsSync)(dir)) { return nodes; } const entries = (0, fs_1.readdirSync)(dir); for (const entry of entries) { const entryPath = (0, path_1.join)(dir, entry); const stat = (0, fs_1.statSync)(entryPath); if (stat.isDirectory()) { // This is a command group (git, server, config, etc.) const groupName = entry; const nodePath = parentPath ? `${parentPath}_${groupName}` : groupName; // Recursively get children const children = discoverCommandTree(entryPath, nodePath); // Filter out the parent command file from children const filteredChildren = children.filter((c) => c.name !== groupName); // Only add if it has visible children if (filteredChildren.length > 0) { nodes.push({ children: filteredChildren, description: `${groupName.charAt(0).toUpperCase()}${groupName.slice(1)} commands`, name: groupName, path: nodePath, }); } } else if (stat.isFile() && (entry.endsWith('.js') || entry.endsWith('.ts'))) { const name = (0, path_1.basename)(entry, '.js').replace('.ts', ''); // Skip internal files (lt.ts is the main entry point) if (name === 'lt') continue; const cmdInfo = extractCommandInfo(entryPath); if (cmdInfo && !cmdInfo.hidden) { const nodePath = parentPath ? `${parentPath}_${name}` : name; nodes.push({ children: [], description: cmdInfo.description, name: cmdInfo.name, path: nodePath, }); } } } // Add native Gluegun commands that are not in the file system if (!parentPath) { nodes.push({ children: [], description: 'Show CLI version', name: 'version', path: 'version', }); nodes.push({ children: [], description: 'Alias for version', name: 'v', path: 'v', }); nodes.push({ children: [], description: 'Show help', name: 'help', path: 'help', }); nodes.push({ children: [], description: 'Alias for help', name: 'h', path: 'h', }); } return nodes.sort((a, b) => a.name.localeCompare(b.name)); } /** * Extract command info from a command file * Uses require() for both .js and .ts files (ts-node handles .ts in dev mode) */ function extractCommandInfo(filePath) { try { const cmd = require(filePath); const command = cmd.default || cmd; // Validate that we got a proper command object if (!command || typeof command.name !== 'string') { return null; } return { description: command.description || '', hidden: command.hidden || false, name: command.name, }; } catch (_a) { // Ignore errors for individual files (missing dependencies, syntax errors, etc.) return null; } } /** * Generate completion script from EJS template */ function generateCompletionFromTemplate(shell, commandTree) { const templateDir = getTemplateDir(); const templatePath = (0, path_1.join)(templateDir, `${shell}.sh.ejs`); if (!(0, fs_1.existsSync)(templatePath)) { throw new Error(`Template not found: ${templatePath}`); } const templateContent = (0, fs_1.readFileSync)(templatePath, 'utf-8'); const maxDepth = getMaxDepth(commandTree); return ejs.render(templateContent, { props: { commandTree, maxDepth, }, }); } /** * Get completion paths for static file installation * Uses ~/.local/share/lt/completions/ for Bash/Zsh (standard XDG location) * Uses ~/.config/fish/completions/ for Fish (auto-loaded by Fish) */ function getCompletionPaths(shell) { const home = (0, os_1.homedir)(); const ltDir = (0, path_1.join)(home, '.local', 'share', 'lt', 'completions'); switch (shell) { case 'fish': return { completionFile: (0, path_1.join)(home, '.config', 'fish', 'completions', 'lt.fish'), configFile: null, // Fish auto-loads from completions dir sourceLine: null, }; case 'zsh': return { completionFile: (0, path_1.join)(ltDir, '_lt'), configFile: (0, path_1.join)(home, '.zshrc'), // Source the file directly - it uses compdef internally // This avoids calling compinit which conflicts with Powerlevel10k instant prompt sourceLine: `# lt CLI completion\n[ -f ~/.local/share/lt/completions/_lt ] && source ~/.local/share/lt/completions/_lt`, }; case 'bash': default: { const bashProfile = (0, path_1.join)(home, '.bash_profile'); return { completionFile: (0, path_1.join)(ltDir, 'lt.bash'), configFile: (0, fs_1.existsSync)(bashProfile) ? bashProfile : (0, path_1.join)(home, '.bashrc'), sourceLine: `# lt CLI completion\n[ -f ~/.local/share/lt/completions/lt.bash ] && source ~/.local/share/lt/completions/lt.bash`, }; } } } /** * Calculate maximum depth of command tree */ function getMaxDepth(nodes, currentDepth = 1) { let maxDepth = currentDepth; for (const node of nodes) { if (node.children.length > 0) { const childDepth = getMaxDepth(node.children, currentDepth + 1); maxDepth = Math.max(maxDepth, childDepth); } } return maxDepth; } /** * Get template directory path */ function getTemplateDir() { // In development (src), templates are in ../templates/completion // In production (dist), templates are in ../templates/completion const srcPath = (0, path_1.join)(__dirname, '..', 'templates', 'completion'); if ((0, fs_1.existsSync)(srcPath)) { return srcPath; } // Fallback for different directory structures return (0, path_1.join)(__dirname, '..', '..', 'src', 'templates', 'completion'); } /** * Install completion using static files (no runtime overhead) * - Generates completion script to ~/.local/share/lt/completions/ * - Adds source line to shell config (Bash/Zsh only) * - Fish auto-loads from ~/.config/fish/completions/ */ function installCompletion(toolbox_1, shell_1) { return __awaiter(this, arguments, void 0, function* (toolbox, shell, options = {}) { const { filesystem, print: { error, info, success, warning }, prompt: { confirm }, } = toolbox; const { noConfirm = false, silent = false } = options; const log = silent ? () => { } : info; const logSuccess = silent ? () => { } : success; const logWarning = silent ? () => { } : warning; const logError = silent ? () => { } : error; const paths = getCompletionPaths(shell); // Confirm installation (skip if noConfirm or Fish) if (!noConfirm && paths.configFile) { log('This will:'); log(` 1. Generate completion script to ${paths.completionFile}`); log(` 2. Add source line to ${paths.configFile}`); log(''); const proceed = yield confirm('Install completion?', true); if (!proceed) { log('Installation cancelled.'); return { configFile: paths.configFile || paths.completionFile, success: false }; } } try { // Ensure completion directory exists const completionDir = (0, path_1.dirname)(paths.completionFile); if (!(0, fs_1.existsSync)(completionDir)) { filesystem.dir(completionDir); } // Generate and write completion script const commandTree = discoverCommandTree(); const script = generateCompletionFromTemplate(shell, commandTree); filesystem.write(paths.completionFile, script); // For Bash/Zsh: add source line to config if not already present if (paths.configFile && paths.sourceLine) { if (isSourceLineInstalled(paths.configFile)) { logSuccess(`Completions updated: ${paths.completionFile}`); logWarning(`Source line already in ${paths.configFile}`); } else { (0, fs_1.appendFileSync)(paths.configFile, `\n${paths.sourceLine}\n`); logSuccess(`Completions installed to ${paths.completionFile}`); log(''); log('Run the following to activate (or restart your terminal):'); log(` source ${paths.configFile}`); } } else { // Fish: just confirm file was written logSuccess(`Fish completions installed to ${paths.completionFile}`); } return { configFile: paths.configFile || paths.completionFile, success: true }; } catch (e) { logError(`Failed to install: ${e.message}`); return { configFile: paths.configFile || paths.completionFile, success: false }; } }); } /** * Check if source line is already in shell config */ function isSourceLineInstalled(configFile) { if (!configFile || !(0, fs_1.existsSync)(configFile)) { return false; } const content = (0, fs_1.readFileSync)(configFile, 'utf-8'); return content.includes('lt CLI completion') || content.includes('lt/completions'); } /** * Generate shell completion scripts (using EJS templates) */ const CompletionCommand = { alias: ['comp'], description: 'Generate shell completions', hidden: false, name: 'completion', run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () { var _a, _b; const { parameters, print: { error, info }, } = toolbox; const action = (_a = parameters.first) === null || _a === void 0 ? void 0 : _a.toLowerCase(); // Handle install command if (action === 'install') { const shell = ((_b = parameters.second) === null || _b === void 0 ? void 0 : _b.toLowerCase()) || detectShell(); if (!['bash', 'fish', 'zsh'].includes(shell)) { error(`Unknown shell: ${shell}`); return 'completion: install error'; } const noConfirm = parameters.options.noConfirm || parameters.options.y || false; const silent = parameters.options.silent || parameters.options.s || false; const result = yield installCompletion(toolbox, shell, { noConfirm, silent }); return result.success ? `completion: installed ${shell}` : 'completion: install cancelled'; } // Show help if no valid shell specified if (!action || !['bash', 'fish', 'zsh'].includes(action)) { info('Usage: lt completion <shell|install>'); info(''); info('Commands:'); info(' bash Output Bash completion script'); info(' zsh Output Zsh completion script'); info(' fish Output Fish completion script'); info(' install Install completion (recommended)'); info(''); info('Installation (recommended):'); info(' lt completion install'); info(''); info('This generates static completion files that are loaded at shell startup.'); info('Completions are auto-updated on CLI install/update.'); info(''); info('Locations:'); info(' Bash/Zsh: ~/.local/share/lt/completions/'); info(' Fish: ~/.config/fish/completions/lt.fish'); return 'completion: help'; } // Discover commands dynamically as tree const commandTree = discoverCommandTree(); // Generate and output the appropriate script using templates try { const script = generateCompletionFromTemplate(action, commandTree); process.stdout.write(script); return `completion: ${action}`; } catch (e) { error(`Failed to generate completion: ${e.message}`); return 'completion: error'; } }), }; exports.default = CompletionCommand;