@lenne.tech/cli
Version:
lenne.Tech CLI: lt
371 lines (370 loc) • 15.4 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 () {
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;