lsh-framework 1.0.0 → 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
@@ -179,6 +179,38 @@ lsh pull --env staging # for testing
179
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 |
@@ -192,6 +224,10 @@ lsh pull --env prod # for production debugging
192
224
  | `lsh delete` | Delete .env file (with confirmation) |
193
225
  | `lsh sync` | Smart sync (auto-setup and sync) |
194
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 |
195
231
 
196
232
  See the complete guide: [SECRETS_GUIDE.md](docs/features/secrets/SECRETS_GUIDE.md)
197
233
 
@@ -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": "1.0.0",
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": {