lsh-framework 0.6.0 → 0.8.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,39 @@ 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(' secrets sync Check sync status & get recommendations');
76
+ console.log(' secrets push Upload .env to encrypted cloud storage');
77
+ console.log(' secrets pull Download .env from cloud storage');
78
+ console.log(' secrets list List all stored environments');
79
+ console.log(' secrets show View secrets (masked)');
80
+ console.log(' secrets key Generate encryption key');
81
+ console.log(' 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 secrets key # Generate encryption key');
90
+ console.log(' lsh secrets push --env dev # Push your secrets');
91
+ console.log(' lsh 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(' lib daemon Daemon management');
97
+ console.log(' lib cron Cron job management');
98
+ console.log(' self Self-management commands');
99
+ console.log(' self zsh ZSH compatibility commands');
100
+ console.log(' -i, --interactive Start interactive shell');
101
+ console.log(' --help Show all options');
102
+ console.log('');
103
+ console.log('📖 Documentation: https://github.com/gwicho38/lsh');
69
104
  }
70
105
  }
71
106
  catch (error) {
@@ -105,8 +140,10 @@ program
105
140
  process.exit(1);
106
141
  }
107
142
  });
108
- // ZSH compatibility subcommand
109
- program
143
+ // Self-management commands
144
+ program.addCommand(selfCommand);
145
+ // ZSH compatibility commands (under self)
146
+ selfCommand
110
147
  .command('zsh')
111
148
  .description('ZSH compatibility commands')
112
149
  .option('--migrate', 'Migrate ZSH configuration to LSH')
@@ -121,8 +158,6 @@ program
121
158
  process.exit(1);
122
159
  }
123
160
  });
124
- // Self-management commands
125
- program.addCommand(selfCommand);
126
161
  // Help subcommand
127
162
  program
128
163
  .command('help')
@@ -130,26 +165,123 @@ program
130
165
  .action(() => {
131
166
  showDetailedHelp();
132
167
  });
168
+ /**
169
+ * Calculate string similarity (Levenshtein distance)
170
+ */
171
+ function similarity(s1, s2) {
172
+ const longer = s1.length > s2.length ? s1 : s2;
173
+ const shorter = s1.length > s2.length ? s2 : s1;
174
+ if (longer.length === 0)
175
+ return 1.0;
176
+ const editDistance = levenshtein(longer, shorter);
177
+ return (longer.length - editDistance) / longer.length;
178
+ }
179
+ function levenshtein(s1, s2) {
180
+ const costs = [];
181
+ for (let i = 0; i <= s1.length; i++) {
182
+ let lastValue = i;
183
+ for (let j = 0; j <= s2.length; j++) {
184
+ if (i === 0) {
185
+ costs[j] = j;
186
+ }
187
+ else if (j > 0) {
188
+ let newValue = costs[j - 1];
189
+ if (s1.charAt(i - 1) !== s2.charAt(j - 1)) {
190
+ newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1;
191
+ }
192
+ costs[j - 1] = lastValue;
193
+ lastValue = newValue;
194
+ }
195
+ }
196
+ if (i > 0)
197
+ costs[s2.length] = lastValue;
198
+ }
199
+ return costs[s2.length];
200
+ }
201
+ /**
202
+ * Find similar commands for suggestions
203
+ */
204
+ function findSimilarCommands(input, validCommands) {
205
+ const similarities = validCommands
206
+ .map(cmd => ({ cmd, score: similarity(input, cmd) }))
207
+ .filter(item => item.score > 0.5) // Only suggest if similarity > 50%
208
+ .sort((a, b) => b.score - a.score)
209
+ .slice(0, 3); // Top 3 suggestions
210
+ return similarities.map(item => item.cmd);
211
+ }
133
212
  // Register async command modules
134
213
  (async () => {
135
214
  // REPL interactive shell
136
215
  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
216
+ // Library commands (parent for service commands)
217
+ const libCommand = await init_lib(program);
218
+ // Nest service commands under lib
219
+ await init_supabase(libCommand);
220
+ await init_daemon(libCommand);
221
+ await init_cron(libCommand);
222
+ registerApiCommands(libCommand);
223
+ // Secrets as top-level command
146
224
  await init_secrets(program);
147
- // API server commands
148
- registerApiCommands(program);
149
- // ZSH import commands
150
- registerZshImportCommands(program);
151
- // Theme commands
152
- registerThemeCommands(program);
225
+ // Self-management commands with nested utilities
226
+ registerZshImportCommands(selfCommand);
227
+ registerThemeCommands(selfCommand);
228
+ // Pre-parse check for unknown commands
229
+ const args = process.argv.slice(2);
230
+ if (args.length > 0) {
231
+ const firstArg = args[0];
232
+ const validCommands = program.commands.map(cmd => cmd.name());
233
+ const validOptions = ['-i', '--interactive', '-c', '--command', '-s', '--script',
234
+ '--rc', '--zsh-compat', '--source-zshrc', '--package-manager',
235
+ '-v', '--verbose', '-d', '--debug', '-h', '--help', '-V', '--version'];
236
+ // Check if first argument looks like a command but isn't valid
237
+ if (!firstArg.startsWith('-') &&
238
+ !validCommands.includes(firstArg) &&
239
+ !validOptions.some(opt => args.includes(opt))) {
240
+ const suggestions = findSimilarCommands(firstArg, validCommands);
241
+ console.error(`error: unknown command '${firstArg}'`);
242
+ if (suggestions.length > 0) {
243
+ console.error(`\nDid you mean one of these?`);
244
+ suggestions.forEach(cmd => console.error(` ${cmd}`));
245
+ }
246
+ console.error(`\nRun 'lsh --help' to see available commands.`);
247
+ process.exit(1);
248
+ }
249
+ }
250
+ // Configure custom error output for better suggestions
251
+ program.configureOutput({
252
+ writeErr: (str) => {
253
+ // Intercept error messages to add suggestions
254
+ if (str.includes('error: unknown command')) {
255
+ const match = str.match(/unknown command '([^']+)'/);
256
+ if (match) {
257
+ const unknownCommand = match[1];
258
+ const validCommands = program.commands.map(cmd => cmd.name());
259
+ const suggestions = findSimilarCommands(unknownCommand, validCommands);
260
+ process.stderr.write(str);
261
+ if (suggestions.length > 0) {
262
+ process.stderr.write(`\nDid you mean one of these?\n`);
263
+ suggestions.forEach(cmd => process.stderr.write(` ${cmd}\n`));
264
+ }
265
+ process.stderr.write(`\nRun 'lsh --help' to see available commands.\n`);
266
+ return;
267
+ }
268
+ }
269
+ process.stderr.write(str);
270
+ }
271
+ });
272
+ // Add custom error handler for unknown commands
273
+ program.on('command:*', (operands) => {
274
+ const unknownCommand = operands[0];
275
+ const validCommands = program.commands.map(cmd => cmd.name());
276
+ const suggestions = findSimilarCommands(unknownCommand, validCommands);
277
+ console.error(`error: unknown command '${unknownCommand}'`);
278
+ if (suggestions.length > 0) {
279
+ console.error(`\nDid you mean one of these?`);
280
+ suggestions.forEach(cmd => console.error(` ${cmd}`));
281
+ }
282
+ console.error(`\nRun 'lsh --help' to see available commands.`);
283
+ process.exit(1);
284
+ });
153
285
  // Parse command line arguments after all commands are registered
154
286
  program.parse(process.argv);
155
287
  })();
@@ -445,22 +577,28 @@ function showDetailedHelp() {
445
577
  console.log(' -V, --version Show version');
446
578
  console.log('');
447
579
  console.log('Subcommands:');
580
+ console.log(' secrets Secrets management (primary feature)');
448
581
  console.log(' repl JavaScript REPL interactive shell');
449
- console.log(' lib Library commands');
450
- console.log(' supabase Supabase database management');
451
582
  console.log(' script <file> Execute shell script');
452
583
  console.log(' config Manage configuration');
453
- 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
584
  console.log(' help Show detailed help');
463
585
  console.log('');
586
+ console.log('Self-Management (lsh self <command>):');
587
+ console.log(' self update Update to latest version');
588
+ console.log(' self version Show version information');
589
+ console.log(' self uninstall Uninstall LSH from system');
590
+ console.log(' self theme Manage themes (import Oh-My-Zsh themes)');
591
+ console.log(' self zsh ZSH compatibility commands');
592
+ console.log(' self zsh-import Import ZSH configs (aliases, functions, exports)');
593
+ console.log('');
594
+ console.log('Library Commands (lsh lib <command>):');
595
+ console.log(' lib api API server management');
596
+ console.log(' lib supabase Supabase database management');
597
+ console.log(' lib daemon Daemon management');
598
+ console.log(' lib daemon job Job management');
599
+ console.log(' lib daemon db Database integration');
600
+ console.log(' lib cron Cron job management');
601
+ console.log('');
464
602
  console.log('Examples:');
465
603
  console.log('');
466
604
  console.log(' Shell Usage:');
@@ -476,18 +614,26 @@ function showDetailedHelp() {
476
614
  console.log(' lsh self version # Show version');
477
615
  console.log(' lsh self update # Update to latest');
478
616
  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');
617
+ console.log(' Self-Management:');
618
+ console.log(' lsh self update # Update to latest version');
619
+ console.log(' lsh self version # Show version');
620
+ console.log(' lsh self theme list # List available themes');
621
+ console.log(' lsh self theme import robbyrussell # Import Oh-My-Zsh theme');
622
+ console.log(' lsh self zsh-import aliases # Import ZSH aliases');
623
+ console.log('');
624
+ console.log(' Secrets Management:');
625
+ console.log(' lsh secrets sync # Check sync status');
626
+ console.log(' lsh secrets push # Push secrets to cloud');
627
+ console.log(' lsh secrets pull # Pull secrets from cloud');
628
+ console.log(' lsh secrets list # List environments');
485
629
  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');
630
+ console.log(' Library Services:');
631
+ console.log(' lsh lib daemon start # Start daemon');
632
+ console.log(' lsh lib daemon status # Check daemon status');
633
+ console.log(' lsh lib daemon job list # List all jobs');
634
+ console.log(' lsh lib cron list # List cron jobs');
635
+ console.log(' lsh lib api start # Start API server');
636
+ console.log(' lsh lib api key # Generate API key');
491
637
  console.log('');
492
638
  console.log('Features:');
493
639
  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