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 +75 -1
- package/dist/cli.js +14 -10
- package/dist/daemon/lshd.js +23 -13
- package/dist/lib/api-error-handler.js +16 -14
- package/dist/lib/base-command-registrar.js +6 -5
- package/dist/lib/daemon-client.js +13 -9
- package/dist/lib/database-persistence.js +8 -8
- package/dist/lib/env-validator.js +0 -3
- package/dist/lib/logger.js +0 -1
- package/dist/lib/secrets-manager.js +254 -153
- package/dist/lib/zsh-import-manager.js +17 -9
- package/dist/pipeline/job-tracker.js +1 -1
- package/dist/pipeline/mcli-bridge.js +11 -5
- package/dist/pipeline/workflow-engine.js +10 -7
- package/dist/services/cron/cron-registrar.js +27 -22
- package/dist/services/daemon/daemon-registrar.js +27 -13
- package/dist/services/secrets/secrets.js +37 -28
- package/dist/services/supabase/supabase-registrar.js +40 -33
- package/package.json +2 -1
|
@@ -6,7 +6,7 @@ import * as fs from 'fs';
|
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import * as crypto from 'crypto';
|
|
8
8
|
import DatabasePersistence from './database-persistence.js';
|
|
9
|
-
import { createLogger } from './logger.js';
|
|
9
|
+
import { createLogger, LogLevel } from './logger.js';
|
|
10
10
|
import { getGitRepoInfo, hasEnvExample, ensureEnvInGitignore } from './git-utils.js';
|
|
11
11
|
const logger = createLogger('SecretsManager');
|
|
12
12
|
export class SecretsManager {
|
|
@@ -66,15 +66,16 @@ export class SecretsManager {
|
|
|
66
66
|
return decrypted;
|
|
67
67
|
}
|
|
68
68
|
catch (error) {
|
|
69
|
-
|
|
69
|
+
const err = error;
|
|
70
|
+
if (err.message.includes('bad decrypt') || err.message.includes('wrong final block length')) {
|
|
70
71
|
throw new Error('Decryption failed. This usually means:\n' +
|
|
71
72
|
' 1. You need to set LSH_SECRETS_KEY environment variable\n' +
|
|
72
73
|
' 2. The key must match the one used during encryption\n' +
|
|
73
74
|
' 3. Generate a shared key with: lsh secrets key\n' +
|
|
74
75
|
' 4. Add it to your .env: LSH_SECRETS_KEY=<key>\n' +
|
|
75
|
-
'\nOriginal error: ' +
|
|
76
|
+
'\nOriginal error: ' + err.message);
|
|
76
77
|
}
|
|
77
|
-
throw
|
|
78
|
+
throw err;
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
/**
|
|
@@ -116,10 +117,48 @@ export class SecretsManager {
|
|
|
116
117
|
})
|
|
117
118
|
.join('\n') + '\n';
|
|
118
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Detect destructive changes (filled secrets becoming empty)
|
|
122
|
+
*/
|
|
123
|
+
detectDestructiveChanges(cloudSecrets, localSecrets) {
|
|
124
|
+
const destructive = [];
|
|
125
|
+
for (const [key, cloudValue] of Object.entries(cloudSecrets)) {
|
|
126
|
+
// Only check if key exists in local AND cloud has a non-empty value
|
|
127
|
+
if (key in localSecrets && cloudValue.trim() !== '') {
|
|
128
|
+
const localValue = localSecrets[key];
|
|
129
|
+
// If cloud had value but local is now empty/whitespace - this is destructive
|
|
130
|
+
if (localValue.trim() === '') {
|
|
131
|
+
destructive.push({ key, cloudValue, localValue });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return destructive;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Format error message for destructive changes
|
|
139
|
+
*/
|
|
140
|
+
formatDestructiveChangesError(destructive) {
|
|
141
|
+
const count = destructive.length;
|
|
142
|
+
const plural = count === 1 ? 'secret' : 'secrets';
|
|
143
|
+
let message = `⚠️ Destructive change detected!\n\n`;
|
|
144
|
+
message += `${count} ${plural} would go from filled → empty:\n\n`;
|
|
145
|
+
for (const { key, cloudValue } of destructive) {
|
|
146
|
+
// Mask the value for security (show first 4-5 chars)
|
|
147
|
+
const preview = cloudValue.length > 5
|
|
148
|
+
? cloudValue.substring(0, 5) + '****'
|
|
149
|
+
: '****';
|
|
150
|
+
message += ` • ${key}: "${preview}" → "" (empty)\n`;
|
|
151
|
+
}
|
|
152
|
+
message += `\nThis is likely unintentional and could break your application.\n\n`;
|
|
153
|
+
message += `To proceed anyway, use the --force flag:\n`;
|
|
154
|
+
message += ` lsh lib secrets push --force\n`;
|
|
155
|
+
message += ` lsh lib secrets sync --force\n`;
|
|
156
|
+
return message;
|
|
157
|
+
}
|
|
119
158
|
/**
|
|
120
159
|
* Push local .env to Supabase
|
|
121
160
|
*/
|
|
122
|
-
async push(envFilePath = '.env', environment = 'dev') {
|
|
161
|
+
async push(envFilePath = '.env', environment = 'dev', force = false) {
|
|
123
162
|
if (!fs.existsSync(envFilePath)) {
|
|
124
163
|
throw new Error(`File not found: ${envFilePath}`);
|
|
125
164
|
}
|
|
@@ -138,6 +177,51 @@ export class SecretsManager {
|
|
|
138
177
|
logger.info(`Pushing ${envFilePath} to Supabase (${environment})...`);
|
|
139
178
|
const content = fs.readFileSync(envFilePath, 'utf8');
|
|
140
179
|
const env = this.parseEnvFile(content);
|
|
180
|
+
// Check for destructive changes unless force is true
|
|
181
|
+
if (!force) {
|
|
182
|
+
try {
|
|
183
|
+
const jobs = await this.persistence.getActiveJobs();
|
|
184
|
+
const safeFilename = filename.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
185
|
+
const secretsJobs = jobs
|
|
186
|
+
.filter(j => {
|
|
187
|
+
return j.command === 'secrets_sync' &&
|
|
188
|
+
j.job_id.includes(environment) &&
|
|
189
|
+
j.job_id.includes(safeFilename);
|
|
190
|
+
})
|
|
191
|
+
.sort((a, b) => new Date(b.started_at).getTime() - new Date(a.started_at).getTime());
|
|
192
|
+
if (secretsJobs.length > 0) {
|
|
193
|
+
const latestSecret = secretsJobs[0];
|
|
194
|
+
if (latestSecret.output) {
|
|
195
|
+
try {
|
|
196
|
+
const decrypted = this.decrypt(latestSecret.output);
|
|
197
|
+
const cloudEnv = this.parseEnvFile(decrypted);
|
|
198
|
+
const destructive = this.detectDestructiveChanges(cloudEnv, env);
|
|
199
|
+
if (destructive.length > 0) {
|
|
200
|
+
throw new Error(this.formatDestructiveChangesError(destructive));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
const err = error;
|
|
205
|
+
// If decryption fails, it's a key mismatch - let it proceed
|
|
206
|
+
// (will fail later with proper error)
|
|
207
|
+
if (!err.message.includes('Destructive change')) {
|
|
208
|
+
// Only ignore decryption errors, re-throw destructive change errors
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
const err = error;
|
|
218
|
+
// Re-throw any errors (including destructive change errors)
|
|
219
|
+
if (err.message.includes('Destructive change') || err.message.includes('Decryption failed')) {
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
222
|
+
// Ignore other errors (like connection issues) and proceed
|
|
223
|
+
}
|
|
224
|
+
}
|
|
141
225
|
// Encrypt entire .env content
|
|
142
226
|
const encrypted = this.encrypt(content);
|
|
143
227
|
// Include filename in job_id for tracking multiple .env files
|
|
@@ -227,7 +311,7 @@ export class SecretsManager {
|
|
|
227
311
|
let filename = '.env';
|
|
228
312
|
if (parts.length >= 4) {
|
|
229
313
|
// New format with filename
|
|
230
|
-
const
|
|
314
|
+
const _timestamp = parts[parts.length - 1];
|
|
231
315
|
// Reconstruct filename from middle parts
|
|
232
316
|
const filenameParts = parts.slice(2, -1);
|
|
233
317
|
if (filenameParts.length > 0) {
|
|
@@ -322,13 +406,13 @@ export class SecretsManager {
|
|
|
322
406
|
status.cloudKeys = Object.keys(env).length;
|
|
323
407
|
status.keyMatches = true;
|
|
324
408
|
}
|
|
325
|
-
catch (
|
|
409
|
+
catch (_error) {
|
|
326
410
|
status.keyMatches = false;
|
|
327
411
|
}
|
|
328
412
|
}
|
|
329
413
|
}
|
|
330
414
|
}
|
|
331
|
-
catch (
|
|
415
|
+
catch (_error) {
|
|
332
416
|
// Cloud check failed, likely no connection
|
|
333
417
|
}
|
|
334
418
|
return status;
|
|
@@ -378,6 +462,7 @@ export class SecretsManager {
|
|
|
378
462
|
return true;
|
|
379
463
|
}
|
|
380
464
|
catch (error) {
|
|
465
|
+
const err = error;
|
|
381
466
|
logger.error(`Failed to save encryption key: ${error.message}`);
|
|
382
467
|
logger.info('Please set it manually:');
|
|
383
468
|
logger.info(`export LSH_SECRETS_KEY=${key}`);
|
|
@@ -414,6 +499,7 @@ LSH_SECRETS_KEY=${this.encryptionKey}
|
|
|
414
499
|
return true;
|
|
415
500
|
}
|
|
416
501
|
catch (error) {
|
|
502
|
+
const err = error;
|
|
417
503
|
logger.error(`Failed to create ${envFilePath}: ${error.message}`);
|
|
418
504
|
return false;
|
|
419
505
|
}
|
|
@@ -431,6 +517,7 @@ LSH_SECRETS_KEY=${this.encryptionKey}
|
|
|
431
517
|
return true;
|
|
432
518
|
}
|
|
433
519
|
catch (error) {
|
|
520
|
+
const err = error;
|
|
434
521
|
logger.error(`Failed to create ${envFilePath}: ${error.message}`);
|
|
435
522
|
return false;
|
|
436
523
|
}
|
|
@@ -475,118 +562,162 @@ LSH_SECRETS_KEY=${this.encryptionKey}
|
|
|
475
562
|
* Smart sync command - automatically set up and synchronize secrets
|
|
476
563
|
* This is the new enhanced sync that does everything automatically
|
|
477
564
|
*/
|
|
478
|
-
async smartSync(envFilePath = '.env', environment = 'dev', autoExecute = true, loadMode = false) {
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
out(`\n🔍 Smart sync for: ${displayEnv}\n`);
|
|
485
|
-
// Show git repo context if detected
|
|
486
|
-
if (this.gitInfo?.isGitRepo) {
|
|
487
|
-
out('📁 Git Repository:');
|
|
488
|
-
out(` Repo: ${this.gitInfo.repoName || 'unknown'}`);
|
|
489
|
-
if (this.gitInfo.currentBranch) {
|
|
490
|
-
out(` Branch: ${this.gitInfo.currentBranch}`);
|
|
491
|
-
}
|
|
492
|
-
out();
|
|
493
|
-
}
|
|
494
|
-
// Step 1: Ensure encryption key exists
|
|
495
|
-
if (!process.env.LSH_SECRETS_KEY) {
|
|
496
|
-
logger.info('🔑 No encryption key found...');
|
|
497
|
-
await this.ensureEncryptionKey();
|
|
498
|
-
out();
|
|
499
|
-
}
|
|
500
|
-
// Step 2: Ensure .gitignore includes .env
|
|
501
|
-
if (this.gitInfo?.isGitRepo) {
|
|
502
|
-
ensureEnvInGitignore(process.cwd());
|
|
565
|
+
async smartSync(envFilePath = '.env', environment = 'dev', autoExecute = true, loadMode = false, force = false) {
|
|
566
|
+
// In load mode, suppress all logger output to prevent zsh glob interpretation
|
|
567
|
+
// Save original level and restore at the end
|
|
568
|
+
const originalLogLevel = loadMode ? logger['config'].level : undefined;
|
|
569
|
+
if (loadMode) {
|
|
570
|
+
logger.setLevel(LogLevel.NONE);
|
|
503
571
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
out('⚠️ Encryption key mismatch!');
|
|
519
|
-
out(' The local key does not match the cloud storage.');
|
|
520
|
-
out(' Please use the original key or push new secrets with:');
|
|
521
|
-
out(` lsh lib secrets push -f ${envFilePath} -e ${environment}`);
|
|
522
|
-
out();
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
if (!status.localExists && !status.cloudExists) {
|
|
526
|
-
action = 'create-and-push';
|
|
527
|
-
out('🆕 No secrets found locally or in cloud');
|
|
528
|
-
out(' Creating new .env file...');
|
|
529
|
-
if (autoExecute) {
|
|
530
|
-
await this.createEnvFromExample(envFilePath);
|
|
531
|
-
out(' Pushing to cloud...');
|
|
532
|
-
await this.push(envFilePath, effectiveEnv);
|
|
572
|
+
try {
|
|
573
|
+
// Use repo-aware environment if in git repo
|
|
574
|
+
const effectiveEnv = this.getRepoAwareEnvironment(environment);
|
|
575
|
+
const displayEnv = this.gitInfo?.repoName ? `${this.gitInfo.repoName}/${environment}` : environment;
|
|
576
|
+
// In load mode, suppress all output except the final export commands
|
|
577
|
+
const out = loadMode ? () => { } : console.log;
|
|
578
|
+
out(`\n🔍 Smart sync for: ${displayEnv}\n`);
|
|
579
|
+
// Show git repo context if detected
|
|
580
|
+
if (this.gitInfo?.isGitRepo) {
|
|
581
|
+
out('📁 Git Repository:');
|
|
582
|
+
out(` Repo: ${this.gitInfo.repoName || 'unknown'}`);
|
|
583
|
+
if (this.gitInfo.currentBranch) {
|
|
584
|
+
out(` Branch: ${this.gitInfo.currentBranch}`);
|
|
585
|
+
}
|
|
533
586
|
out();
|
|
534
|
-
out('✅ Setup complete! Edit your .env and run sync again to update.');
|
|
535
|
-
}
|
|
536
|
-
else {
|
|
537
|
-
out('💡 Run: lsh lib secrets create && lsh lib secrets push');
|
|
538
587
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
588
|
+
// Step 1: Ensure encryption key exists
|
|
589
|
+
if (!process.env.LSH_SECRETS_KEY) {
|
|
590
|
+
logger.info('🔑 No encryption key found...');
|
|
591
|
+
await this.ensureEncryptionKey();
|
|
592
|
+
out();
|
|
543
593
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
action = 'push';
|
|
548
|
-
out('⬆️ Local .env exists but not in cloud');
|
|
549
|
-
if (autoExecute) {
|
|
550
|
-
out(' Pushing to cloud...');
|
|
551
|
-
await this.push(envFilePath, effectiveEnv);
|
|
552
|
-
out('✅ Secrets pushed to cloud!');
|
|
594
|
+
// Step 2: Ensure .gitignore includes .env
|
|
595
|
+
if (this.gitInfo?.isGitRepo) {
|
|
596
|
+
ensureEnvInGitignore(process.cwd());
|
|
553
597
|
}
|
|
554
|
-
|
|
555
|
-
|
|
598
|
+
// Step 3: Check current status
|
|
599
|
+
const status = await this.status(envFilePath, effectiveEnv);
|
|
600
|
+
out('📊 Current Status:');
|
|
601
|
+
out(` Encryption key: ${status.keySet ? '✅' : '❌'}`);
|
|
602
|
+
out(` Local ${envFilePath}: ${status.localExists ? `✅ (${status.localKeys} keys)` : '❌'}`);
|
|
603
|
+
out(` Cloud storage: ${status.cloudExists ? `✅ (${status.cloudKeys} keys)` : '❌'}`);
|
|
604
|
+
if (status.cloudExists && status.keyMatches !== undefined) {
|
|
605
|
+
out(` Key matches: ${status.keyMatches ? '✅' : '❌'}`);
|
|
556
606
|
}
|
|
557
607
|
out();
|
|
558
|
-
//
|
|
559
|
-
|
|
560
|
-
|
|
608
|
+
// Step 4: Determine action and execute if auto mode
|
|
609
|
+
let _action = 'in-sync';
|
|
610
|
+
if (status.cloudExists && status.keyMatches === false) {
|
|
611
|
+
_action = 'key-mismatch';
|
|
612
|
+
out('⚠️ Encryption key mismatch!');
|
|
613
|
+
out(' The local key does not match the cloud storage.');
|
|
614
|
+
out(' Please use the original key or push new secrets with:');
|
|
615
|
+
out(` lsh lib secrets push -f ${envFilePath} -e ${environment}`);
|
|
616
|
+
out();
|
|
617
|
+
return;
|
|
561
618
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
619
|
+
if (!status.localExists && !status.cloudExists) {
|
|
620
|
+
_action = 'create-and-push';
|
|
621
|
+
out('🆕 No secrets found locally or in cloud');
|
|
622
|
+
out(' Creating new .env file...');
|
|
623
|
+
if (autoExecute) {
|
|
624
|
+
await this.createEnvFromExample(envFilePath);
|
|
625
|
+
out(' Pushing to cloud...');
|
|
626
|
+
await this.push(envFilePath, effectiveEnv, force);
|
|
627
|
+
out();
|
|
628
|
+
out('✅ Setup complete! Edit your .env and run sync again to update.');
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
out('💡 Run: lsh lib secrets create && lsh lib secrets push');
|
|
632
|
+
}
|
|
633
|
+
out();
|
|
634
|
+
// Output export commands in load mode
|
|
635
|
+
if (loadMode && fs.existsSync(envFilePath)) {
|
|
636
|
+
console.log(this.generateExportCommands(envFilePath));
|
|
637
|
+
}
|
|
638
|
+
return;
|
|
571
639
|
}
|
|
572
|
-
|
|
573
|
-
|
|
640
|
+
if (status.localExists && !status.cloudExists) {
|
|
641
|
+
_action = 'push';
|
|
642
|
+
out('⬆️ Local .env exists but not in cloud');
|
|
643
|
+
if (autoExecute) {
|
|
644
|
+
out(' Pushing to cloud...');
|
|
645
|
+
await this.push(envFilePath, effectiveEnv, force);
|
|
646
|
+
out('✅ Secrets pushed to cloud!');
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
out(`💡 Run: lsh lib secrets push -f ${envFilePath} -e ${environment}`);
|
|
650
|
+
}
|
|
651
|
+
out();
|
|
652
|
+
// Output export commands in load mode
|
|
653
|
+
if (loadMode && fs.existsSync(envFilePath)) {
|
|
654
|
+
console.log(this.generateExportCommands(envFilePath));
|
|
655
|
+
}
|
|
656
|
+
return;
|
|
574
657
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
658
|
+
if (!status.localExists && status.cloudExists && status.keyMatches) {
|
|
659
|
+
_action = 'pull';
|
|
660
|
+
out('⬇️ Cloud secrets available but no local file');
|
|
661
|
+
if (autoExecute) {
|
|
662
|
+
out(' Pulling from cloud...');
|
|
663
|
+
await this.pull(envFilePath, effectiveEnv, false);
|
|
664
|
+
out('✅ Secrets pulled from cloud!');
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
out(`💡 Run: lsh lib secrets pull -f ${envFilePath} -e ${environment}`);
|
|
668
|
+
}
|
|
669
|
+
out();
|
|
670
|
+
// Output export commands in load mode
|
|
671
|
+
if (loadMode && fs.existsSync(envFilePath)) {
|
|
672
|
+
console.log(this.generateExportCommands(envFilePath));
|
|
673
|
+
}
|
|
674
|
+
return;
|
|
579
675
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
676
|
+
if (status.localExists && status.cloudExists && status.keyMatches) {
|
|
677
|
+
if (status.localModified && status.cloudModified) {
|
|
678
|
+
const localNewer = status.localModified > status.cloudModified;
|
|
679
|
+
const timeDiff = Math.abs(status.localModified.getTime() - status.cloudModified.getTime());
|
|
680
|
+
const minutesDiff = Math.floor(timeDiff / (1000 * 60));
|
|
681
|
+
// If difference is less than 1 minute, consider in sync
|
|
682
|
+
if (minutesDiff < 1) {
|
|
683
|
+
out('✅ Local and cloud are in sync!');
|
|
684
|
+
out();
|
|
685
|
+
if (!loadMode) {
|
|
686
|
+
this.showLoadInstructions(envFilePath);
|
|
687
|
+
}
|
|
688
|
+
else if (fs.existsSync(envFilePath)) {
|
|
689
|
+
console.log(this.generateExportCommands(envFilePath));
|
|
690
|
+
}
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (localNewer) {
|
|
694
|
+
_action = 'push';
|
|
695
|
+
out('⬆️ Local file is newer than cloud');
|
|
696
|
+
out(` Local: ${status.localModified.toLocaleString()}`);
|
|
697
|
+
out(` Cloud: ${status.cloudModified.toLocaleString()}`);
|
|
698
|
+
if (autoExecute) {
|
|
699
|
+
out(' Pushing to cloud...');
|
|
700
|
+
await this.push(envFilePath, effectiveEnv, force);
|
|
701
|
+
out('✅ Secrets synced to cloud!');
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
out(`💡 Run: lsh lib secrets push -f ${envFilePath} -e ${environment}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
_action = 'pull';
|
|
709
|
+
out('⬇️ Cloud is newer than local file');
|
|
710
|
+
out(` Local: ${status.localModified.toLocaleString()}`);
|
|
711
|
+
out(` Cloud: ${status.cloudModified.toLocaleString()}`);
|
|
712
|
+
if (autoExecute) {
|
|
713
|
+
out(' Pulling from cloud (backup created)...');
|
|
714
|
+
await this.pull(envFilePath, effectiveEnv, false);
|
|
715
|
+
out('✅ Secrets synced from cloud!');
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
out(`💡 Run: lsh lib secrets pull -f ${envFilePath} -e ${environment}`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
590
721
|
out();
|
|
591
722
|
if (!loadMode) {
|
|
592
723
|
this.showLoadInstructions(envFilePath);
|
|
@@ -596,52 +727,22 @@ LSH_SECRETS_KEY=${this.encryptionKey}
|
|
|
596
727
|
}
|
|
597
728
|
return;
|
|
598
729
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
609
|
-
else {
|
|
610
|
-
out(`💡 Run: lsh lib secrets push -f ${envFilePath} -e ${environment}`);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
else {
|
|
614
|
-
action = 'pull';
|
|
615
|
-
out('⬇️ Cloud is newer than local file');
|
|
616
|
-
out(` Local: ${status.localModified.toLocaleString()}`);
|
|
617
|
-
out(` Cloud: ${status.cloudModified.toLocaleString()}`);
|
|
618
|
-
if (autoExecute) {
|
|
619
|
-
out(' Pulling from cloud (backup created)...');
|
|
620
|
-
await this.pull(envFilePath, effectiveEnv, false);
|
|
621
|
-
out('✅ Secrets synced from cloud!');
|
|
622
|
-
}
|
|
623
|
-
else {
|
|
624
|
-
out(`💡 Run: lsh lib secrets pull -f ${envFilePath} -e ${environment}`);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
out();
|
|
628
|
-
if (!loadMode) {
|
|
629
|
-
this.showLoadInstructions(envFilePath);
|
|
630
|
-
}
|
|
631
|
-
else if (fs.existsSync(envFilePath)) {
|
|
632
|
-
console.log(this.generateExportCommands(envFilePath));
|
|
633
|
-
}
|
|
634
|
-
return;
|
|
730
|
+
}
|
|
731
|
+
// Default: everything is in sync
|
|
732
|
+
out('✅ Secrets are synchronized!');
|
|
733
|
+
out();
|
|
734
|
+
if (!loadMode) {
|
|
735
|
+
this.showLoadInstructions(envFilePath);
|
|
736
|
+
}
|
|
737
|
+
else if (fs.existsSync(envFilePath)) {
|
|
738
|
+
console.log(this.generateExportCommands(envFilePath));
|
|
635
739
|
}
|
|
636
740
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
643
|
-
else if (fs.existsSync(envFilePath)) {
|
|
644
|
-
console.log(this.generateExportCommands(envFilePath));
|
|
741
|
+
finally {
|
|
742
|
+
// Restore original logger level if it was changed
|
|
743
|
+
if (loadMode && originalLogLevel !== undefined) {
|
|
744
|
+
logger.setLevel(originalLogLevel);
|
|
745
|
+
}
|
|
645
746
|
}
|
|
646
747
|
}
|
|
647
748
|
/**
|
|
@@ -124,15 +124,16 @@ export class ZshImportManager {
|
|
|
124
124
|
return result;
|
|
125
125
|
}
|
|
126
126
|
catch (error) {
|
|
127
|
+
const err = error;
|
|
127
128
|
this.log({
|
|
128
129
|
type: 'error',
|
|
129
130
|
name: 'IMPORT_ERROR',
|
|
130
131
|
status: 'failed',
|
|
131
|
-
reason:
|
|
132
|
+
reason: err.message
|
|
132
133
|
});
|
|
133
134
|
const result = {
|
|
134
135
|
success: false,
|
|
135
|
-
message: `Import failed: ${
|
|
136
|
+
message: `Import failed: ${err.message}`,
|
|
136
137
|
diagnostics: this.diagnostics,
|
|
137
138
|
stats: { total: 0, succeeded: 0, failed: 1, skipped: 0, conflicts: 0 },
|
|
138
139
|
};
|
|
@@ -280,7 +281,7 @@ export class ZshImportManager {
|
|
|
280
281
|
*/
|
|
281
282
|
async loadExistingItems() {
|
|
282
283
|
// Get existing aliases from context
|
|
283
|
-
const context = this.executor.
|
|
284
|
+
const context = this.executor.getContext();
|
|
284
285
|
if (context && context.variables) {
|
|
285
286
|
for (const key in context.variables) {
|
|
286
287
|
if (key.startsWith('alias_')) {
|
|
@@ -353,11 +354,12 @@ export class ZshImportManager {
|
|
|
353
354
|
stats.succeeded++;
|
|
354
355
|
}
|
|
355
356
|
catch (error) {
|
|
357
|
+
const err = error;
|
|
356
358
|
this.log({
|
|
357
359
|
type: 'alias',
|
|
358
360
|
name: alias.name,
|
|
359
361
|
status: 'failed',
|
|
360
|
-
reason:
|
|
362
|
+
reason: err.message,
|
|
361
363
|
source: `line ${alias.line}`,
|
|
362
364
|
});
|
|
363
365
|
stats.failed++;
|
|
@@ -415,11 +417,12 @@ export class ZshImportManager {
|
|
|
415
417
|
stats.succeeded++;
|
|
416
418
|
}
|
|
417
419
|
catch (error) {
|
|
420
|
+
const err = error;
|
|
418
421
|
this.log({
|
|
419
422
|
type: 'export',
|
|
420
423
|
name: export_.name,
|
|
421
424
|
status: 'failed',
|
|
422
|
-
reason:
|
|
425
|
+
reason: err.message,
|
|
423
426
|
source: `line ${export_.line}`,
|
|
424
427
|
});
|
|
425
428
|
stats.failed++;
|
|
@@ -490,11 +493,12 @@ export class ZshImportManager {
|
|
|
490
493
|
stats.succeeded++;
|
|
491
494
|
}
|
|
492
495
|
catch (error) {
|
|
496
|
+
const err = error;
|
|
493
497
|
this.log({
|
|
494
498
|
type: 'function',
|
|
495
499
|
name: func.name,
|
|
496
500
|
status: 'disabled',
|
|
497
|
-
reason: `Parse error: ${
|
|
501
|
+
reason: `Parse error: ${err.message}`,
|
|
498
502
|
source: `line ${func.line}`,
|
|
499
503
|
});
|
|
500
504
|
stats.failed++;
|
|
@@ -521,11 +525,12 @@ export class ZshImportManager {
|
|
|
521
525
|
stats.succeeded++;
|
|
522
526
|
}
|
|
523
527
|
catch (error) {
|
|
528
|
+
const err = error;
|
|
524
529
|
this.log({
|
|
525
530
|
type: 'setopt',
|
|
526
531
|
name: setopt.option,
|
|
527
532
|
status: 'disabled',
|
|
528
|
-
reason:
|
|
533
|
+
reason: err.message,
|
|
529
534
|
source: `line ${setopt.line}`,
|
|
530
535
|
});
|
|
531
536
|
stats.failed++;
|
|
@@ -635,7 +640,8 @@ export class ZshImportManager {
|
|
|
635
640
|
fs.appendFileSync(this.options.diagnosticLog, logContent + '\n\n', 'utf8');
|
|
636
641
|
}
|
|
637
642
|
catch (error) {
|
|
638
|
-
|
|
643
|
+
const err = error;
|
|
644
|
+
console.error(`Failed to write diagnostic log: ${err.message}`);
|
|
639
645
|
}
|
|
640
646
|
}
|
|
641
647
|
/**
|
|
@@ -684,7 +690,7 @@ export class ZshImportManager {
|
|
|
684
690
|
if (diagnostic.status === 'conflict')
|
|
685
691
|
stats.conflicts++;
|
|
686
692
|
if (!stats.byType[diagnostic.type]) {
|
|
687
|
-
stats.byType[diagnostic.type] = { total: 0, succeeded: 0, failed: 0, skipped: 0 };
|
|
693
|
+
stats.byType[diagnostic.type] = { total: 0, succeeded: 0, failed: 0, skipped: 0, conflicts: 0 };
|
|
688
694
|
}
|
|
689
695
|
stats.byType[diagnostic.type].total++;
|
|
690
696
|
if (diagnostic.status === 'success')
|
|
@@ -693,6 +699,8 @@ export class ZshImportManager {
|
|
|
693
699
|
stats.byType[diagnostic.type].failed++;
|
|
694
700
|
if (diagnostic.status === 'skipped')
|
|
695
701
|
stats.byType[diagnostic.type].skipped++;
|
|
702
|
+
if (diagnostic.status === 'conflict')
|
|
703
|
+
stats.byType[diagnostic.type].conflicts++;
|
|
696
704
|
}
|
|
697
705
|
return stats;
|
|
698
706
|
}
|
|
@@ -443,7 +443,7 @@ export class JobTracker extends EventEmitter {
|
|
|
443
443
|
targetSystem: row.target_system,
|
|
444
444
|
status: row.status,
|
|
445
445
|
priority: row.priority,
|
|
446
|
-
config: row.config,
|
|
446
|
+
config: row.config || {},
|
|
447
447
|
parameters: row.parameters,
|
|
448
448
|
cpuRequest: row.cpu_request ? parseFloat(row.cpu_request) : undefined,
|
|
449
449
|
memoryRequest: row.memory_request,
|