lsh-framework 0.10.2 → 1.1.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
@@ -32,7 +32,7 @@ npm install -g lsh-framework
32
32
 
33
33
  # 3. ONE command does everything!
34
34
  cd ~/repos/your-project
35
- lsh lib secrets sync
35
+ lsh sync
36
36
 
37
37
  # That's it! Smart Sync:
38
38
  # ✅ Auto-generates encryption key
@@ -46,7 +46,7 @@ lsh lib secrets sync
46
46
 
47
47
  ```bash
48
48
  # Sync and load secrets into current shell
49
- eval "$(lsh lib secrets sync --load)"
49
+ eval "$(lsh sync --load)"
50
50
 
51
51
  # Your secrets are now available!
52
52
  echo $DATABASE_URL
@@ -59,7 +59,7 @@ echo $DATABASE_URL
59
59
  npm install -g lsh-framework
60
60
 
61
61
  # 2. Generate encryption key
62
- lsh lib secrets key
62
+ lsh key
63
63
  # Add the output to your .env:
64
64
  # LSH_SECRETS_KEY=<your-key>
65
65
 
@@ -69,10 +69,10 @@ lsh lib secrets key
69
69
  # SUPABASE_ANON_KEY=<your-anon-key>
70
70
 
71
71
  # 4. Push your secrets
72
- lsh lib secrets push
72
+ lsh push
73
73
 
74
74
  # 5. Pull on any other machine
75
- lsh lib secrets pull
75
+ lsh pull
76
76
 
77
77
  # Done! Your secrets are synced.
78
78
  ```
@@ -85,8 +85,8 @@ lsh lib secrets pull
85
85
 
86
86
  ```bash
87
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
88
+ lsh sync # Auto-setup and sync
89
+ eval "$(lsh sync --load)" # Sync AND load into shell
90
90
  ```
91
91
 
92
92
  What Smart Sync does automatically:
@@ -100,10 +100,10 @@ What Smart Sync does automatically:
100
100
  **Repository Isolation:**
101
101
  ```bash
102
102
  cd ~/repos/app1
103
- lsh lib secrets sync # Stored as: app1_dev
103
+ lsh sync # Stored as: app1_dev
104
104
 
105
105
  cd ~/repos/app2
106
- lsh lib secrets sync # Stored as: app2_dev (separate!)
106
+ lsh sync # Stored as: app2_dev (separate!)
107
107
  ```
108
108
 
109
109
  No more conflicts between projects using the same environment names!
@@ -124,16 +124,16 @@ Use the built-in daemon to automatically rotate secrets on a schedule:
124
124
 
125
125
  ```bash
126
126
  # Schedule API key rotation every 30 days
127
- lsh lib cron add \
127
+ lsh cron add \
128
128
  --name "rotate-api-keys" \
129
129
  --schedule "0 0 1 * *" \
130
- --command "./scripts/rotate-keys.sh && lsh lib secrets push"
130
+ --command "./scripts/rotate-keys.sh && lsh push"
131
131
 
132
132
  # Or use interval-based scheduling
133
- lsh lib cron add \
133
+ lsh cron add \
134
134
  --name "sync-secrets" \
135
135
  --interval 3600 \
136
- --command "lsh lib secrets pull && ./scripts/reload-app.sh"
136
+ --command "lsh pull && ./scripts/reload-app.sh"
137
137
  ```
138
138
 
139
139
  **No other secrets manager has this built-in!** Most require complex integrations with cron or external tools.
@@ -143,8 +143,8 @@ lsh lib cron add \
143
143
  **Setup (One Time):**
144
144
  ```bash
145
145
  # Project lead:
146
- lsh lib secrets key # Generate shared key
147
- lsh lib secrets push --env prod # Push team secrets
146
+ lsh key # Generate shared key
147
+ lsh push --env prod # Push team secrets
148
148
  # Share LSH_SECRETS_KEY via 1Password
149
149
  ```
150
150
 
@@ -155,7 +155,7 @@ lsh lib secrets push --env prod # Push team secrets
155
155
  echo "LSH_SECRETS_KEY=<shared-key>" > .env
156
156
 
157
157
  # 3. Pull secrets
158
- lsh lib secrets pull --env prod
158
+ lsh pull --env prod
159
159
 
160
160
  # 4. Start coding!
161
161
  npm start
@@ -165,31 +165,69 @@ npm start
165
165
 
166
166
  ```bash
167
167
  # Development
168
- lsh lib secrets push --env dev
168
+ lsh push --env dev
169
169
 
170
170
  # Staging (different values)
171
- lsh lib secrets push --file .env.staging --env staging
171
+ lsh push --file .env.staging --env staging
172
172
 
173
173
  # Production (super secret)
174
- lsh lib secrets push --file .env.production --env prod
174
+ lsh push --file .env.production --env prod
175
175
 
176
176
  # Pull whatever you need
177
- lsh lib secrets pull --env dev # for local dev
178
- lsh lib secrets pull --env staging # for testing
179
- lsh lib secrets pull --env prod # for production debugging
177
+ lsh pull --env dev # for local dev
178
+ lsh pull --env staging # for testing
179
+ lsh pull --env prod # for production debugging
180
180
  ```
181
181
 
182
+ ### 📝 Batch Upsert Secrets
183
+
184
+ **New in v1.1.0:** Pipe environment variables directly into your `.env` file!
185
+
186
+ ```bash
187
+ # Copy all current environment variables
188
+ printenv | lsh set
189
+
190
+ # Import from another .env file
191
+ cat .env.backup | lsh set
192
+
193
+ # Import specific variables
194
+ printenv | grep "^AWS_" | lsh set
195
+
196
+ # Merge multiple sources
197
+ cat .env.base .env.local | lsh set
198
+
199
+ # From file with --stdin flag
200
+ lsh set --stdin < .env.production
201
+
202
+ # Single key-value still works
203
+ lsh set API_KEY sk_live_12345
204
+ lsh set DATABASE_URL postgres://localhost/db
205
+ ```
206
+
207
+ **Features:**
208
+ - ✅ Automatic upsert (updates existing, adds new)
209
+ - ✅ Preserves comments and formatting
210
+ - ✅ Handles quoted values
211
+ - ✅ Validates key names
212
+ - ✅ Shows summary of changes
213
+
182
214
  ## Secrets Commands
183
215
 
184
216
  | Command | Description |
185
217
  |---------|-------------|
186
- | `lsh lib secrets push` | Upload .env to encrypted cloud storage |
187
- | `lsh lib secrets pull` | Download .env from cloud storage |
188
- | `lsh lib secrets list` | List all stored environments |
189
- | `lsh lib secrets show` | View secrets (masked) |
190
- | `lsh lib secrets key` | Generate encryption key |
191
- | `lsh lib secrets create` | Create new .env file |
192
- | `lsh lib secrets delete` | Delete .env file (with confirmation) |
218
+ | `lsh push` | Upload .env to encrypted cloud storage |
219
+ | `lsh pull` | Download .env from cloud storage |
220
+ | `lsh list` | List secrets in current .env file |
221
+ | `lsh env` | List all stored environments |
222
+ | `lsh key` | Generate encryption key |
223
+ | `lsh create` | Create new .env file |
224
+ | `lsh delete` | Delete .env file (with confirmation) |
225
+ | `lsh sync` | Smart sync (auto-setup and sync) |
226
+ | `lsh status` | Get detailed secrets status |
227
+ | `lsh get <key>` | Get a specific secret value |
228
+ | `lsh set <key> <value>` | Set a single secret value |
229
+ | `printenv \| lsh set` | Batch upsert from stdin (pipe) |
230
+ | `lsh set --stdin < file` | Batch upsert from file |
193
231
 
194
232
  See the complete guide: [SECRETS_GUIDE.md](docs/features/secrets/SECRETS_GUIDE.md)
195
233
 
@@ -217,7 +255,7 @@ lsh self version
217
255
 
218
256
  ```bash
219
257
  # 1. Generate encryption key
220
- lsh lib secrets key
258
+ lsh key
221
259
 
222
260
  # 2. Create .env file
223
261
  cat > .env <<EOF
@@ -234,7 +272,7 @@ API_KEY=your-api-key
234
272
  EOF
235
273
 
236
274
  # 3. Push to cloud
237
- lsh lib secrets push --env dev
275
+ lsh push --env dev
238
276
  ```
239
277
 
240
278
  ## Advanced Features (Bonus!)
@@ -247,13 +285,13 @@ Run jobs reliably in the background:
247
285
 
248
286
  ```bash
249
287
  # Start daemon
250
- lsh lib daemon start
288
+ lsh daemon start
251
289
 
252
290
  # Check status
253
- lsh lib daemon status
291
+ lsh daemon status
254
292
 
255
293
  # Stop daemon
256
- lsh lib daemon stop
294
+ lsh daemon stop
257
295
  ```
258
296
 
259
297
  ### Cron-Style Scheduling
@@ -262,20 +300,20 @@ Schedule any task with cron expressions:
262
300
 
263
301
  ```bash
264
302
  # Daily backup at midnight
265
- lsh lib cron add --name "backup" \
303
+ lsh cron add --name "backup" \
266
304
  --schedule "0 0 * * *" \
267
305
  --command "./backup.sh"
268
306
 
269
307
  # Every 6 hours
270
- lsh lib cron add --name "sync" \
308
+ lsh cron add --name "sync" \
271
309
  --schedule "0 */6 * * *" \
272
- --command "lsh lib secrets pull && ./reload.sh"
310
+ --command "lsh pull && ./reload.sh"
273
311
 
274
312
  # List all jobs
275
- lsh lib cron list
313
+ lsh cron list
276
314
 
277
315
  # Trigger manually
278
- lsh lib cron trigger backup
316
+ lsh cron trigger backup
279
317
  ```
280
318
 
281
319
  ### RESTful API
@@ -284,7 +322,7 @@ Control everything via HTTP API:
284
322
 
285
323
  ```bash
286
324
  # Start API server
287
- LSH_API_KEY=your-key lsh lib api start --port 3030
325
+ LSH_API_KEY=your-key lsh api start --port 3030
288
326
 
289
327
  # Use the API
290
328
  curl -H "X-API-Key: your-key" http://localhost:3030/api/jobs
@@ -368,13 +406,13 @@ LSH validates all environment variables at startup and fails fast if:
368
406
  **Solution:**
369
407
  ```bash
370
408
  # Laptop
371
- lsh lib secrets push --env dev
409
+ lsh push --env dev
372
410
 
373
411
  # Desktop
374
- lsh lib secrets pull --env dev
412
+ lsh pull --env dev
375
413
 
376
414
  # Cloud server
377
- lsh lib secrets pull --env dev
415
+ lsh pull --env dev
378
416
 
379
417
  # All synced!
380
418
  ```
@@ -386,11 +424,11 @@ lsh lib secrets pull --env dev
386
424
  **Solution:**
387
425
  ```bash
388
426
  # New team member (after getting LSH_SECRETS_KEY from 1Password)
389
- cd ~/projects/service-1 && lsh lib secrets pull --env dev
390
- cd ~/projects/service-2 && lsh lib secrets pull --env dev
391
- cd ~/projects/service-3 && lsh lib secrets pull --env dev
392
- cd ~/projects/service-4 && lsh lib secrets pull --env dev
393
- cd ~/projects/service-5 && lsh lib secrets pull --env dev
427
+ cd ~/projects/service-1 && lsh pull --env dev
428
+ cd ~/projects/service-2 && lsh pull --env dev
429
+ cd ~/projects/service-3 && lsh pull --env dev
430
+ cd ~/projects/service-4 && lsh pull --env dev
431
+ cd ~/projects/service-5 && lsh pull --env dev
394
432
 
395
433
  # Done in 30 seconds instead of 30 minutes
396
434
  ```
@@ -411,7 +449,7 @@ NEW_KEY=$(curl -X POST https://api.provider.com/keys/rotate)
411
449
  sed -i "s/API_KEY=.*/API_KEY=$NEW_KEY/" .env
412
450
 
413
451
  # Push to cloud
414
- lsh lib secrets push --env prod
452
+ lsh push --env prod
415
453
 
416
454
  # Notify team
417
455
  echo "API keys rotated at $(date)" | mail -s "Key Rotation" team@company.com
@@ -430,18 +468,18 @@ lsh lib cron add --name "rotate-keys" \
430
468
  **Solution:**
431
469
  ```bash
432
470
  # Push from local dev
433
- lsh lib secrets push --file .env.development --env dev
471
+ lsh push --file .env.development --env dev
434
472
 
435
473
  # Push staging config
436
- lsh lib secrets push --file .env.staging --env staging
474
+ lsh push --file .env.staging --env staging
437
475
 
438
476
  # Push production config (from secure machine only)
439
- lsh lib secrets push --file .env.production --env prod
477
+ lsh push --file .env.production --env prod
440
478
 
441
479
  # CI/CD pulls the right one
442
480
  # In .github/workflows/deploy.yml:
443
481
  - name: Get secrets
444
- run: lsh lib secrets pull --env ${{ github.ref == 'refs/heads/main' && 'prod' || 'staging' }}
482
+ run: lsh pull --env ${{ github.ref == 'refs/heads/main' && 'prod' || 'staging' }}
445
483
  ```
446
484
 
447
485
  ## Comparison with Other Tools
@@ -504,7 +542,7 @@ LSH_ALLOW_DANGEROUS_COMMANDS=false # Keep false in production
504
542
 
505
543
  ```bash
506
544
  # Encryption key for secrets
507
- lsh lib secrets key
545
+ lsh key
508
546
 
509
547
  # API key for HTTP API
510
548
  openssl rand -hex 32
@@ -561,10 +599,10 @@ lsh --version
561
599
 
562
600
  ```bash
563
601
  # Check stored environments
564
- lsh lib secrets list
602
+ lsh list
565
603
 
566
604
  # Push if missing
567
- lsh lib secrets push --env dev
605
+ lsh push --env dev
568
606
  ```
569
607
 
570
608
  ### "Decryption failed"
@@ -574,8 +612,8 @@ lsh lib secrets push --env dev
574
612
  # Make sure LSH_SECRETS_KEY matches the one used to encrypt
575
613
 
576
614
  # Generate new key and re-push
577
- lsh lib secrets key
578
- lsh lib secrets push
615
+ lsh key
616
+ lsh push
579
617
  ```
580
618
 
581
619
  ### "Supabase not configured"
@@ -675,7 +713,7 @@ See API endpoints documentation in the Advanced Features section.
675
713
 
676
714
  ## Roadmap
677
715
 
678
- - [ ] CLI command shortcuts (`lsh push` instead of `lsh lib secrets push`)
716
+ - [ ] CLI command shortcuts (`lsh push` instead of `lsh push`)
679
717
  - [ ] Built-in secret rotation templates (AWS, GCP, Azure)
680
718
  - [ ] Web dashboard for team secret management
681
719
  - [ ] Audit logging for secret access
package/dist/cli.js CHANGED
@@ -85,9 +85,9 @@ program
85
85
  console.log(' status Get detailed secrets status');
86
86
  console.log('');
87
87
  console.log('🔄 Automation (Schedule secret rotation):');
88
- console.log(' lib cron add Schedule automatic tasks');
89
- console.log(' lib cron list List scheduled jobs');
90
- console.log(' lib daemon start Start persistent daemon');
88
+ console.log(' cron add Schedule automatic tasks');
89
+ console.log(' cron list List scheduled jobs');
90
+ console.log(' daemon start Start persistent daemon');
91
91
  console.log('');
92
92
  console.log('🚀 Quick Start:');
93
93
  console.log(' lsh key # Generate encryption key');
@@ -95,10 +95,10 @@ program
95
95
  console.log(' lsh pull --env dev # Pull on another machine');
96
96
  console.log('');
97
97
  console.log('📚 More Commands:');
98
- console.log(' lib api API server management');
99
- console.log(' lib supabase Supabase database management');
100
- console.log(' lib daemon Daemon management');
101
- console.log(' lib cron Cron job management');
98
+ console.log(' api API server management');
99
+ console.log(' supabase Supabase database management');
100
+ console.log(' daemon Daemon management');
101
+ console.log(' cron Cron job management');
102
102
  console.log(' self Self-management commands');
103
103
  console.log(' self zsh ZSH compatibility commands');
104
104
  console.log(' -i, --interactive Start interactive shell');
@@ -217,9 +217,13 @@ function findSimilarCommands(input, validCommands) {
217
217
  (async () => {
218
218
  // REPL interactive shell
219
219
  await init_ishell(program);
220
- // Library commands (parent for service commands)
220
+ // Flatten all service commands to top-level (no more 'lib' parent)
221
+ await init_supabase(program);
222
+ await init_daemon(program);
223
+ await init_cron(program);
224
+ registerApiCommands(program);
225
+ // Legacy 'lib' command group with deprecation warnings
221
226
  const libCommand = await init_lib(program);
222
- // Nest service commands under lib
223
227
  await init_supabase(libCommand);
224
228
  await init_daemon(libCommand);
225
229
  await init_cron(libCommand);
@@ -596,13 +600,13 @@ function showDetailedHelp() {
596
600
  console.log(' self zsh ZSH compatibility commands');
597
601
  console.log(' self zsh-import Import ZSH configs (aliases, functions, exports)');
598
602
  console.log('');
599
- console.log('Library Commands (lsh lib <command>):');
600
- console.log(' lib api API server management');
601
- console.log(' lib supabase Supabase database management');
602
- console.log(' lib daemon Daemon management');
603
- console.log(' lib daemon job Job management');
604
- console.log(' lib daemon db Database integration');
605
- console.log(' lib cron Cron job management');
603
+ console.log('Service Commands:');
604
+ console.log(' api API server management');
605
+ console.log(' supabase Supabase database management');
606
+ console.log(' daemon Daemon management');
607
+ console.log(' daemon job Job management');
608
+ console.log(' daemon db Database integration');
609
+ console.log(' cron Cron job management');
606
610
  console.log('');
607
611
  console.log('Examples:');
608
612
  console.log('');
@@ -632,13 +636,13 @@ function showDetailedHelp() {
632
636
  console.log(' lsh secrets pull # Pull secrets from cloud');
633
637
  console.log(' lsh secrets list # List environments');
634
638
  console.log('');
635
- console.log(' Library Services:');
636
- console.log(' lsh lib daemon start # Start daemon');
637
- console.log(' lsh lib daemon status # Check daemon status');
638
- console.log(' lsh lib daemon job list # List all jobs');
639
- console.log(' lsh lib cron list # List cron jobs');
640
- console.log(' lsh lib api start # Start API server');
641
- console.log(' lsh lib api key # Generate API key');
639
+ console.log(' Service Operations:');
640
+ console.log(' lsh daemon start # Start daemon');
641
+ console.log(' lsh daemon status # Check daemon status');
642
+ console.log(' lsh daemon job list # List all jobs');
643
+ console.log(' lsh cron list # List cron jobs');
644
+ console.log(' lsh api start # Start API server');
645
+ console.log(' lsh api key # Generate API key');
642
646
  console.log('');
643
647
  console.log('Features:');
644
648
  console.log(' ✅ POSIX Shell Compliance (85-95%)');
@@ -170,7 +170,6 @@ export class JobManager extends BaseJobManager {
170
170
  job.process.kill(signal);
171
171
  }
172
172
  catch (error) {
173
- const err = error;
174
173
  this.logger.error(`Failed to kill job ${jobId}`, error);
175
174
  }
176
175
  // Update status
@@ -462,8 +462,8 @@ export class SecretsManager {
462
462
  return true;
463
463
  }
464
464
  catch (error) {
465
- const err = error;
466
- logger.error(`Failed to save encryption key: ${error.message}`);
465
+ const _err = error;
466
+ logger.error(`Failed to save encryption key: ${_err.message}`);
467
467
  logger.info('Please set it manually:');
468
468
  logger.info(`export LSH_SECRETS_KEY=${key}`);
469
469
  return false;
@@ -499,8 +499,8 @@ LSH_SECRETS_KEY=${this.encryptionKey}
499
499
  return true;
500
500
  }
501
501
  catch (error) {
502
- const err = error;
503
- logger.error(`Failed to create ${envFilePath}: ${error.message}`);
502
+ const _err = error;
503
+ logger.error(`Failed to create ${envFilePath}: ${_err.message}`);
504
504
  return false;
505
505
  }
506
506
  }
@@ -517,8 +517,8 @@ LSH_SECRETS_KEY=${this.encryptionKey}
517
517
  return true;
518
518
  }
519
519
  catch (error) {
520
- const err = error;
521
- logger.error(`Failed to create ${envFilePath}: ${error.message}`);
520
+ const _err = error;
521
+ logger.error(`Failed to create ${envFilePath}: ${_err.message}`);
522
522
  return false;
523
523
  }
524
524
  }
@@ -330,7 +330,6 @@ export class ZshCompatibility {
330
330
  /**
331
331
  * Generate completions based on patterns
332
332
  */
333
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
334
333
  async generateCompletions(context, patterns) {
335
334
  const completions = [];
336
335
  for (const pattern of patterns) {
@@ -170,10 +170,11 @@ export class MCLIBridge extends EventEmitter {
170
170
  await this.jobTracker.completeExecution(execution.id, result, metrics, artifacts);
171
171
  break;
172
172
  case 'failed':
173
- case 'error':
173
+ case 'error': {
174
174
  const errorObj = error;
175
175
  await this.jobTracker.failExecution(execution.id, errorObj?.message || 'Job failed in MCLI', error);
176
176
  break;
177
+ }
177
178
  case 'cancelled':
178
179
  await this.jobTracker.updateJobStatus(pipelineJobId, JobStatus.CANCELLED);
179
180
  break;
@@ -68,7 +68,7 @@ async function _makeCommand(commander) {
68
68
  export async function init_lib(program) {
69
69
  const lib = program
70
70
  .command("lib")
71
- .description("LSH library and service commands");
71
+ .description("⚠️ DEPRECATED: Use top-level commands instead (daemon, cron, api, supabase)");
72
72
  // Load and register dynamic commands
73
73
  const commands = await loadCommands();
74
74
  for (const commandName of Object.keys(commands)) {
@@ -80,6 +80,16 @@ export async function init_lib(program) {
80
80
  .description("commandName")
81
81
  .usage(`${commandName} used as follows:`);
82
82
  }
83
+ // Show deprecation warning when 'lib' is used
84
+ lib.hook('preAction', (_thisCommand, _actionCommand) => {
85
+ console.warn('\x1b[33m⚠️ WARNING: "lsh lib" commands are deprecated as of v1.0.0\x1b[0m');
86
+ console.warn('\x1b[33m Use top-level commands instead:\x1b[0m');
87
+ console.warn('\x1b[33m - lsh daemon (instead of lsh lib daemon)\x1b[0m');
88
+ console.warn('\x1b[33m - lsh cron (instead of lsh lib cron)\x1b[0m');
89
+ console.warn('\x1b[33m - lsh api (instead of lsh lib api)\x1b[0m');
90
+ console.warn('\x1b[33m - lsh supabase (instead of lsh lib supabase)\x1b[0m');
91
+ console.warn('');
92
+ });
83
93
  // Optional: Enhance the 'lib' command group with additional descriptions and error handling
84
94
  lib
85
95
  .showHelpAfterError("Command not recognized, here's some help.")
@@ -351,52 +351,31 @@ API_KEY=
351
351
  process.exit(1);
352
352
  }
353
353
  });
354
- // Set a specific secret value
354
+ // Set a specific secret value or batch upsert from stdin
355
355
  program
356
- .command('set <key> <value>')
357
- .description('Set a specific secret value in .env file')
356
+ .command('set [key] [value]')
357
+ .description('Set a specific secret value in .env file, or batch upsert from stdin (KEY=VALUE format)')
358
358
  .option('-f, --file <path>', 'Path to .env file', '.env')
359
+ .option('--stdin', 'Read KEY=VALUE pairs from stdin (one per line)')
359
360
  .action(async (key, value, options) => {
360
361
  try {
361
362
  const envPath = path.resolve(options.file);
362
- // Validate key format
363
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
364
- console.error(`❌ Invalid key format: ${key}. Must be a valid environment variable name.`);
365
- process.exit(1);
363
+ // Check if we should read from stdin
364
+ const isStdin = options.stdin || (!key && !value);
365
+ if (isStdin) {
366
+ // Batch mode: read from stdin
367
+ await batchSetSecrets(envPath);
366
368
  }
367
- let content = '';
368
- let found = false;
369
- if (fs.existsSync(envPath)) {
370
- content = fs.readFileSync(envPath, 'utf8');
371
- const lines = content.split('\n');
372
- const newLines = [];
373
- for (const line of lines) {
374
- if (line.trim().startsWith('#') || !line.trim()) {
375
- newLines.push(line);
376
- continue;
377
- }
378
- const match = line.match(/^([^=]+)=(.*)$/);
379
- if (match && match[1].trim() === key) {
380
- // Quote values with spaces or special characters
381
- const needsQuotes = /[\s#]/.test(value);
382
- const quotedValue = needsQuotes ? `"${value}"` : value;
383
- newLines.push(`${key}=${quotedValue}`);
384
- found = true;
385
- }
386
- else {
387
- newLines.push(line);
388
- }
369
+ else {
370
+ // Single mode: set one key-value pair
371
+ if (!key || value === undefined) {
372
+ console.error('❌ Usage: lsh set <key> <value>');
373
+ console.error(' Or pipe input: printenv | lsh set');
374
+ console.error(' Or use stdin: lsh set --stdin < file.env');
375
+ process.exit(1);
389
376
  }
390
- content = newLines.join('\n');
391
- }
392
- // If key wasn't found, append it
393
- if (!found) {
394
- const needsQuotes = /[\s#]/.test(value);
395
- const quotedValue = needsQuotes ? `"${value}"` : value;
396
- content = content.trimRight() + `\n${key}=${quotedValue}\n`;
377
+ await setSingleSecret(envPath, key, value);
397
378
  }
398
- fs.writeFileSync(envPath, content, 'utf8');
399
- console.log(`✅ Set ${key} in ${options.file}`);
400
379
  }
401
380
  catch (error) {
402
381
  const err = error;
@@ -404,6 +383,199 @@ API_KEY=
404
383
  process.exit(1);
405
384
  }
406
385
  });
386
+ /**
387
+ * Set a single secret value
388
+ */
389
+ async function setSingleSecret(envPath, key, value) {
390
+ // Validate key format
391
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
392
+ console.error(`❌ Invalid key format: ${key}. Must be a valid environment variable name.`);
393
+ process.exit(1);
394
+ }
395
+ let content = '';
396
+ let found = false;
397
+ if (fs.existsSync(envPath)) {
398
+ content = fs.readFileSync(envPath, 'utf8');
399
+ const lines = content.split('\n');
400
+ const newLines = [];
401
+ for (const line of lines) {
402
+ if (line.trim().startsWith('#') || !line.trim()) {
403
+ newLines.push(line);
404
+ continue;
405
+ }
406
+ const match = line.match(/^([^=]+)=(.*)$/);
407
+ if (match && match[1].trim() === key) {
408
+ // Quote values with spaces or special characters
409
+ const needsQuotes = /[\s#]/.test(value);
410
+ const quotedValue = needsQuotes ? `"${value}"` : value;
411
+ newLines.push(`${key}=${quotedValue}`);
412
+ found = true;
413
+ }
414
+ else {
415
+ newLines.push(line);
416
+ }
417
+ }
418
+ content = newLines.join('\n');
419
+ }
420
+ // If key wasn't found, append it
421
+ if (!found) {
422
+ const needsQuotes = /[\s#]/.test(value);
423
+ const quotedValue = needsQuotes ? `"${value}"` : value;
424
+ content = content.trimRight() + `\n${key}=${quotedValue}\n`;
425
+ }
426
+ fs.writeFileSync(envPath, content, 'utf8');
427
+ console.log(`✅ Set ${key}`);
428
+ }
429
+ /**
430
+ * Batch upsert secrets from stdin
431
+ */
432
+ async function batchSetSecrets(envPath) {
433
+ return new Promise((resolve, reject) => {
434
+ let inputData = '';
435
+ const stdin = process.stdin;
436
+ // Check if stdin is a TTY (interactive terminal)
437
+ if (stdin.isTTY) {
438
+ console.error('❌ No input provided. Please pipe data or use --stdin flag.');
439
+ console.error('');
440
+ console.error('Examples:');
441
+ console.error(' printenv | lsh set');
442
+ console.error(' lsh set --stdin < .env.backup');
443
+ console.error(' echo "API_KEY=secret123" | lsh set');
444
+ process.exit(1);
445
+ }
446
+ stdin.setEncoding('utf8');
447
+ stdin.on('data', (chunk) => {
448
+ inputData += chunk;
449
+ });
450
+ stdin.on('end', () => {
451
+ try {
452
+ const lines = inputData.split('\n').filter(line => line.trim());
453
+ if (lines.length === 0) {
454
+ console.error('❌ No valid KEY=VALUE pairs found in input');
455
+ process.exit(1);
456
+ }
457
+ // Read existing .env file
458
+ let content = '';
459
+ const existingKeys = new Map();
460
+ if (fs.existsSync(envPath)) {
461
+ content = fs.readFileSync(envPath, 'utf8');
462
+ const existingLines = content.split('\n');
463
+ for (const line of existingLines) {
464
+ if (line.trim().startsWith('#') || !line.trim())
465
+ continue;
466
+ const match = line.match(/^([^=]+)=(.*)$/);
467
+ if (match) {
468
+ existingKeys.set(match[1].trim(), line);
469
+ }
470
+ }
471
+ }
472
+ const updates = [];
473
+ const errors = [];
474
+ const newKeys = new Map();
475
+ // Parse input lines
476
+ for (const line of lines) {
477
+ const trimmed = line.trim();
478
+ // Skip comments and empty lines
479
+ if (trimmed.startsWith('#') || !trimmed)
480
+ continue;
481
+ // Parse KEY=VALUE format
482
+ const match = trimmed.match(/^([^=]+)=(.*)$/);
483
+ if (!match) {
484
+ errors.push(`Invalid format: ${trimmed}`);
485
+ continue;
486
+ }
487
+ const key = match[1].trim();
488
+ let value = match[2].trim();
489
+ // Validate key format
490
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
491
+ errors.push(`Invalid key format: ${key}`);
492
+ continue;
493
+ }
494
+ // Remove surrounding quotes if present
495
+ if ((value.startsWith('"') && value.endsWith('"')) ||
496
+ (value.startsWith("'") && value.endsWith("'"))) {
497
+ value = value.slice(1, -1);
498
+ }
499
+ // Track if this is an update or addition
500
+ const action = existingKeys.has(key) ? 'updated' : 'added';
501
+ updates.push({ key, value, action });
502
+ newKeys.set(key, value);
503
+ }
504
+ // Build new content
505
+ const newLines = [];
506
+ let hasContent = false;
507
+ if (fs.existsSync(envPath)) {
508
+ const existingLines = content.split('\n');
509
+ for (const line of existingLines) {
510
+ if (line.trim().startsWith('#') || !line.trim()) {
511
+ newLines.push(line);
512
+ continue;
513
+ }
514
+ const match = line.match(/^([^=]+)=(.*)$/);
515
+ if (match) {
516
+ const key = match[1].trim();
517
+ if (newKeys.has(key)) {
518
+ // Update existing key
519
+ const value = newKeys.get(key);
520
+ const needsQuotes = /[\s#]/.test(value);
521
+ const quotedValue = needsQuotes ? `"${value}"` : value;
522
+ newLines.push(`${key}=${quotedValue}`);
523
+ newKeys.delete(key); // Mark as processed
524
+ hasContent = true;
525
+ }
526
+ else {
527
+ // Keep existing line
528
+ newLines.push(line);
529
+ hasContent = true;
530
+ }
531
+ }
532
+ else {
533
+ newLines.push(line);
534
+ }
535
+ }
536
+ }
537
+ // Add new keys that weren't in the existing file
538
+ for (const [key, value] of newKeys.entries()) {
539
+ const needsQuotes = /[\s#]/.test(value);
540
+ const quotedValue = needsQuotes ? `"${value}"` : value;
541
+ if (hasContent) {
542
+ newLines.push(`${key}=${quotedValue}`);
543
+ }
544
+ else {
545
+ newLines.push(`${key}=${quotedValue}`);
546
+ hasContent = true;
547
+ }
548
+ }
549
+ // Write updated content
550
+ let finalContent = newLines.join('\n');
551
+ if (hasContent && !finalContent.endsWith('\n')) {
552
+ finalContent += '\n';
553
+ }
554
+ fs.writeFileSync(envPath, finalContent, 'utf8');
555
+ // Report results
556
+ const added = updates.filter(u => u.action === 'added').length;
557
+ const updated = updates.filter(u => u.action === 'updated').length;
558
+ console.log(`✅ Batch upsert complete:`);
559
+ if (added > 0)
560
+ console.log(` Added: ${added} key(s)`);
561
+ if (updated > 0)
562
+ console.log(` Updated: ${updated} key(s)`);
563
+ if (errors.length > 0) {
564
+ console.log('');
565
+ console.log('⚠️ Skipped invalid entries:');
566
+ errors.forEach(err => console.log(` ${err}`));
567
+ }
568
+ resolve();
569
+ }
570
+ catch (error) {
571
+ reject(error);
572
+ }
573
+ });
574
+ stdin.on('error', (error) => {
575
+ reject(error);
576
+ });
577
+ });
578
+ }
407
579
  // Delete .env file with confirmation
408
580
  program
409
581
  .command('delete')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "0.10.2",
3
+ "version": "1.1.0",
4
4
  "description": "Encrypted secrets manager with automatic rotation, team sync, and multi-environment support. Built on a powerful shell with daemon scheduling and CI/CD integration.",
5
5
  "main": "dist/app.js",
6
6
  "bin": {