@twelvehart/envctl
Version:
Environment variable context manager for development workflows
444 lines (433 loc) • 16.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const commander_1 = require("commander");
const chalk_1 = tslib_1.__importDefault(require("chalk"));
const readline = tslib_1.__importStar(require("readline"));
const env_manager_1 = require("./env-manager");
const program = new commander_1.Command();
const envManager = new env_manager_1.EnvManager();
// Helper functions
const success = (message) => console.log(chalk_1.default.green(`✓ ${message}`));
const error = (message) => console.log(chalk_1.default.red(`✗ ${message}`));
const info = (message) => console.log(chalk_1.default.blue(`ℹ ${message}`));
const warn = (message) => console.log(chalk_1.default.yellow(`⚠ ${message}`));
program.name('envctl').description('Environment variable context manager').version('x.x.x');
program
.command('create')
.description('Create a new profile')
.argument('<profile>', 'Profile name')
.action(async (profile) => {
try {
await envManager.createProfile(profile);
success(`Created profile '${profile}'`);
}
catch (err) {
error(err.message);
process.exit(1);
}
});
program
.command('add')
.description('Add/update environment variable(s)')
.argument('<profile>', 'Profile name')
.argument('[keyvalue...]', 'KEY=VALUE pairs')
.option('-f, --file <path>', 'Load variables from file')
.action(async (profile, keyvalues, options) => {
try {
if (options?.file) {
const count = await envManager.addVariablesFromFile(profile, options.file);
success(`Added ${count} variables from '${options.file}' to profile '${profile}'`);
}
else if (keyvalues && keyvalues.length > 0) {
const processedVars = {};
const duplicateKeys = [];
for (const keyvalue of keyvalues) {
if (!keyvalue.includes('=')) {
error(`Invalid format for '${keyvalue}'. Use KEY=VALUE`);
process.exit(1);
}
const [key, ...valueParts] = keyvalue.split('=');
const value = valueParts.join('=');
if (!key) {
error(`Invalid format for '${keyvalue}'. Key cannot be empty`);
process.exit(1);
}
if (processedVars[key] !== undefined) {
// If key is a duplicate, add it to duplicateKeys if it's not already there
if (!duplicateKeys.includes(key)) {
duplicateKeys.push(key);
}
}
processedVars[key] = value;
}
for (const [key, value] of Object.entries(processedVars)) {
await envManager.addVariable(profile, key, value);
}
const addedKeys = Object.keys(processedVars);
if (addedKeys.length === 1) {
success(`Added ${addedKeys[0]} to profile '${profile}'`);
}
else {
success(`Added ${addedKeys.length} variables (${addedKeys.join(', ')}) to profile '${profile}'`);
}
if (duplicateKeys.length > 0) {
warn(`Duplicate keys detected: ${duplicateKeys.join(', ')} (used last value for each)`);
}
}
else {
error('Either provide KEY=VALUE pairs or use --file option');
process.exit(1);
}
}
catch (err) {
error(err.message);
process.exit(1);
}
});
program
.command('remove')
.description('Remove environment variable')
.argument('<profile>', 'Profile name')
.argument('<key>', 'Variable key to remove')
.action(async (profile, key) => {
try {
await envManager.removeVariable(profile, key);
success(`Removed ${key} from profile '${profile}'`);
}
catch (err) {
error(err.message);
process.exit(1);
}
});
program
.command('load')
.description('Load profile into current session (reloads if same profile already loaded)')
.argument('<profile>', 'Profile name')
.action(async (profile) => {
try {
const commands = await envManager.loadProfile(profile);
console.log(commands);
}
catch (err) {
console.error(`Error: ${err.message}`);
process.exit(1);
}
});
program
.command('unload')
.description('Unload current profile')
.action(async () => {
try {
const result = await envManager.unloadProfile();
console.log(result.commands);
}
catch (err) {
console.error(`Error: ${err.message}`);
process.exit(1);
}
});
program
.command('switch')
.description('Switch to a different profile (unload current + load new)')
.argument('<profile>', 'Profile name to switch to')
.action(async (profile) => {
try {
const result = await envManager.switchProfile(profile);
console.log(result.commands);
}
catch (err) {
console.error(`Error: ${err.message}`);
process.exit(1);
}
});
program
.command('status')
.description('Show current profile status')
.action(async () => {
try {
const status = await envManager.getStatus();
// Current session status
if (status.currentSession.profileName) {
info(`Current session: ${chalk_1.default.cyan(status.currentSession.profileName)} (${status.currentSession.variableCount} variables)`);
}
else {
info('Current session: No profile loaded');
}
// Other sessions status
if (status.otherSessions.length > 0) {
console.log();
info('Other active sessions:');
status.otherSessions.forEach((session) => {
const profileDisplay = session.profileName ? chalk_1.default.cyan(session.profileName) : chalk_1.default.gray('no profile');
const sessionDisplay = chalk_1.default.yellow(session.sessionId);
console.log(` Session ${sessionDisplay}: ${profileDisplay}`);
});
}
// Summary
if (status.totalSessions > 1) {
console.log();
info(`Total active sessions: ${status.totalSessions}`);
}
}
catch (err) {
error(err.message);
process.exit(1);
}
});
program
.command('list')
.description('List profiles or variables in profile')
.argument('[profile]', 'Profile name (optional)')
.option('-s, --sessions', 'Show session information for loaded profiles')
.action(async (profile, options) => {
try {
if (profile) {
const profileData = await envManager.getProfile(profile);
if (!profileData) {
error(`Profile '${profile}' does not exist`);
process.exit(1);
}
console.log(chalk_1.default.cyan(`Variables in profile '${profile}':`));
if (Object.keys(profileData.variables).length === 0) {
info('No variables defined');
}
else {
for (const [key, value] of Object.entries(profileData.variables)) {
console.log(` ${chalk_1.default.yellow(key)}=${value}`);
}
}
}
else {
const profiles = await envManager.listProfiles();
if (profiles.length === 0) {
info('No profiles found');
}
else {
console.log(chalk_1.default.cyan('Available profiles:'));
for (const p of profiles) {
let status = '';
if (p.isLoaded) {
if (options?.sessions && p.loadedInSessions) {
const sessionList = p.loadedInSessions.map((s) => chalk_1.default.yellow(s)).join(', ');
status = chalk_1.default.green(` (loaded in sessions: ${sessionList})`);
}
else {
status = chalk_1.default.green(' (loaded)');
}
}
console.log(` ${chalk_1.default.yellow(p.name)} (${p.variableCount} variables)${status}`);
}
}
}
}
catch (err) {
error(err.message);
process.exit(1);
}
});
program
.command('delete')
.description('Delete a profile')
.argument('<profile>', 'Profile name')
.action(async (profile) => {
try {
await envManager.deleteProfile(profile);
success(`Deleted profile '${profile}'`);
}
catch (err) {
error(err.message);
process.exit(1);
}
});
program
.command('export')
.description('Export profile to stdout')
.argument('<profile>', 'Profile name')
.action(async (profile) => {
try {
const exported = await envManager.exportProfile(profile);
console.log(exported);
}
catch (err) {
error(err.message);
process.exit(1);
}
});
program
.command('setup')
.description('Install shell integration functions')
.action(async () => {
try {
const result = await envManager.setupShellIntegration();
success('Shell integration installed successfully!');
info(`Integration script: ${result.integrationFile}`);
info(`Added to: ${result.rcFile}`);
info('');
info('Available functions:');
info(' envctl-load <profile> (or: ecl <profile>)');
info(' envctl-unload (or: ecu)');
info(' envctl-switch <profile> (or: ecsw <profile>)');
info(' envctl status (or: ecs)');
info(' envctl list (or: ecls)');
info('');
warn(`Please restart your shell or run: source ${result.rcFile}`);
}
catch (err) {
error(err.message);
process.exit(1);
}
});
program
.command('unsetup')
.description('Remove shell integration and optionally all envctl data')
.option('--all', 'Remove all envctl data including profiles (WARNING: destructive)')
.option('--force', 'Skip confirmation prompts (for non-interactive use)')
.action(async (options) => {
try {
if (options?.all) {
console.log(chalk_1.default.yellow('⚠ WARNING: This will remove ALL envctl data including:'));
console.log(' - All profiles and their environment variables');
console.log(' - Shell integration functions');
console.log(' - Configuration files');
console.log('');
// Check if we need confirmation
let shouldProceed = false;
if (options?.force) {
// Force flag bypasses confirmation
shouldProceed = true;
}
else if (!process.stdin.isTTY) {
// Non-interactive environment (CI/automated scripts)
console.log(chalk_1.default.red('✗ Cannot proceed: This is a destructive operation'));
console.log(' In non-interactive environments, use --force to confirm');
console.log(' Example: envctl unsetup --all --force');
process.exit(1);
}
else {
// Interactive confirmation
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
try {
const answer = await new Promise((resolve) => {
rl.question(chalk_1.default.yellow('Are you sure you want to proceed? Type "yes" to confirm: '), resolve);
});
shouldProceed = answer.toLowerCase() === 'yes';
if (!shouldProceed) {
console.log(chalk_1.default.blue('ℹ Operation cancelled.'));
process.exit(0);
}
}
finally {
rl.close();
}
}
if (shouldProceed) {
const shellResult = await envManager.unsetupShellIntegration();
const dataResult = await envManager.cleanupAllData();
const allRemoved = [...shellResult.removed, ...dataResult.removed];
if (allRemoved.length > 0) {
success('Complete cleanup completed!');
info('Removed:');
allRemoved.forEach((item) => info(` - ${item}`));
}
else {
info('No envctl data found to remove');
}
}
}
else {
// Just remove shell integration (no confirmation needed)
const result = await envManager.unsetupShellIntegration();
if (result.removed.length > 0) {
success('Shell integration removed successfully!');
info('Removed:');
result.removed.forEach((item) => info(` - ${item}`));
info('');
warn(`Please restart your shell or run: source ${result.rcFile}`);
info('');
info('Your profiles and data are still available.');
info('Use "envctl unsetup --all" to remove everything.');
}
else {
info('No shell integration found to remove');
}
}
}
catch (err) {
error(err.message);
process.exit(1);
}
});
// Debug command to show session info - temporarily disabled
/*
program
.command('debug-session')
.description('Debug session ID generation (for troubleshooting)')
.action(async () => {
try {
const { Storage } = await import('./storage')
const storage = new Storage()
console.log('=== Debug: Session Information ===')
console.log(`Node.js PID: ${process.pid}`)
console.log(`Node.js PPID: ${process.ppid}`)
console.log(`SHLVL: ${process.env.SHLVL}`)
console.log(`TERM: ${process.env.TERM}`)
console.log(`TERM_PROGRAM: ${process.env.TERM_PROGRAM}`)
console.log(`SSH_TTY: ${process.env.SSH_TTY}`)
const sessionId = storage['getSessionId']()
console.log(`Generated Session ID: ${sessionId}`)
const backupPath = storage['sessionBackupFilePath']
console.log(`Expected backup file: ${backupPath}`)
// Check if backup file exists
const fs = require('fs-extra')
const configDir = storage['config'].configDir
const files = await fs.readdir(configDir).catch(() => [])
const backupFiles = files.filter((f: string) => f.startsWith('backup-') && f.endsWith('.env'))
console.log(`Actual backup files: ${backupFiles.join(', ') || 'none'}`)
// Debug getCurrentlyLoadedProfile
console.log('\n=== Debug: getCurrentlyLoadedProfile ===')
const fileExists = await fs.pathExists(backupPath)
console.log(`Backup file exists: ${fileExists}`)
if (fileExists) {
const content = await fs.readFile(backupPath, 'utf-8')
console.log(`Backup file content: ${JSON.stringify(content)}`)
const lines = content.split('\n')
console.log(`First line: ${JSON.stringify(lines[0])}`)
const firstLine = lines[0]?.trim()
console.log(`First line trimmed: ${JSON.stringify(firstLine)}`)
console.log(`Starts with profile marker: ${firstLine?.startsWith('# envctl-profile:')}`)
if (firstLine?.startsWith('# envctl-profile:')) {
const profileName = firstLine.replace('# envctl-profile:', '').trim()
console.log(`Extracted profile name: ${JSON.stringify(profileName)}`)
}
}
const currentProfile = await storage.getCurrentlyLoadedProfile()
console.log(`getCurrentlyLoadedProfile result: ${JSON.stringify(currentProfile)}`)
// Debug isSessionActive
console.log('\n=== Debug: isSessionActive ===')
console.log(`Testing if session ${sessionId} is active...`)
const isActive = storage['isSessionActive'](sessionId)
console.log(`isSessionActive result: ${isActive}`)
// Test PID 1 specifically
console.log(`Testing if PID 1 is active...`)
try {
process.kill(1, 0)
console.log(`PID 1 signal test: SUCCESS (process exists)`)
} catch (error) {
console.log(`PID 1 signal test: FAILED (${error})`)
}
} catch (error) {
console.error('Error:', error instanceof Error ? error.message : error)
process.exit(1)
}
})
*/
program.parse();
if (!process.argv.slice(2).length) {
program.outputHelp();
}
//# sourceMappingURL=cli.js.map