@lenne.tech/cli
Version:
lenne.Tech CLI: lt
462 lines (461 loc) • 19.1 kB
JavaScript
;
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 });
exports.PLUGIN_POST_INSTALL = exports.MAX_COMMANDS_TO_SHOW = exports.EMPTY_PLUGIN_CONTENTS = void 0;
exports.findMarkdownFiles = findMarkdownFiles;
exports.handleMissingEnvVars = handleMissingEnvVars;
exports.printPluginSummary = printPluginSummary;
exports.processPostInstall = processPostInstall;
exports.readPluginContents = readPluginContents;
exports.safeExecCommand = safeExecCommand;
exports.setupPermissions = setupPermissions;
/**
* Plugin utilities for Claude Code plugin management
* Handles reading plugin contents, permissions, and post-installation setup
*/
const child_process_1 = require("child_process");
const fs_1 = require("fs");
const os_1 = require("os");
const path_1 = require("path");
const claude_cli_1 = require("./claude-cli");
const json_utils_1 = require("./json-utils");
const shell_config_1 = require("./shell-config");
/**
* Empty plugin contents constant (used for failed installations)
*/
exports.EMPTY_PLUGIN_CONTENTS = {
agents: [],
commands: [],
hooks: 0,
mcpServers: [],
permissions: [],
skills: [],
};
/**
* Maximum number of commands to show in plugin summary before truncating
*/
exports.MAX_COMMANDS_TO_SHOW = 5;
/**
* Post-installation configurations for specific plugins
* These define additional setup steps needed after plugin installation
*/
exports.PLUGIN_POST_INSTALL = {
'lt-offers': {
// No envVars needed — OAuth via browser login
// No requirements — MCP connection is remote HTTP
},
'typescript-lsp': {
envVars: [
{
description: 'Enable LSP tools in Claude Code (required for LSP features)',
name: 'ENABLE_LSP_TOOL',
value: '1',
},
],
requirements: [
{
checkCommand: 'which typescript-language-server',
description: 'TypeScript language server',
installCommand: 'npm install -g typescript-language-server typescript',
},
],
},
};
/**
* Recursively find all .md files in a directory and convert to command names
* @param dir - Directory to search
* @param basePath - Base path for relative path calculation
* @returns Array of command names (e.g., '/git:commit-message')
*/
function findMarkdownFiles(dir, basePath = '') {
const results = [];
if (!(0, fs_1.existsSync)(dir))
return results;
try {
const entries = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = (0, path_1.join)(dir, entry.name);
const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
results.push(...findMarkdownFiles(fullPath, relativePath));
}
else if (entry.isFile() && entry.name.endsWith('.md')) {
// Convert path to command name (e.g., "git/commit-message.md" -> "/git:commit-message")
const commandName = relativePath.replace(/\.md$/, '').replace(/\//g, ':');
results.push(`/${commandName}`);
}
}
}
catch (_a) {
// Directory read failed, return empty results
}
return results;
}
/**
* Handle missing environment variables from plugin installations
* Checks shell config, prompts user to add missing vars, and provides manual instructions
* @param missingEnvVars - Map of env var name to PluginEnvVar
* @param toolbox - Toolbox with print and prompt functions
* @returns Result indicating if vars were needed and if they were configured
*/
function handleMissingEnvVars(missingEnvVars, toolbox) {
return __awaiter(this, void 0, void 0, function* () {
const { print: { error, info, success, warning }, prompt, } = toolbox;
const result = {
configured: false,
needed: false,
};
if (missingEnvVars.size === 0) {
return result;
}
// Get preferred shell config
const targetConfig = (0, shell_config_1.getPreferredShellConfig)();
// Check which env vars are truly missing (not in file either)
const envVarsToAdd = [];
const envVarsAlreadyInFile = [];
for (const envVar of missingEnvVars.values()) {
if (targetConfig && (0, shell_config_1.checkEnvVarInFile)(targetConfig.path, envVar.name, envVar.value)) {
envVarsAlreadyInFile.push(envVar);
}
else {
envVarsToAdd.push(envVar);
}
}
// Show already configured vars
if (envVarsAlreadyInFile.length > 0) {
info('');
for (const envVar of envVarsAlreadyInFile) {
info(`${envVar.name} already configured in ${targetConfig === null || targetConfig === void 0 ? void 0 : targetConfig.path}`);
}
info(`Run 'source ${targetConfig === null || targetConfig === void 0 ? void 0 : targetConfig.path}' or restart your terminal to apply.`);
}
// Handle vars that need to be added
if (envVarsToAdd.length > 0) {
result.needed = true;
info('');
warning('Environment variables required:');
for (const envVar of envVarsToAdd) {
info(` ${envVar.name}=${envVar.value}`);
info(` ${envVar.description}`);
}
if (targetConfig) {
// Ask user if they want to add the env vars automatically
const shouldAdd = yield prompt.confirm(`Add ${envVarsToAdd.length > 1 ? 'these variables' : envVarsToAdd[0].name} to ${targetConfig.path}?`, true);
if (shouldAdd) {
let allAdded = true;
for (const envVar of envVarsToAdd) {
const added = (0, shell_config_1.addEnvVarToShellConfig)(targetConfig.path, envVar.name, envVar.value);
if (added) {
success(` Added ${envVar.name}=${envVar.value} to ${targetConfig.path}`);
}
else {
error(` Failed to add ${envVar.name} to ${targetConfig.path}`);
allAdded = false;
}
}
if (allAdded) {
result.configured = true;
info('');
info(`Run 'source ${targetConfig.path}' or restart your terminal to apply changes.`);
}
}
else {
info('');
info('To add manually, run:');
for (const envVar of envVarsToAdd) {
info(` echo 'export ${envVar.name}=${envVar.value}' >> ${targetConfig.path}`);
}
}
}
else {
info('');
info('Add manually to your shell profile:');
for (const envVar of envVarsToAdd) {
info(` export ${envVar.name}=${envVar.value}`);
}
}
}
return result;
});
}
/**
* Print plugin summary with skills, commands, agents, etc.
* @param pluginName - Name of the plugin
* @param contents - Plugin contents
* @param info - Info print function from toolbox
*/
function printPluginSummary(pluginName, contents, info) {
const isLspPlugin = pluginName.endsWith('-lsp');
const hasContent = contents.skills.length > 0 || contents.commands.length > 0 || contents.agents.length > 0;
if (hasContent || isLspPlugin) {
info('');
info(`${pluginName}:`);
// Show LSP indicator for LSP plugins
if (isLspPlugin) {
info(` Type: Language Server (LSP)`);
}
// Alphabetical order: Agents, Commands, Hooks, MCP Servers, Skills
if (contents.agents.length > 0) {
info(` Agents (${contents.agents.length}): ${contents.agents.join(', ')}`);
}
if (contents.commands.length > 0) {
const shown = contents.commands.slice(0, exports.MAX_COMMANDS_TO_SHOW);
const remaining = contents.commands.length - exports.MAX_COMMANDS_TO_SHOW;
const commandsStr = remaining > 0 ? `${shown.join(', ')} and ${remaining} more` : shown.join(', ');
info(` Commands (${contents.commands.length}): ${commandsStr}`);
}
if (contents.hooks > 0) {
info(` Hooks: ${contents.hooks} auto-detection hooks`);
}
if (contents.mcpServers.length > 0) {
info(` MCP Servers (${contents.mcpServers.length}): ${contents.mcpServers.join(', ')}`);
}
if (contents.skills.length > 0) {
info(` Skills (${contents.skills.length}): ${contents.skills.join(', ')}`);
}
}
}
/**
* Process post-installation requirements for a plugin
* Checks and installs required dependencies, reports missing env vars
* @param pluginName - Name of the plugin
* @param toolbox - Extended Gluegun toolbox
* @returns Post-install result with success status and details
*/
function processPostInstall(pluginName, toolbox) {
const { print: { spin, warning }, } = toolbox;
const result = {
envVarsMissing: [],
requirementsInstalled: [],
requirementsMissing: [],
success: true,
};
const postInstall = exports.PLUGIN_POST_INSTALL[pluginName];
if (!postInstall) {
return result;
}
// Check and install requirements
if (postInstall.requirements) {
for (const req of postInstall.requirements) {
// If checkCommand is provided, verify if requirement is already met
if (req.checkCommand) {
const checkSpinner = spin(`Checking ${req.description}`);
if ((0, claude_cli_1.checkCommandExists)(req.checkCommand)) {
checkSpinner.succeed(`${req.description} already installed`);
continue;
}
// Try to install
checkSpinner.text = `Installing ${req.description}`;
try {
safeExecCommand(req.installCommand);
// Verify installation
if ((0, claude_cli_1.checkCommandExists)(req.checkCommand)) {
checkSpinner.succeed(`${req.description} installed`);
result.requirementsInstalled.push(req.description);
}
else {
checkSpinner.fail(`${req.description} installation may have failed`);
result.requirementsMissing.push(req.description);
result.success = false;
}
}
catch (err) {
checkSpinner.fail(`Failed to install ${req.description}`);
warning(` Command: ${req.installCommand}`);
warning(` Error: ${err.message}`);
result.requirementsMissing.push(req.description);
result.success = false;
}
}
else {
// No checkCommand provided - always run installCommand
const installSpinner = spin(`Running ${req.description}`);
try {
safeExecCommand(req.installCommand);
installSpinner.succeed(`${req.description} completed`);
result.requirementsInstalled.push(req.description);
}
catch (err) {
installSpinner.fail(`Failed: ${req.description}`);
warning(` Command: ${req.installCommand}`);
warning(` Error: ${err.message}`);
result.requirementsMissing.push(req.description);
result.success = false;
}
}
}
}
// Check environment variables
if (postInstall.envVars) {
for (const envVar of postInstall.envVars) {
const currentValue = process.env[envVar.name];
if (currentValue !== envVar.value) {
result.envVarsMissing.push(envVar);
}
}
}
return result;
}
/**
* Read plugin contents from installed plugin directory
* @param marketplaceName - Name of the marketplace
* @param pluginName - Name of the plugin
* @returns Plugin contents with skills, commands, agents, hooks, etc.
*/
function readPluginContents(marketplaceName, pluginName) {
const pluginDir = (0, path_1.join)((0, os_1.homedir)(), '.claude', 'plugins', 'marketplaces', marketplaceName, 'plugins', pluginName);
const result = {
agents: [],
commands: [],
hooks: 0,
mcpServers: [],
permissions: [],
skills: [],
};
// Read skills
const skillsDir = (0, path_1.join)(pluginDir, 'skills');
if ((0, fs_1.existsSync)(skillsDir)) {
try {
result.skills = (0, fs_1.readdirSync)(skillsDir, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
}
catch (_a) {
// Skills directory read failed
}
}
// Read agents
const agentsDir = (0, path_1.join)(pluginDir, 'agents');
if ((0, fs_1.existsSync)(agentsDir)) {
try {
result.agents = (0, fs_1.readdirSync)(agentsDir, { withFileTypes: true })
.filter((dirent) => dirent.isFile() && dirent.name.endsWith('.md'))
.map((dirent) => dirent.name.replace(/\.md$/, ''));
}
catch (_b) {
// Agents directory read failed
}
}
// Read commands
const commandsDir = (0, path_1.join)(pluginDir, 'commands');
result.commands = findMarkdownFiles(commandsDir);
// Read hooks
const hooksPath = (0, path_1.join)(pluginDir, 'hooks', 'hooks.json');
const hooksContent = safeReadJson(hooksPath);
if (hooksContent === null || hooksContent === void 0 ? void 0 : hooksContent.hooks) {
// Count hooks across all event types
for (const eventHooks of Object.values(hooksContent.hooks)) {
if (Array.isArray(eventHooks)) {
for (const hookGroup of eventHooks) {
if (hookGroup.hooks && Array.isArray(hookGroup.hooks)) {
result.hooks += hookGroup.hooks.length;
}
}
}
}
}
// Read MCP servers
const mcpPath = (0, path_1.join)(pluginDir, '.mcp.json');
const mcpContent = safeReadJson(mcpPath);
if (mcpContent === null || mcpContent === void 0 ? void 0 : mcpContent.mcpServers) {
result.mcpServers = Object.keys(mcpContent.mcpServers);
}
// Read permissions
const permissionsPath = (0, path_1.join)(pluginDir, 'permissions.json');
const pluginPerms = safeReadJson(permissionsPath);
if (pluginPerms === null || pluginPerms === void 0 ? void 0 : pluginPerms.permissions) {
result.permissions = pluginPerms.permissions.map((p) => p.pattern);
}
return result;
}
/**
* Execute a shell command safely using spawnSync (no shell interpretation)
* Splits the command string into executable and arguments to prevent command injection
* @param command - Command string to execute (e.g., 'npm install -g typescript')
* @throws Error if command fails (non-zero exit code or signal)
*/
function safeExecCommand(command) {
var _a;
const [cmd, ...args] = command.trim().split(/\s+/);
const result = (0, child_process_1.spawnSync)(cmd, args, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
if (result.status !== 0 || result.error) {
throw new Error(result.stderr || ((_a = result.error) === null || _a === void 0 ? void 0 : _a.message) || `Command failed: ${command}`);
}
}
/**
* Setup permissions in settings.json
* Only adds new permissions, does not remove existing ones
* (User may have customized permissions that should be preserved)
* @param requiredPermissions - Array of permission patterns to add
* @param error - Error print function from toolbox
* @returns Result with added and existing permissions
*/
function setupPermissions(requiredPermissions, error) {
const settingsPath = (0, path_1.join)((0, os_1.homedir)(), '.claude', 'settings.json');
try {
// Read existing settings
let settings = {};
if ((0, fs_1.existsSync)(settingsPath)) {
const content = (0, fs_1.readFileSync)(settingsPath, 'utf-8');
const parsed = (0, json_utils_1.safeJsonParse)(content);
if (parsed) {
settings = parsed;
}
}
// Ensure permissions.allow exists
if (!settings.permissions) {
settings.permissions = {};
}
const perms = settings.permissions;
if (!perms.allow) {
perms.allow = [];
}
const currentAllowList = perms.allow;
// Determine what to add
const added = [];
const existing = [];
requiredPermissions.forEach((perm) => {
if (currentAllowList.includes(perm)) {
existing.push(perm);
}
else {
added.push(perm);
}
});
// Add new permissions
if (added.length > 0) {
perms.allow = [...currentAllowList, ...added];
(0, fs_1.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2));
}
return { added, existing, success: true };
}
catch (err) {
error(`Could not configure permissions: ${err.message}`);
return { added: [], existing: [], success: false };
}
}
/**
* Safely read and parse a JSON file
* @param filePath - Path to the JSON file
* @returns Parsed object or null if reading/parsing fails
*/
function safeReadJson(filePath) {
try {
if (!(0, fs_1.existsSync)(filePath)) {
return null;
}
const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
return (0, json_utils_1.safeJsonParse)(content);
}
catch (_a) {
return null;
}
}