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.
@@ -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
- if (error.message.includes('bad decrypt') || error.message.includes('wrong final block length')) {
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: ' + error.message);
76
+ '\nOriginal error: ' + err.message);
76
77
  }
77
- throw error;
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 timestamp = parts[parts.length - 1];
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 (error) {
409
+ catch (_error) {
326
410
  status.keyMatches = false;
327
411
  }
328
412
  }
329
413
  }
330
414
  }
331
- catch (error) {
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
- // Use repo-aware environment if in git repo
480
- const effectiveEnv = this.getRepoAwareEnvironment(environment);
481
- const displayEnv = this.gitInfo?.repoName ? `${this.gitInfo.repoName}/${environment}` : environment;
482
- // In load mode, redirect output to stderr so stdout only has export commands
483
- const out = loadMode ? console.error : console.log;
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
- // Step 3: Check current status
505
- const status = await this.status(envFilePath, effectiveEnv);
506
- out('📊 Current Status:');
507
- out(` Encryption key: ${status.keySet ? '✅' : '❌'}`);
508
- out(` Local ${envFilePath}: ${status.localExists ? `✅ (${status.localKeys} keys)` : '❌'}`);
509
- out(` Cloud storage: ${status.cloudExists ? `✅ (${status.cloudKeys} keys)` : '❌'}`);
510
- if (status.cloudExists && status.keyMatches !== undefined) {
511
- out(` Key matches: ${status.keyMatches ? '✅' : '❌'}`);
512
- }
513
- out();
514
- // Step 4: Determine action and execute if auto mode
515
- let action = 'in-sync';
516
- if (status.cloudExists && status.keyMatches === false) {
517
- action = 'key-mismatch';
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
- out();
540
- // Output export commands in load mode
541
- if (loadMode && fs.existsSync(envFilePath)) {
542
- console.log(this.generateExportCommands(envFilePath));
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
- return;
545
- }
546
- if (status.localExists && !status.cloudExists) {
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
- else {
555
- out(`💡 Run: lsh lib secrets push -f ${envFilePath} -e ${environment}`);
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
- // Output export commands in load mode
559
- if (loadMode && fs.existsSync(envFilePath)) {
560
- console.log(this.generateExportCommands(envFilePath));
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
- return;
563
- }
564
- if (!status.localExists && status.cloudExists && status.keyMatches) {
565
- action = 'pull';
566
- out('⬇️ Cloud secrets available but no local file');
567
- if (autoExecute) {
568
- out(' Pulling from cloud...');
569
- await this.pull(envFilePath, effectiveEnv, false);
570
- out('✅ Secrets pulled from cloud!');
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
- else {
573
- out(`💡 Run: lsh lib secrets pull -f ${envFilePath} -e ${environment}`);
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
- out();
576
- // Output export commands in load mode
577
- if (loadMode && fs.existsSync(envFilePath)) {
578
- console.log(this.generateExportCommands(envFilePath));
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
- return;
581
- }
582
- if (status.localExists && status.cloudExists && status.keyMatches) {
583
- if (status.localModified && status.cloudModified) {
584
- const localNewer = status.localModified > status.cloudModified;
585
- const timeDiff = Math.abs(status.localModified.getTime() - status.cloudModified.getTime());
586
- const minutesDiff = Math.floor(timeDiff / (1000 * 60));
587
- // If difference is less than 1 minute, consider in sync
588
- if (minutesDiff < 1) {
589
- out('✅ Local and cloud are in sync!');
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
- if (localNewer) {
600
- action = 'push';
601
- out('⬆️ Local file is newer than cloud');
602
- out(` Local: ${status.localModified.toLocaleString()}`);
603
- out(` Cloud: ${status.cloudModified.toLocaleString()}`);
604
- if (autoExecute) {
605
- out(' Pushing to cloud...');
606
- await this.push(envFilePath, effectiveEnv);
607
- out('✅ Secrets synced to cloud!');
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
- // Default: everything is in sync
638
- out('✅ Secrets are synchronized!');
639
- out();
640
- if (!loadMode) {
641
- this.showLoadInstructions(envFilePath);
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: error.message
132
+ reason: err.message
132
133
  });
133
134
  const result = {
134
135
  success: false,
135
- message: `Import failed: ${error.message}`,
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.context;
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: error.message,
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: error.message,
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: ${error.message}`,
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: error.message,
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
- console.error(`Failed to write diagnostic log: ${error.message}`);
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,