lsh-framework 0.8.2 → 0.9.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/README.md CHANGED
@@ -19,9 +19,44 @@ Traditional secret management tools are either too complex, too expensive, or re
19
19
 
20
20
  ## Quick Start (30 seconds)
21
21
 
22
+ ### New in v0.8.2+: Smart Sync (Easiest Way!)
23
+
22
24
  ```bash
23
25
  # 1. Install
24
- npm install -g gwicho38-lsh
26
+ npm install -g lsh-framework
27
+
28
+ # 2. Configure Supabase (free tier works!)
29
+ # Add to .env:
30
+ # SUPABASE_URL=https://your-project.supabase.co
31
+ # SUPABASE_ANON_KEY=<your-anon-key>
32
+
33
+ # 3. ONE command does everything!
34
+ cd ~/repos/your-project
35
+ lsh lib secrets sync
36
+
37
+ # That's it! Smart Sync:
38
+ # ✅ Auto-generates encryption key
39
+ # ✅ Creates .env from .env.example
40
+ # ✅ Adds .env to .gitignore
41
+ # ✅ Pushes to cloud
42
+ # ✅ Namespaces by repo name
43
+ ```
44
+
45
+ ### Sync AND Load in One Command
46
+
47
+ ```bash
48
+ # Sync and load secrets into current shell
49
+ eval "$(lsh lib secrets sync --load)"
50
+
51
+ # Your secrets are now available!
52
+ echo $DATABASE_URL
53
+ ```
54
+
55
+ ### Traditional Method (Still Works)
56
+
57
+ ```bash
58
+ # 1. Install
59
+ npm install -g lsh-framework
25
60
 
26
61
  # 2. Generate encryption key
27
62
  lsh lib secrets key
@@ -44,6 +79,35 @@ lsh lib secrets pull
44
79
 
45
80
  ## Core Features
46
81
 
82
+ ### 🚀 Smart Sync (New in v0.8.2!)
83
+
84
+ **One command. Zero configuration. Automatic everything.**
85
+
86
+ ```bash
87
+ cd ~/repos/my-app
88
+ lsh lib secrets sync # Auto-setup and sync
89
+ eval "$(lsh lib secrets sync --load)" # Sync AND load into shell
90
+ ```
91
+
92
+ What Smart Sync does automatically:
93
+ - ✅ **Detects git repos** - Namespaces secrets by project name
94
+ - ✅ **Generates keys** - Creates encryption key if missing
95
+ - ✅ **Creates .env** - From .env.example or template
96
+ - ✅ **Updates .gitignore** - Ensures .env is never committed
97
+ - ✅ **Intelligent sync** - Pushes/pulls based on what's newer
98
+ - ✅ **Load mode** - Sync and load with `eval` in one command
99
+
100
+ **Repository Isolation:**
101
+ ```bash
102
+ cd ~/repos/app1
103
+ lsh lib secrets sync # Stored as: app1_dev
104
+
105
+ cd ~/repos/app2
106
+ lsh lib secrets sync # Stored as: app2_dev (separate!)
107
+ ```
108
+
109
+ No more conflicts between projects using the same environment names!
110
+
47
111
  ### 🔐 Secrets Management
48
112
 
49
113
  - **AES-256 Encryption** - Military-grade encryption for all secrets
@@ -52,6 +116,7 @@ lsh lib secrets pull
52
116
  - **Masked Viewing** - View secrets safely without exposing full values
53
117
  - **Automatic Backup** - Never lose your `.env` files
54
118
  - **Version Control** - Track changes to your secrets over time
119
+ - **Smart Sync** - Auto-setup with git repo detection (v0.8.2+)
55
120
 
56
121
  ### 🔄 Automatic Rotation (Unique Feature!)
57
122
 
@@ -536,12 +601,21 @@ lsh lib daemon start
536
601
 
537
602
  ## Documentation
538
603
 
604
+ ### Secrets Management
605
+ - **[SMART_SYNC_GUIDE.md](docs/features/secrets/SMART_SYNC_GUIDE.md)** - 🆕 Smart Sync complete guide (v0.8.2+)
539
606
  - **[SECRETS_GUIDE.md](docs/features/secrets/SECRETS_GUIDE.md)** - Complete secrets management guide
540
607
  - **[SECRETS_QUICK_REFERENCE.md](docs/features/secrets/SECRETS_QUICK_REFERENCE.md)** - Quick reference for daily use
541
608
  - **[SECRETS_CHEATSHEET.txt](SECRETS_CHEATSHEET.txt)** - Command cheatsheet
609
+
610
+ ### Installation & Development
542
611
  - **[INSTALL.md](docs/deployment/INSTALL.md)** - Detailed installation instructions
543
612
  - **[CLAUDE.md](CLAUDE.md)** - Developer guide for contributors
544
613
 
614
+ ### Release Notes
615
+ - **[v0.8.3](docs/releases/0.8.3.md)** - Hotfix: Logger output in load mode
616
+ - **[v0.8.2](docs/releases/0.8.2.md)** - Smart Sync feature release
617
+ - **[v0.8.1](docs/releases/0.8.1.md)** - Previous releases
618
+
545
619
  ## Architecture
546
620
 
547
621
  ### Secrets Flow
package/dist/cli.js CHANGED
@@ -72,13 +72,17 @@ program
72
72
  console.log('LSH - Encrypted Secrets Manager with Automatic Rotation');
73
73
  console.log('');
74
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');
75
+ console.log(' sync Check sync status & get recommendations');
76
+ console.log(' push Upload .env to encrypted cloud storage');
77
+ console.log(' pull Download .env from cloud storage');
78
+ console.log(' list List all stored environments');
79
+ console.log(' show View secrets (masked)');
80
+ console.log(' key Generate encryption key');
81
+ console.log(' create Create new .env file');
82
+ console.log(' get <key> Get a specific secret value');
83
+ console.log(' set <key> <value> Set a specific secret value');
84
+ console.log(' delete Delete .env file');
85
+ console.log(' status Get detailed secrets status');
82
86
  console.log('');
83
87
  console.log('🔄 Automation (Schedule secret rotation):');
84
88
  console.log(' lib cron add Schedule automatic tasks');
@@ -86,9 +90,9 @@ program
86
90
  console.log(' lib daemon start Start persistent daemon');
87
91
  console.log('');
88
92
  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');
93
+ console.log(' lsh key # Generate encryption key');
94
+ console.log(' lsh push --env dev # Push your secrets');
95
+ console.log(' lsh pull --env dev # Pull on another machine');
92
96
  console.log('');
93
97
  console.log('📚 More Commands:');
94
98
  console.log(' lib api API server management');
@@ -92,7 +92,8 @@ export class LSHJobDaemon extends EventEmitter {
92
92
  this.log('INFO', `API Server started on port ${this.config.apiPort}`);
93
93
  }
94
94
  catch (error) {
95
- this.log('ERROR', `Failed to start API server: ${error.message}`);
95
+ const err = error;
96
+ this.log('ERROR', `Failed to start API server: ${err.message}`);
96
97
  }
97
98
  }
98
99
  // Setup cleanup handlers
@@ -151,14 +152,23 @@ export class LSHJobDaemon extends EventEmitter {
151
152
  async getStatus() {
152
153
  const stats = this.jobManager.getJobStats();
153
154
  const uptime = process.uptime();
155
+ const memUsage = process.memoryUsage();
154
156
  return {
155
- isRunning: this.isRunning,
157
+ running: this.isRunning,
156
158
  pid: process.pid,
157
159
  uptime,
158
- jobs: stats,
159
- config: this.config,
160
- memoryUsage: process.memoryUsage(),
161
- cpuUsage: process.cpuUsage()
160
+ jobCount: stats.total || 0,
161
+ memoryUsage: {
162
+ heapUsed: memUsage.heapUsed,
163
+ heapTotal: memUsage.heapTotal,
164
+ external: memUsage.external
165
+ },
166
+ jobs: {
167
+ total: stats.total || 0,
168
+ running: stats.running || 0,
169
+ completed: stats.completed,
170
+ failed: stats.failed
171
+ }
162
172
  };
163
173
  }
164
174
  /**
@@ -425,7 +435,7 @@ export class LSHJobDaemon extends EventEmitter {
425
435
  job.completedAt = undefined;
426
436
  job.stdout = '';
427
437
  job.stderr = '';
428
- this.jobManager.persistJobs();
438
+ await this.jobManager.persistJobs();
429
439
  this.log('INFO', `🔄 Reset completed job for recurring execution: ${job.id} (${job.name})`);
430
440
  }
431
441
  // Track that we're running this job now
@@ -534,7 +544,7 @@ export class LSHJobDaemon extends EventEmitter {
534
544
  job.stderr = '';
535
545
  // Force persistence by calling internal method via reflection
536
546
  // Note: This is a temporary workaround for private method access
537
- this.jobManager.persistJobs();
547
+ await this.jobManager.persistJobs();
538
548
  this.log('INFO', `🔄 Reset recurring job status: ${jobId} (${job.name}) for next scheduled run`);
539
549
  }
540
550
  }
@@ -636,7 +646,7 @@ export class LSHJobDaemon extends EventEmitter {
636
646
  fs.unlinkSync(this.config.socketPath);
637
647
  // Retry after cleanup
638
648
  setTimeout(() => {
639
- this.ipcServer.listen(this.config.socketPath);
649
+ this.ipcServer?.listen(this.config.socketPath);
640
650
  }, 1000);
641
651
  }
642
652
  catch (cleanupError) {
@@ -647,7 +657,7 @@ export class LSHJobDaemon extends EventEmitter {
647
657
  }
648
658
  }
649
659
  async handleIPCMessage(message) {
650
- const { command, args } = message;
660
+ const { command, args = {} } = message;
651
661
  switch (command) {
652
662
  case 'status':
653
663
  return await this.getStatus();
@@ -764,14 +774,13 @@ if (import.meta.url === `file://${process.argv[1]}`) {
764
774
  id: `job_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
765
775
  name: `Manual Job - ${jobCommand}`,
766
776
  command: jobCommand,
767
- type: 'manual',
777
+ type: 'shell',
768
778
  schedule: { interval: 0 }, // Run once
769
779
  env: process.env,
770
780
  cwd: process.cwd(),
771
781
  user: process.env.USER,
772
782
  priority: 5,
773
783
  tags: ['manual'],
774
- enabled: true,
775
784
  maxRetries: 0,
776
785
  timeout: 0,
777
786
  };
@@ -784,7 +793,8 @@ if (import.meta.url === `file://${process.argv[1]}`) {
784
793
  process.exit(0);
785
794
  }
786
795
  catch (error) {
787
- cliLogger.error('Failed to add job', error);
796
+ const err = error;
797
+ cliLogger.error('Failed to add job', err);
788
798
  process.exit(1);
789
799
  }
790
800
  })();
@@ -37,20 +37,21 @@ export class ApiError extends Error {
37
37
  * @param statusCode - HTTP status code (default: 500)
38
38
  */
39
39
  export function sendError(res, error, statusCode) {
40
- const status = statusCode || (error instanceof ApiError ? error.statusCode : 500);
40
+ const err = error instanceof Error ? error : new Error(String(error));
41
+ const status = statusCode || (err instanceof ApiError ? err.statusCode : 500);
41
42
  const response = {
42
- error: error.message || 'An unexpected error occurred',
43
+ error: err.message || 'An unexpected error occurred',
43
44
  timestamp: new Date().toISOString(),
44
45
  };
45
- if (error instanceof ApiError) {
46
- if (error.code)
47
- response.code = error.code;
48
- if (error.details)
49
- response.details = error.details;
46
+ if (err instanceof ApiError) {
47
+ if (err.code)
48
+ response.code = err.code;
49
+ if (err.details)
50
+ response.details = err.details;
50
51
  }
51
52
  // Log error for debugging (in production, use proper logger)
52
53
  if (status >= 500) {
53
- console.error('API Error:', error);
54
+ console.error('API Error:', err);
54
55
  }
55
56
  res.status(status).json(response);
56
57
  }
@@ -137,21 +138,22 @@ export async function handleApiOperation(res, operation, config = {}, webhookTri
137
138
  }
138
139
  }
139
140
  catch (error) {
141
+ const err = error;
140
142
  // Determine appropriate status code
141
143
  let statusCode = 400;
142
- if (error instanceof ApiError) {
143
- statusCode = error.statusCode;
144
+ if (err instanceof ApiError) {
145
+ statusCode = err.statusCode;
144
146
  }
145
- else if (error.message.includes('not found') || error.message.includes('Not found')) {
147
+ else if (err.message.includes('not found') || err.message.includes('Not found')) {
146
148
  statusCode = 404;
147
149
  }
148
- else if (error.message.includes('permission') || error.message.includes('unauthorized')) {
150
+ else if (err.message.includes('permission') || err.message.includes('unauthorized')) {
149
151
  statusCode = 403;
150
152
  }
151
- else if (error.message.includes('exists') || error.message.includes('duplicate')) {
153
+ else if (err.message.includes('exists') || err.message.includes('duplicate')) {
152
154
  statusCode = 409;
153
155
  }
154
- sendError(res, error, statusCode);
156
+ sendError(res, err, statusCode);
155
157
  }
156
158
  }
157
159
  /**
@@ -86,7 +86,8 @@ export class BaseCommandRegistrar {
86
86
  await config.action(...args);
87
87
  }
88
88
  catch (error) {
89
- this.logError('Command failed', error);
89
+ const err = error;
90
+ this.logError('Command failed', err);
90
91
  process.exit(1);
91
92
  }
92
93
  });
@@ -133,14 +134,14 @@ export class BaseCommandRegistrar {
133
134
  logSuccess(message, data) {
134
135
  this.logger.info(message);
135
136
  if (data !== undefined) {
136
- if (typeof data === 'object' && !Array.isArray(data)) {
137
+ if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
137
138
  Object.entries(data).forEach(([key, value]) => {
138
139
  this.logger.info(` ${key}: ${value}`);
139
140
  });
140
141
  }
141
142
  else if (Array.isArray(data)) {
142
143
  data.forEach(item => {
143
- if (typeof item === 'object') {
144
+ if (typeof item === 'object' && item !== null) {
144
145
  this.logger.info(` ${JSON.stringify(item, null, 2)}`);
145
146
  }
146
147
  else {
@@ -230,8 +231,8 @@ export class BaseCommandRegistrar {
230
231
  cron: options.schedule,
231
232
  interval: options.interval ? parseInt(options.interval) : undefined,
232
233
  },
233
- workingDirectory: options.workingDir,
234
- environment: options.env ? this.parseJSON(options.env, 'environment variables') : {},
234
+ cwd: options.workingDir,
235
+ env: options.env ? this.parseJSON(options.env, 'environment variables') : {},
235
236
  tags: options.tags ? this.parseTags(options.tags) : [],
236
237
  priority: options.priority ? parseInt(options.priority) : 5,
237
238
  maxRetries: options.maxRetries ? parseInt(options.maxRetries) : 3,
@@ -288,23 +288,25 @@ export class DaemonClient extends EventEmitter {
288
288
  // Record job execution in database
289
289
  if (this.databasePersistence) {
290
290
  try {
291
+ const jobResult = result;
291
292
  await this.databasePersistence.saveJob({
292
293
  user_id: this.userId,
293
294
  session_id: this.sessionId,
294
295
  job_id: jobId,
295
296
  command: `Triggered execution of ${jobId}`,
296
- status: result.success ? 'completed' : 'failed',
297
+ status: jobResult.success ? 'completed' : 'failed',
297
298
  working_directory: process.cwd(),
298
299
  started_at: new Date().toISOString(),
299
300
  completed_at: new Date().toISOString(),
300
- exit_code: result.success ? 0 : 1,
301
- output: result.output,
302
- error: result.error
301
+ exit_code: jobResult.success ? 0 : 1,
302
+ output: jobResult.output,
303
+ error: jobResult.error
303
304
  });
304
305
  }
305
306
  catch (error) {
306
307
  // Don't fail the trigger if database save fails
307
- this.logger.warn(`Failed to save job execution to database: ${error.message}`);
308
+ const err = error;
309
+ this.logger.warn(`Failed to save job execution to database: ${err.message}`);
308
310
  }
309
311
  }
310
312
  return result;
@@ -339,7 +341,7 @@ export class DaemonClient extends EventEmitter {
339
341
  if (Array.isArray(result)) {
340
342
  return result;
341
343
  }
342
- else if (result && typeof result === 'object' && Array.isArray(result.jobs)) {
344
+ else if (result && typeof result === 'object' && 'jobs' in result && Array.isArray(result.jobs)) {
343
345
  return result.jobs;
344
346
  }
345
347
  else {
@@ -464,15 +466,17 @@ export class DaemonClient extends EventEmitter {
464
466
  calculateJobStatistics(jobs) {
465
467
  const total = jobs.length;
466
468
  const byStatus = jobs.reduce((acc, job) => {
467
- acc[job.status] = (acc[job.status] || 0) + 1;
469
+ const status = String(job.status);
470
+ acc[status] = (acc[status] || 0) + 1;
468
471
  return acc;
469
472
  }, {});
470
- const successRate = byStatus.completed ? (byStatus.completed / total) * 100 : 0;
473
+ const completedCount = byStatus.completed || 0;
474
+ const successRate = total > 0 ? (completedCount / total) * 100 : 0;
471
475
  return {
472
476
  totalJobs: total,
473
477
  byStatus,
474
478
  successRate,
475
- lastExecution: jobs.length > 0 ? jobs[0].started_at : null,
479
+ lastExecution: jobs.length > 0 ? (jobs[0].started_at || null) : null,
476
480
  };
477
481
  }
478
482
  /**
@@ -62,7 +62,7 @@ export class DatabasePersistence {
62
62
  if (this.userId !== undefined) {
63
63
  insertData.user_id = this.userId;
64
64
  }
65
- const { _data, error } = await this.client
65
+ const { error } = await this.client
66
66
  .from('shell_history')
67
67
  .insert([insertData]);
68
68
  if (error) {
@@ -120,7 +120,7 @@ export class DatabasePersistence {
120
120
  if (this.userId !== undefined) {
121
121
  insertData.user_id = this.userId;
122
122
  }
123
- const { _data, error } = await this.client
123
+ const { error } = await this.client
124
124
  .from('shell_jobs')
125
125
  .insert([insertData]);
126
126
  if (error) {
@@ -160,7 +160,7 @@ export class DatabasePersistence {
160
160
  else {
161
161
  query = query.is('user_id', null);
162
162
  }
163
- const { _data, error } = await query;
163
+ const { error } = await query;
164
164
  if (error) {
165
165
  console.error('Failed to update job status:', error);
166
166
  return false;
@@ -215,7 +215,7 @@ export class DatabasePersistence {
215
215
  if (this.userId !== undefined) {
216
216
  upsertData.user_id = this.userId;
217
217
  }
218
- const { _data, error } = await this.client
218
+ const { error } = await this.client
219
219
  .from('shell_configuration')
220
220
  .upsert([upsertData], {
221
221
  onConflict: 'user_id,config_key'
@@ -276,7 +276,7 @@ export class DatabasePersistence {
276
276
  if (this.userId !== undefined) {
277
277
  upsertData.user_id = this.userId;
278
278
  }
279
- const { _data, error } = await this.client
279
+ const { error } = await this.client
280
280
  .from('shell_aliases')
281
281
  .upsert([upsertData], {
282
282
  onConflict: 'user_id,alias_name'
@@ -334,7 +334,7 @@ export class DatabasePersistence {
334
334
  if (this.userId !== undefined) {
335
335
  upsertData.user_id = this.userId;
336
336
  }
337
- const { _data, error } = await this.client
337
+ const { error } = await this.client
338
338
  .from('shell_functions')
339
339
  .upsert([upsertData], {
340
340
  onConflict: 'user_id,function_name'
@@ -397,7 +397,7 @@ export class DatabasePersistence {
397
397
  if (this.userId !== undefined) {
398
398
  insertData.user_id = this.userId;
399
399
  }
400
- const { _data, error } = await this.client
400
+ const { error } = await this.client
401
401
  .from('shell_sessions')
402
402
  .insert([insertData]);
403
403
  if (error) {
@@ -431,7 +431,7 @@ export class DatabasePersistence {
431
431
  else {
432
432
  query = query.is('user_id', null);
433
433
  }
434
- const { _data, error } = await query;
434
+ const { error } = await query;
435
435
  if (error) {
436
436
  console.error('Failed to end session:', error);
437
437
  return false;
@@ -232,13 +232,10 @@ export function printValidationResults(result, exitOnError = false) {
232
232
  result.warnings.forEach(warn => console.warn(` - ${warn}`));
233
233
  }
234
234
  if (result.recommendations.length > 0) {
235
- // eslint-disable-next-line no-console
236
235
  console.log('\nℹ️ Environment Variable Recommendations:');
237
- // eslint-disable-next-line no-console
238
236
  result.recommendations.forEach(rec => console.log(` - ${rec}`));
239
237
  }
240
238
  if (result.isValid) {
241
- // eslint-disable-next-line no-console
242
239
  console.log('\n✅ Environment validation passed');
243
240
  }
244
241
  else {
@@ -224,7 +224,6 @@ export class Logger {
224
224
  else {
225
225
  // INFO and DEBUG go to stdout
226
226
  // Using console.log here is intentional for the logger itself
227
- // eslint-disable-next-line no-console
228
227
  console.log(output);
229
228
  }
230
229
  }