lsh-framework 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -36,8 +36,12 @@ function getVersion() {
36
36
  const program = new Command();
37
37
  program
38
38
  .name('lsh')
39
- .description('LSH - A modern shell with ZSH features and superior job management')
40
- .version(getVersion());
39
+ .description('LSH - Encrypted secrets manager with automatic rotation and team sync')
40
+ .version(getVersion())
41
+ .showSuggestionAfterError(true)
42
+ .showHelpAfterError('(add --help for additional information)')
43
+ .allowUnknownOption(false)
44
+ .enablePositionalOptions();
41
45
  // Options for main command
42
46
  program
43
47
  .option('-i, --interactive', 'Start interactive shell')
@@ -64,8 +68,36 @@ program
64
68
  await startInteractiveShell(options);
65
69
  }
66
70
  else {
67
- // No arguments - show help
68
- program.help();
71
+ // No arguments - show secrets-focused help
72
+ console.log('LSH - Encrypted Secrets Manager with Automatic Rotation');
73
+ console.log('');
74
+ console.log('🔐 Secrets Management (Primary Features):');
75
+ console.log(' lib secrets sync Check sync status & get recommendations');
76
+ console.log(' lib secrets push Upload .env to encrypted cloud storage');
77
+ console.log(' lib secrets pull Download .env from cloud storage');
78
+ console.log(' lib secrets list List all stored environments');
79
+ console.log(' lib secrets show View secrets (masked)');
80
+ console.log(' lib secrets key Generate encryption key');
81
+ console.log(' lib secrets create Create new .env file');
82
+ console.log('');
83
+ console.log('🔄 Automation (Schedule secret rotation):');
84
+ console.log(' lib cron add Schedule automatic tasks');
85
+ console.log(' lib cron list List scheduled jobs');
86
+ console.log(' lib daemon start Start persistent daemon');
87
+ console.log('');
88
+ console.log('🚀 Quick Start:');
89
+ console.log(' lsh lib secrets key # Generate encryption key');
90
+ console.log(' lsh lib secrets push --env dev # Push your secrets');
91
+ console.log(' lsh lib secrets pull --env dev # Pull on another machine');
92
+ console.log('');
93
+ console.log('📚 More Commands:');
94
+ console.log(' lib api API server management');
95
+ console.log(' lib supabase Supabase database management');
96
+ console.log(' self Self-management commands');
97
+ console.log(' -i, --interactive Start interactive shell');
98
+ console.log(' --help Show all options');
99
+ console.log('');
100
+ console.log('📖 Documentation: https://github.com/gwicho38/lsh');
69
101
  }
70
102
  }
71
103
  catch (error) {
@@ -130,26 +162,122 @@ program
130
162
  .action(() => {
131
163
  showDetailedHelp();
132
164
  });
165
+ /**
166
+ * Calculate string similarity (Levenshtein distance)
167
+ */
168
+ function similarity(s1, s2) {
169
+ const longer = s1.length > s2.length ? s1 : s2;
170
+ const shorter = s1.length > s2.length ? s2 : s1;
171
+ if (longer.length === 0)
172
+ return 1.0;
173
+ const editDistance = levenshtein(longer, shorter);
174
+ return (longer.length - editDistance) / longer.length;
175
+ }
176
+ function levenshtein(s1, s2) {
177
+ const costs = [];
178
+ for (let i = 0; i <= s1.length; i++) {
179
+ let lastValue = i;
180
+ for (let j = 0; j <= s2.length; j++) {
181
+ if (i === 0) {
182
+ costs[j] = j;
183
+ }
184
+ else if (j > 0) {
185
+ let newValue = costs[j - 1];
186
+ if (s1.charAt(i - 1) !== s2.charAt(j - 1)) {
187
+ newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1;
188
+ }
189
+ costs[j - 1] = lastValue;
190
+ lastValue = newValue;
191
+ }
192
+ }
193
+ if (i > 0)
194
+ costs[s2.length] = lastValue;
195
+ }
196
+ return costs[s2.length];
197
+ }
198
+ /**
199
+ * Find similar commands for suggestions
200
+ */
201
+ function findSimilarCommands(input, validCommands) {
202
+ const similarities = validCommands
203
+ .map(cmd => ({ cmd, score: similarity(input, cmd) }))
204
+ .filter(item => item.score > 0.5) // Only suggest if similarity > 50%
205
+ .sort((a, b) => b.score - a.score)
206
+ .slice(0, 3); // Top 3 suggestions
207
+ return similarities.map(item => item.cmd);
208
+ }
133
209
  // Register async command modules
134
210
  (async () => {
135
211
  // REPL interactive shell
136
212
  await init_ishell(program);
137
- // Library commands
138
- await init_lib(program);
139
- // Supabase commands
140
- await init_supabase(program);
141
- // Daemon management commands
142
- await init_daemon(program);
143
- // Cron commands
144
- await init_cron(program);
145
- // Secrets management commands
146
- await init_secrets(program);
147
- // API server commands
148
- registerApiCommands(program);
149
- // ZSH import commands
150
- registerZshImportCommands(program);
151
- // Theme commands
152
- registerThemeCommands(program);
213
+ // Library commands (parent for service commands)
214
+ const libCommand = await init_lib(program);
215
+ // Nest service commands under lib
216
+ await init_supabase(libCommand);
217
+ await init_daemon(libCommand);
218
+ await init_cron(libCommand);
219
+ await init_secrets(libCommand);
220
+ registerApiCommands(libCommand);
221
+ // Self-management commands with nested utilities
222
+ registerZshImportCommands(selfCommand);
223
+ registerThemeCommands(selfCommand);
224
+ // Pre-parse check for unknown commands
225
+ const args = process.argv.slice(2);
226
+ if (args.length > 0) {
227
+ const firstArg = args[0];
228
+ const validCommands = program.commands.map(cmd => cmd.name());
229
+ const validOptions = ['-i', '--interactive', '-c', '--command', '-s', '--script',
230
+ '--rc', '--zsh-compat', '--source-zshrc', '--package-manager',
231
+ '-v', '--verbose', '-d', '--debug', '-h', '--help', '-V', '--version'];
232
+ // Check if first argument looks like a command but isn't valid
233
+ if (!firstArg.startsWith('-') &&
234
+ !validCommands.includes(firstArg) &&
235
+ !validOptions.some(opt => args.includes(opt))) {
236
+ const suggestions = findSimilarCommands(firstArg, validCommands);
237
+ console.error(`error: unknown command '${firstArg}'`);
238
+ if (suggestions.length > 0) {
239
+ console.error(`\nDid you mean one of these?`);
240
+ suggestions.forEach(cmd => console.error(` ${cmd}`));
241
+ }
242
+ console.error(`\nRun 'lsh --help' to see available commands.`);
243
+ process.exit(1);
244
+ }
245
+ }
246
+ // Configure custom error output for better suggestions
247
+ program.configureOutput({
248
+ writeErr: (str) => {
249
+ // Intercept error messages to add suggestions
250
+ if (str.includes('error: unknown command')) {
251
+ const match = str.match(/unknown command '([^']+)'/);
252
+ if (match) {
253
+ const unknownCommand = match[1];
254
+ const validCommands = program.commands.map(cmd => cmd.name());
255
+ const suggestions = findSimilarCommands(unknownCommand, validCommands);
256
+ process.stderr.write(str);
257
+ if (suggestions.length > 0) {
258
+ process.stderr.write(`\nDid you mean one of these?\n`);
259
+ suggestions.forEach(cmd => process.stderr.write(` ${cmd}\n`));
260
+ }
261
+ process.stderr.write(`\nRun 'lsh --help' to see available commands.\n`);
262
+ return;
263
+ }
264
+ }
265
+ process.stderr.write(str);
266
+ }
267
+ });
268
+ // Add custom error handler for unknown commands
269
+ program.on('command:*', (operands) => {
270
+ const unknownCommand = operands[0];
271
+ const validCommands = program.commands.map(cmd => cmd.name());
272
+ const suggestions = findSimilarCommands(unknownCommand, validCommands);
273
+ console.error(`error: unknown command '${unknownCommand}'`);
274
+ if (suggestions.length > 0) {
275
+ console.error(`\nDid you mean one of these?`);
276
+ suggestions.forEach(cmd => console.error(` ${cmd}`));
277
+ }
278
+ console.error(`\nRun 'lsh --help' to see available commands.`);
279
+ process.exit(1);
280
+ });
153
281
  // Parse command line arguments after all commands are registered
154
282
  program.parse(process.argv);
155
283
  })();
@@ -446,21 +574,27 @@ function showDetailedHelp() {
446
574
  console.log('');
447
575
  console.log('Subcommands:');
448
576
  console.log(' repl JavaScript REPL interactive shell');
449
- console.log(' lib Library commands');
450
- console.log(' supabase Supabase database management');
451
577
  console.log(' script <file> Execute shell script');
452
578
  console.log(' config Manage configuration');
453
579
  console.log(' zsh ZSH compatibility commands');
454
- console.log(' zsh-import Import ZSH configs (aliases, functions, exports)');
455
- console.log(' theme Manage themes (import Oh-My-Zsh themes)');
456
- console.log(' self Self-management (update, version)');
457
- console.log(' daemon Daemon management');
458
- console.log(' daemon job Job management');
459
- console.log(' daemon db Database integration');
460
- console.log(' cron Cron job management');
461
- console.log(' api API server management');
462
580
  console.log(' help Show detailed help');
463
581
  console.log('');
582
+ console.log('Self-Management (lsh self <command>):');
583
+ console.log(' self update Update to latest version');
584
+ console.log(' self version Show version information');
585
+ console.log(' self uninstall Uninstall LSH from system');
586
+ console.log(' self theme Manage themes (import Oh-My-Zsh themes)');
587
+ console.log(' self zsh-import Import ZSH configs (aliases, functions, exports)');
588
+ console.log('');
589
+ console.log('Library Commands (lsh lib <command>):');
590
+ console.log(' lib api API server management');
591
+ console.log(' lib supabase Supabase database management');
592
+ console.log(' lib daemon Daemon management');
593
+ console.log(' lib daemon job Job management');
594
+ console.log(' lib daemon db Database integration');
595
+ console.log(' lib cron Cron job management');
596
+ console.log(' lib secrets Secrets management');
597
+ console.log('');
464
598
  console.log('Examples:');
465
599
  console.log('');
466
600
  console.log(' Shell Usage:');
@@ -476,18 +610,22 @@ function showDetailedHelp() {
476
610
  console.log(' lsh self version # Show version');
477
611
  console.log(' lsh self update # Update to latest');
478
612
  console.log('');
479
- console.log(' Daemon & Job Management:');
480
- console.log(' lsh daemon start # Start daemon');
481
- console.log(' lsh daemon status # Check daemon status');
482
- console.log(' lsh daemon job list # List all jobs');
483
- console.log(' lsh daemon job create # Create new job');
484
- console.log(' lsh daemon job trigger <id> # Run job immediately');
613
+ console.log(' Self-Management:');
614
+ console.log(' lsh self update # Update to latest version');
615
+ console.log(' lsh self version # Show version');
616
+ console.log(' lsh self theme list # List available themes');
617
+ console.log(' lsh self theme import robbyrussell # Import Oh-My-Zsh theme');
618
+ console.log(' lsh self zsh-import aliases # Import ZSH aliases');
485
619
  console.log('');
486
- console.log(' API Server:');
487
- console.log(' lsh api start # Start daemon with API');
488
- console.log(' lsh api key # Generate API key');
489
- console.log(' lsh api test # Test API connection');
490
- console.log(' lsh api example -l python # Show Python client code');
620
+ console.log(' Library Services:');
621
+ console.log(' lsh lib daemon start # Start daemon');
622
+ console.log(' lsh lib daemon status # Check daemon status');
623
+ console.log(' lsh lib daemon job list # List all jobs');
624
+ console.log(' lsh lib cron list # List cron jobs');
625
+ console.log(' lsh lib secrets push # Push secrets to cloud');
626
+ console.log(' lsh lib secrets list # List environments');
627
+ console.log(' lsh lib api start # Start API server');
628
+ console.log(' lsh lib api key # Generate API key');
491
629
  console.log('');
492
630
  console.log('Features:');
493
631
  console.log(' ✅ POSIX Shell Compliance (85-95%)');
@@ -60,7 +60,7 @@ async function fetchLatestVersion() {
60
60
  const options = {
61
61
  hostname: 'registry.npmjs.org',
62
62
  port: 443,
63
- path: '/gwicho38-lsh',
63
+ path: '/lsh-framework',
64
64
  method: 'GET',
65
65
  headers: {
66
66
  'User-Agent': 'lsh-cli',
@@ -239,7 +239,7 @@ selfCommand
239
239
  // Install update
240
240
  console.log(chalk.cyan(`📦 Installing lsh ${latestVersion}...`));
241
241
  const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
242
- const updateProcess = spawn(npmCmd, ['install', '-g', 'gwicho38-lsh@latest'], {
242
+ const updateProcess = spawn(npmCmd, ['install', '-g', 'lsh-framework@latest'], {
243
243
  stdio: 'inherit',
244
244
  });
245
245
  updateProcess.on('close', (code) => {
@@ -249,7 +249,7 @@ selfCommand
249
249
  }
250
250
  else {
251
251
  console.log(chalk.red('✗ Update failed'));
252
- console.log(chalk.yellow('ℹ Try running with sudo: sudo npm install -g gwicho38-lsh@latest'));
252
+ console.log(chalk.yellow('ℹ Try running with sudo: sudo npm install -g lsh-framework@latest'));
253
253
  }
254
254
  });
255
255
  }
@@ -315,4 +315,60 @@ selfCommand
315
315
  console.log();
316
316
  console.log(chalk.dim('For more info, visit: https://github.com/gwicho38/lsh'));
317
317
  });
318
+ /**
319
+ * Uninstall command - remove LSH from the system
320
+ */
321
+ selfCommand
322
+ .command('uninstall')
323
+ .description('Uninstall LSH from your system')
324
+ .option('-y, --yes', 'Skip confirmation prompt')
325
+ .action(async (options) => {
326
+ try {
327
+ console.log(chalk.yellow('╔════════════════════════════════════╗'));
328
+ console.log(chalk.yellow('║ Uninstall LSH Framework ║'));
329
+ console.log(chalk.yellow('╚════════════════════════════════════╝'));
330
+ console.log();
331
+ // Ask for confirmation unless --yes flag is used
332
+ if (!options.yes) {
333
+ const readline = await import('readline');
334
+ const rl = readline.createInterface({
335
+ input: process.stdin,
336
+ output: process.stdout,
337
+ });
338
+ const answer = await new Promise((resolve) => {
339
+ rl.question(chalk.yellow('Are you sure you want to uninstall LSH? (y/N) '), (ans) => {
340
+ rl.close();
341
+ resolve(ans);
342
+ });
343
+ });
344
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
345
+ console.log(chalk.yellow('Uninstall cancelled'));
346
+ return;
347
+ }
348
+ }
349
+ console.log(chalk.cyan('📦 Uninstalling lsh-framework...'));
350
+ console.log();
351
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
352
+ const uninstallProcess = spawn(npmCmd, ['uninstall', '-g', 'lsh-framework'], {
353
+ stdio: 'inherit',
354
+ });
355
+ uninstallProcess.on('close', (code) => {
356
+ if (code === 0) {
357
+ console.log();
358
+ console.log(chalk.green('✓ LSH has been uninstalled successfully'));
359
+ console.log();
360
+ console.log(chalk.dim('Thank you for using LSH!'));
361
+ console.log(chalk.dim('You can reinstall anytime with: npm install -g lsh-framework'));
362
+ }
363
+ else {
364
+ console.log();
365
+ console.log(chalk.red('✗ Uninstall failed'));
366
+ console.log(chalk.yellow('ℹ Try running with sudo: sudo npm uninstall -g lsh-framework'));
367
+ }
368
+ });
369
+ }
370
+ catch (error) {
371
+ console.error(chalk.red('✗ Error during uninstall:'), error);
372
+ }
373
+ });
318
374
  export default selfCommand;
@@ -61,7 +61,8 @@ export function registerThemeCommands(program) {
61
61
  console.log(chalk.dim(' lsh theme import <name> # For Oh-My-Zsh themes'));
62
62
  console.log(chalk.dim(' lsh theme apply <name>'));
63
63
  console.log('');
64
- process.exit(0);
64
+ // Note: Removed process.exit(0) to allow proper Jest testing
65
+ // Commander will handle exit automatically
65
66
  });
66
67
  themeCommand
67
68
  .command('import <name>')
@@ -203,7 +203,8 @@ export class BaseJobManager extends EventEmitter {
203
203
  async removeJob(jobId, force = false) {
204
204
  const job = await this.getJob(jobId);
205
205
  if (!job) {
206
- throw new Error(`Job ${jobId} not found`);
206
+ // Return false instead of throwing when job doesn't exist
207
+ return false;
207
208
  }
208
209
  // Check if job is running
209
210
  if (job.status === 'running' && !force) {
@@ -14,13 +14,54 @@ export class JobManager extends BaseJobManager {
14
14
  nextJobId = 1;
15
15
  persistenceFile;
16
16
  schedulerInterval;
17
+ initPromise;
17
18
  constructor(persistenceFile = '/tmp/lsh-jobs.json') {
18
19
  super(new MemoryJobStorage(), 'JobManager');
19
20
  this.persistenceFile = persistenceFile;
20
- this.loadPersistedJobs();
21
+ this.initPromise = this.loadPersistedJobs();
21
22
  this.startScheduler();
22
23
  this.setupCleanupHandlers();
23
24
  }
25
+ /**
26
+ * Wait for initialization to complete
27
+ */
28
+ async ready() {
29
+ await this.initPromise;
30
+ }
31
+ /**
32
+ * Create a job and persist to filesystem
33
+ */
34
+ async createJob(spec) {
35
+ const job = await super.createJob(spec);
36
+ await this.persistJobs();
37
+ return job;
38
+ }
39
+ /**
40
+ * Update a job and persist to filesystem
41
+ */
42
+ async updateJob(jobId, updates) {
43
+ const job = await super.updateJob(jobId, updates);
44
+ await this.persistJobs();
45
+ return job;
46
+ }
47
+ /**
48
+ * Remove a job and persist to filesystem
49
+ */
50
+ async removeJob(jobId, force = false) {
51
+ const result = await super.removeJob(jobId, force);
52
+ if (result) {
53
+ await this.persistJobs();
54
+ }
55
+ return result;
56
+ }
57
+ /**
58
+ * Update job status and persist to filesystem
59
+ */
60
+ async updateJobStatus(jobId, status, additionalUpdates) {
61
+ const job = await super.updateJobStatus(jobId, status, additionalUpdates);
62
+ await this.persistJobs();
63
+ return job;
64
+ }
24
65
  /**
25
66
  * Start a job (execute it as a process)
26
67
  */
@@ -67,14 +108,19 @@ export class JobManager extends BaseJobManager {
67
108
  this.emit('jobOutput', job.id, 'stderr', data.toString());
68
109
  });
69
110
  // Handle completion
70
- job.process.on('exit', (code, signal) => {
111
+ job.process.on('exit', async (code, signal) => {
112
+ // Check if job still exists (might have been removed during cleanup)
113
+ const existingJob = await this.getJob(job.id);
114
+ if (!existingJob) {
115
+ this.logger.debug(`Job ${job.id} already removed, skipping status update`);
116
+ return;
117
+ }
71
118
  const status = code === 0 ? 'completed' : (signal === 'SIGKILL' ? 'killed' : 'failed');
72
- this.updateJobStatus(job.id, status, {
119
+ await this.updateJobStatus(job.id, status, {
73
120
  completedAt: new Date(),
74
121
  exitCode: code || undefined,
75
122
  });
76
123
  this.emit('jobCompleted', job, code, signal);
77
- this.persistJobs();
78
124
  });
79
125
  // Set timeout if specified
80
126
  if (job.timeout) {
@@ -87,7 +133,6 @@ export class JobManager extends BaseJobManager {
87
133
  startedAt: new Date(),
88
134
  pid: job.pid,
89
135
  });
90
- await this.persistJobs();
91
136
  return updatedJob;
92
137
  }
93
138
  catch (error) {
@@ -96,7 +141,6 @@ export class JobManager extends BaseJobManager {
96
141
  stderr: error.message,
97
142
  });
98
143
  this.emit('jobFailed', job, error);
99
- await this.persistJobs();
100
144
  throw error;
101
145
  }
102
146
  }
@@ -131,7 +175,6 @@ export class JobManager extends BaseJobManager {
131
175
  const updatedJob = await this.updateJobStatus(jobId, 'stopped', {
132
176
  completedAt: new Date(),
133
177
  });
134
- await this.persistJobs();
135
178
  return updatedJob;
136
179
  }
137
180
  /**
@@ -289,6 +332,11 @@ export class JobManager extends BaseJobManager {
289
332
  try {
290
333
  if (fs.existsSync(this.persistenceFile)) {
291
334
  const data = fs.readFileSync(this.persistenceFile, 'utf8');
335
+ // Handle empty file
336
+ if (!data || data.trim() === '') {
337
+ this.logger.info('Persistence file is empty, starting fresh');
338
+ return;
339
+ }
292
340
  const persistedJobs = JSON.parse(data);
293
341
  for (const job of persistedJobs) {
294
342
  // Convert date strings back to Date objects
@@ -11,7 +11,9 @@ const __dirname = path.dirname(__filename);
11
11
  export class LshrcManager {
12
12
  lshrcPath;
13
13
  constructor(lshrcPath) {
14
- this.lshrcPath = lshrcPath || path.join(os.homedir(), '.lshrc');
14
+ // Use process.env.HOME if set (for testability), fallback to os.homedir()
15
+ const homeDir = process.env.HOME || os.homedir();
16
+ this.lshrcPath = lshrcPath || path.join(homeDir, '.lshrc');
15
17
  }
16
18
  /**
17
19
  * Initialize .lshrc if it doesn't exist