@eldrforge/kodrdriv 0.1.0 → 1.2.1

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.
Files changed (71) hide show
  1. package/README.md +1 -0
  2. package/dist/application.js +25 -3
  3. package/dist/application.js.map +1 -1
  4. package/dist/arguments.js +103 -18
  5. package/dist/arguments.js.map +1 -1
  6. package/dist/commands/audio-commit.js +28 -7
  7. package/dist/commands/audio-commit.js.map +1 -1
  8. package/dist/commands/audio-review.js +28 -7
  9. package/dist/commands/audio-review.js.map +1 -1
  10. package/dist/commands/commit.js +75 -18
  11. package/dist/commands/commit.js.map +1 -1
  12. package/dist/commands/development.js +264 -0
  13. package/dist/commands/development.js.map +1 -0
  14. package/dist/commands/link.js +356 -181
  15. package/dist/commands/link.js.map +1 -1
  16. package/dist/commands/publish.js +166 -32
  17. package/dist/commands/publish.js.map +1 -1
  18. package/dist/commands/release.js +78 -13
  19. package/dist/commands/release.js.map +1 -1
  20. package/dist/commands/review.js +10 -6
  21. package/dist/commands/review.js.map +1 -1
  22. package/dist/commands/tree.js +450 -24
  23. package/dist/commands/tree.js.map +1 -1
  24. package/dist/commands/unlink.js +267 -372
  25. package/dist/commands/unlink.js.map +1 -1
  26. package/dist/commands/versions.js +224 -0
  27. package/dist/commands/versions.js.map +1 -0
  28. package/dist/constants.js +29 -10
  29. package/dist/constants.js.map +1 -1
  30. package/dist/content/diff.js.map +1 -1
  31. package/dist/content/files.js +192 -0
  32. package/dist/content/files.js.map +1 -0
  33. package/dist/content/log.js +16 -0
  34. package/dist/content/log.js.map +1 -1
  35. package/dist/main.js +0 -0
  36. package/dist/prompt/commit.js +9 -2
  37. package/dist/prompt/commit.js.map +1 -1
  38. package/dist/prompt/instructions/commit.md +20 -2
  39. package/dist/prompt/instructions/release.md +27 -10
  40. package/dist/prompt/instructions/review.md +75 -8
  41. package/dist/prompt/release.js +13 -5
  42. package/dist/prompt/release.js.map +1 -1
  43. package/dist/types.js +21 -5
  44. package/dist/types.js.map +1 -1
  45. package/dist/util/child.js +112 -26
  46. package/dist/util/child.js.map +1 -1
  47. package/dist/util/countdown.js +215 -0
  48. package/dist/util/countdown.js.map +1 -0
  49. package/dist/util/general.js +31 -7
  50. package/dist/util/general.js.map +1 -1
  51. package/dist/util/git.js +587 -0
  52. package/dist/util/git.js.map +1 -0
  53. package/dist/util/github.js +519 -3
  54. package/dist/util/github.js.map +1 -1
  55. package/dist/util/interactive.js +245 -79
  56. package/dist/util/interactive.js.map +1 -1
  57. package/dist/util/openai.js +70 -22
  58. package/dist/util/openai.js.map +1 -1
  59. package/dist/util/performance.js +1 -69
  60. package/dist/util/performance.js.map +1 -1
  61. package/dist/util/storage.js +28 -1
  62. package/dist/util/storage.js.map +1 -1
  63. package/dist/util/validation.js +1 -25
  64. package/dist/util/validation.js.map +1 -1
  65. package/package.json +10 -8
  66. package/test-multiline/cli/package.json +8 -0
  67. package/test-multiline/core/package.json +5 -0
  68. package/test-multiline/mobile/package.json +8 -0
  69. package/test-multiline/web/package.json +8 -0
  70. package/dist/util/npmOptimizations.js +0 -174
  71. package/dist/util/npmOptimizations.js.map +0 -1
@@ -9,6 +9,7 @@ import { safeJsonParse, validatePackageJson } from '../util/validation.js';
9
9
  import { getOutputPath } from '../util/general.js';
10
10
  import { DEFAULT_OUTPUT_DIRECTORY } from '../constants.js';
11
11
  import { execute as execute$1 } from './commit.js';
12
+ import { getGloballyLinkedPackages, getGitStatusSummary, getLinkedDependencies, getLinkCompatibilityProblems } from '../util/git.js';
12
13
 
13
14
  function _define_property(obj, key, value) {
14
15
  if (key in obj) {
@@ -29,7 +30,11 @@ let executionContext = null;
29
30
  // Simple mutex to prevent race conditions in global state access
30
31
  class SimpleMutex {
31
32
  async lock() {
32
- return new Promise((resolve)=>{
33
+ return new Promise((resolve, reject)=>{
34
+ if (this.destroyed) {
35
+ reject(new Error('Mutex has been destroyed'));
36
+ return;
37
+ }
33
38
  if (!this.locked) {
34
39
  this.locked = true;
35
40
  resolve();
@@ -39,16 +44,49 @@ class SimpleMutex {
39
44
  });
40
45
  }
41
46
  unlock() {
47
+ if (this.destroyed) {
48
+ return;
49
+ }
42
50
  this.locked = false;
43
51
  const next = this.queue.shift();
44
52
  if (next) {
45
53
  this.locked = true;
46
- next();
54
+ try {
55
+ next();
56
+ } catch {
57
+ // If resolver throws, unlock and continue with next in queue
58
+ this.locked = false;
59
+ const nextInQueue = this.queue.shift();
60
+ if (nextInQueue) {
61
+ this.locked = true;
62
+ nextInQueue();
63
+ }
64
+ }
65
+ }
66
+ }
67
+ destroy() {
68
+ this.destroyed = true;
69
+ this.locked = false;
70
+ // Reject all queued promises to prevent memory leaks
71
+ while(this.queue.length > 0){
72
+ const resolver = this.queue.shift();
73
+ if (resolver) {
74
+ try {
75
+ // Treat as rejected promise to clean up
76
+ resolver(new Error('Mutex destroyed'));
77
+ } catch {
78
+ // Ignore errors from rejected resolvers
79
+ }
80
+ }
47
81
  }
48
82
  }
83
+ isDestroyed() {
84
+ return this.destroyed;
85
+ }
49
86
  constructor(){
50
87
  _define_property(this, "locked", false);
51
88
  _define_property(this, "queue", []);
89
+ _define_property(this, "destroyed", false);
52
90
  }
53
91
  }
54
92
  const globalStateMutex = new SimpleMutex();
@@ -149,7 +187,7 @@ const loadExecutionContext = async (outputDirectory)=>{
149
187
  return null;
150
188
  }
151
189
  const contextContent = await storage.readFile(contextFilePath, 'utf-8');
152
- const contextData = JSON.parse(contextContent);
190
+ const contextData = safeJsonParse(contextContent, contextFilePath);
153
191
  // Restore dates from ISO strings
154
192
  return {
155
193
  ...contextData,
@@ -537,21 +575,24 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
537
575
  try {
538
576
  if (isDryRun) {
539
577
  // Handle inter-project dependency updates for publish commands in dry run mode
540
- await globalStateMutex.lock();
541
- try {
542
- if (isBuiltInCommand && commandToRun.includes('publish') && publishedVersions.length > 0) {
578
+ if (isBuiltInCommand && commandToRun.includes('publish') && publishedVersions.length > 0) {
579
+ let mutexLocked = false;
580
+ try {
581
+ await globalStateMutex.lock();
582
+ mutexLocked = true;
543
583
  packageLogger.info('Would check for inter-project dependency updates before publish...');
544
584
  const versionSnapshot = [
545
585
  ...publishedVersions
546
586
  ]; // Create safe copy
547
587
  globalStateMutex.unlock();
588
+ mutexLocked = false;
548
589
  await updateInterProjectDependencies(packageDir, versionSnapshot, allPackageNames, packageLogger, isDryRun);
549
- } else {
550
- globalStateMutex.unlock();
590
+ } catch (error) {
591
+ if (mutexLocked) {
592
+ globalStateMutex.unlock();
593
+ }
594
+ throw error;
551
595
  }
552
- } catch (error) {
553
- globalStateMutex.unlock();
554
- throw error;
555
596
  }
556
597
  // Use main logger for the specific message tests expect
557
598
  logger.info(`DRY RUN: Would execute: ${commandToRun}`);
@@ -562,6 +603,16 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
562
603
  // Change to the package directory and run the command
563
604
  const originalCwd = process.cwd();
564
605
  try {
606
+ // Validate package directory exists before changing to it
607
+ try {
608
+ await fs__default.access(packageDir);
609
+ const stat = await fs__default.stat(packageDir);
610
+ if (!stat.isDirectory()) {
611
+ throw new Error(`Path is not a directory: ${packageDir}`);
612
+ }
613
+ } catch (accessError) {
614
+ throw new Error(`Cannot access package directory: ${packageDir} - ${accessError.message}`);
615
+ }
565
616
  process.chdir(packageDir);
566
617
  if (runConfig.debug) {
567
618
  packageLogger.debug(`Changed to directory: ${packageDir}`);
@@ -610,12 +661,19 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
610
661
  if (isBuiltInCommand && commandToRun.includes('publish')) {
611
662
  const publishedVersion = await extractPublishedVersion(packageDir, packageLogger);
612
663
  if (publishedVersion) {
613
- await globalStateMutex.lock();
664
+ let mutexLocked = false;
614
665
  try {
666
+ await globalStateMutex.lock();
667
+ mutexLocked = true;
615
668
  publishedVersions.push(publishedVersion);
616
669
  packageLogger.info(`Tracked published version: ${publishedVersion.packageName}@${publishedVersion.version}`);
617
- } finally{
618
670
  globalStateMutex.unlock();
671
+ mutexLocked = false;
672
+ } catch (error) {
673
+ if (mutexLocked) {
674
+ globalStateMutex.unlock();
675
+ }
676
+ throw error;
619
677
  }
620
678
  }
621
679
  }
@@ -626,9 +684,20 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
626
684
  logger.info(`[${index + 1}/${total}] ${packageName}: ✅ Completed`);
627
685
  }
628
686
  } finally{
629
- process.chdir(originalCwd);
630
- if (runConfig.debug) {
631
- packageLogger.debug(`Restored working directory to: ${originalCwd}`);
687
+ // Safely restore working directory
688
+ try {
689
+ // Validate original directory still exists before changing back
690
+ const fs = await import('fs/promises');
691
+ await fs.access(originalCwd);
692
+ process.chdir(originalCwd);
693
+ if (runConfig.debug) {
694
+ packageLogger.debug(`Restored working directory to: ${originalCwd}`);
695
+ }
696
+ } catch (restoreError) {
697
+ // If we can't restore to original directory, at least log the issue
698
+ packageLogger.error(`Failed to restore working directory to ${originalCwd}: ${restoreError.message}`);
699
+ packageLogger.error(`Current working directory is now: ${process.cwd()}`);
700
+ // Don't throw here to avoid masking the original error
632
701
  }
633
702
  }
634
703
  }
@@ -661,11 +730,18 @@ const execute = async (runConfig)=>{
661
730
  logger.info(`Started: ${savedContext.startTime.toISOString()}`);
662
731
  logger.info(`Previously completed: ${savedContext.completedPackages.length}/${savedContext.buildOrder.length} packages`);
663
732
  // Restore state safely
664
- await globalStateMutex.lock();
733
+ let mutexLocked = false;
665
734
  try {
735
+ await globalStateMutex.lock();
736
+ mutexLocked = true;
666
737
  publishedVersions = savedContext.publishedVersions;
667
- } finally{
668
738
  globalStateMutex.unlock();
739
+ mutexLocked = false;
740
+ } catch (error) {
741
+ if (mutexLocked) {
742
+ globalStateMutex.unlock();
743
+ }
744
+ throw error;
669
745
  }
670
746
  executionContext = savedContext;
671
747
  // Use original config but allow some overrides (like dry run)
@@ -688,7 +764,9 @@ const execute = async (runConfig)=>{
688
764
  'commit',
689
765
  'publish',
690
766
  'link',
691
- 'unlink'
767
+ 'unlink',
768
+ 'development',
769
+ 'branches'
692
770
  ];
693
771
  if (builtInCommand && !supportedBuiltInCommands.includes(builtInCommand)) {
694
772
  throw new Error(`Unsupported built-in command: ${builtInCommand}. Supported commands: ${supportedBuiltInCommands.join(', ')}`);
@@ -703,7 +781,7 @@ const execute = async (runConfig)=>{
703
781
  logger.info(`${isDryRun ? 'DRY RUN: ' : ''}Analyzing workspaces at: ${targetDirectories.join(', ')}`);
704
782
  }
705
783
  try {
706
- var _runConfig_tree3, _runConfig_tree4, _runConfig_tree5, _runConfig_tree6;
784
+ var _runConfig_tree3, _runConfig_tree4, _runConfig_tree5, _runConfig_tree6, _runConfig_tree7;
707
785
  // Get exclusion patterns from config, fallback to empty array
708
786
  const excludedPatterns = ((_runConfig_tree3 = runConfig.tree) === null || _runConfig_tree3 === void 0 ? void 0 : _runConfig_tree3.excludedPatterns) || [];
709
787
  if (excludedPatterns.length > 0) {
@@ -781,13 +859,335 @@ const execute = async (runConfig)=>{
781
859
  logger.info(`${isDryRun ? 'DRY RUN: ' : ''}Resuming from '${startFrom}' - skipping ${skippedCount} package${skippedCount === 1 ? '' : 's'}`);
782
860
  }
783
861
  }
862
+ // Handle stop-at functionality if specified
863
+ const stopAt = (_runConfig_tree5 = runConfig.tree) === null || _runConfig_tree5 === void 0 ? void 0 : _runConfig_tree5.stopAt;
864
+ if (stopAt) {
865
+ logger.verbose(`${isDryRun ? 'DRY RUN: ' : ''}Looking for stop package: ${stopAt}`);
866
+ // Find the package that matches the stopAt directory name
867
+ const stopIndex = buildOrder.findIndex((packageName)=>{
868
+ const packageInfo = dependencyGraph.packages.get(packageName);
869
+ const dirName = path__default.basename(packageInfo.path);
870
+ return dirName === stopAt || packageName === stopAt;
871
+ });
872
+ if (stopIndex === -1) {
873
+ // Check if the package exists but was excluded across all directories
874
+ let allPackageJsonPathsForCheck = [];
875
+ for (const targetDirectory of targetDirectories){
876
+ const packageJsonPaths = await scanForPackageJsonFiles(targetDirectory, []); // No exclusions
877
+ allPackageJsonPathsForCheck = allPackageJsonPathsForCheck.concat(packageJsonPaths);
878
+ }
879
+ let wasExcluded = false;
880
+ for (const packageJsonPath of allPackageJsonPathsForCheck){
881
+ try {
882
+ const packageInfo = await parsePackageJson(packageJsonPath);
883
+ const dirName = path__default.basename(packageInfo.path);
884
+ if (dirName === stopAt || packageInfo.name === stopAt) {
885
+ // Check if this package was excluded
886
+ if (shouldExclude(packageJsonPath, excludedPatterns)) {
887
+ wasExcluded = true;
888
+ break;
889
+ }
890
+ }
891
+ } catch {
892
+ continue;
893
+ }
894
+ }
895
+ if (wasExcluded) {
896
+ const excludedPatternsStr = excludedPatterns.join(', ');
897
+ throw new Error(`Package directory '${stopAt}' was excluded by exclusion patterns: ${excludedPatternsStr}. Remove the exclusion pattern or choose a different stop package.`);
898
+ } else {
899
+ const availablePackages = buildOrder.map((name)=>{
900
+ const packageInfo = dependencyGraph.packages.get(name);
901
+ return `${path__default.basename(packageInfo.path)} (${name})`;
902
+ }).join(', ');
903
+ throw new Error(`Package directory '${stopAt}' not found. Available packages: ${availablePackages}`);
904
+ }
905
+ }
906
+ // Truncate the build order before the stop package (the stop package is not executed)
907
+ const originalLength = buildOrder.length;
908
+ buildOrder = buildOrder.slice(0, stopIndex);
909
+ const stoppedCount = originalLength - stopIndex;
910
+ if (stoppedCount > 0) {
911
+ logger.info(`${isDryRun ? 'DRY RUN: ' : ''}Stopping before '${stopAt}' - excluding ${stoppedCount} package${stoppedCount === 1 ? '' : 's'}`);
912
+ }
913
+ }
914
+ // Helper function to determine version scope indicator
915
+ const getVersionScopeIndicator = (versionRange)=>{
916
+ // Remove whitespace and check the pattern
917
+ const cleanRange = versionRange.trim();
918
+ // Preserve the original prefix (^, ~, >=, etc.)
919
+ const prefixMatch = cleanRange.match(/^([^0-9]*)/);
920
+ const prefix = prefixMatch ? prefixMatch[1] : '';
921
+ // Extract the version part after the prefix
922
+ const versionPart = cleanRange.substring(prefix.length);
923
+ // Count the number of dots to determine scope
924
+ const dotCount = (versionPart.match(/\./g) || []).length;
925
+ if (dotCount >= 2) {
926
+ // Has patch version (e.g., "^4.4.32" -> "^P")
927
+ return prefix + 'P';
928
+ } else if (dotCount === 1) {
929
+ // Has minor version only (e.g., "^4.4" -> "^m")
930
+ return prefix + 'm';
931
+ } else if (dotCount === 0 && versionPart.match(/^\d+$/)) {
932
+ // Has major version only (e.g., "^4" -> "^M")
933
+ return prefix + 'M';
934
+ }
935
+ // For complex ranges or non-standard formats, return as-is
936
+ return cleanRange;
937
+ };
938
+ // Helper function to find packages that consume a given package
939
+ const findConsumingPackagesForBranches = async (targetPackageName, allPackages, storage)=>{
940
+ const consumers = [];
941
+ // Extract scope from target package name (e.g., "@fjell/eslint-config" -> "@fjell/")
942
+ const targetScope = targetPackageName.includes('/') ? targetPackageName.split('/')[0] + '/' : null;
943
+ for (const [packageName, packageInfo] of allPackages){
944
+ if (packageName === targetPackageName) continue;
945
+ try {
946
+ const packageJsonPath = path__default.join(packageInfo.path, 'package.json');
947
+ const packageJsonContent = await storage.readFile(packageJsonPath, 'utf-8');
948
+ const parsed = safeJsonParse(packageJsonContent, packageJsonPath);
949
+ const packageJson = validatePackageJson(parsed, packageJsonPath);
950
+ // Check if this package depends on the target package and get the version range
951
+ const dependencyTypes = [
952
+ 'dependencies',
953
+ 'devDependencies',
954
+ 'peerDependencies',
955
+ 'optionalDependencies'
956
+ ];
957
+ let versionRange = null;
958
+ for (const depType of dependencyTypes){
959
+ if (packageJson[depType] && packageJson[depType][targetPackageName]) {
960
+ versionRange = packageJson[depType][targetPackageName];
961
+ break;
962
+ }
963
+ }
964
+ if (versionRange) {
965
+ // Apply scope substitution for consumers in the same scope
966
+ let consumerDisplayName = packageName;
967
+ if (targetScope && packageName.startsWith(targetScope)) {
968
+ // Replace scope with "./" (e.g., "@fjell/core" -> "./core")
969
+ consumerDisplayName = './' + packageName.substring(targetScope.length);
970
+ }
971
+ // Add version scope indicator
972
+ const scopeIndicator = getVersionScopeIndicator(versionRange);
973
+ consumerDisplayName += ` (${scopeIndicator})`;
974
+ consumers.push(consumerDisplayName);
975
+ }
976
+ } catch {
977
+ continue;
978
+ }
979
+ }
980
+ return consumers.sort();
981
+ };
982
+ // Handle special "branches" command that displays table
983
+ if (builtInCommand === 'branches') {
984
+ logger.info(`${isDryRun ? 'DRY RUN: ' : ''}Branch Status Summary:`);
985
+ logger.info('');
986
+ // Calculate column widths for nice formatting
987
+ let maxNameLength = 'Package'.length;
988
+ let maxBranchLength = 'Branch'.length;
989
+ let maxVersionLength = 'Version'.length;
990
+ let maxStatusLength = 'Status'.length;
991
+ let maxLinkLength = 'Linked'.length;
992
+ let maxConsumersLength = 'Consumers'.length;
993
+ const branchInfos = [];
994
+ // Create storage instance for consumer lookup
995
+ const storage = create({
996
+ log: ()=>{}
997
+ });
998
+ // Get globally linked packages once at the beginning
999
+ const globallyLinkedPackages = await getGloballyLinkedPackages();
1000
+ // ANSI escape codes for progress display
1001
+ const ANSI = {
1002
+ CURSOR_UP: '\x1b[1A',
1003
+ CURSOR_TO_START: '\x1b[0G',
1004
+ CLEAR_LINE: '\x1b[2K',
1005
+ GREEN: '\x1b[32m',
1006
+ BLUE: '\x1b[34m',
1007
+ YELLOW: '\x1b[33m',
1008
+ RESET: '\x1b[0m',
1009
+ BOLD: '\x1b[1m'
1010
+ };
1011
+ // Check if terminal supports ANSI
1012
+ const supportsAnsi = process.stdout.isTTY && process.env.TERM !== 'dumb' && !process.env.NO_COLOR;
1013
+ const totalPackages = buildOrder.length;
1014
+ const concurrency = 5; // Process up to 5 packages at a time
1015
+ let completedCount = 0;
1016
+ let isFirstProgress = true;
1017
+ // Function to update progress display
1018
+ const updateProgress = (currentPackage, completed, total)=>{
1019
+ if (!supportsAnsi) return;
1020
+ if (!isFirstProgress) {
1021
+ // Move cursor up and clear the line
1022
+ process.stdout.write(ANSI.CURSOR_UP + ANSI.CURSOR_TO_START + ANSI.CLEAR_LINE);
1023
+ }
1024
+ const percentage = Math.round(completed / total * 100);
1025
+ const progressBar = '█'.repeat(Math.floor(percentage / 5)) + '░'.repeat(20 - Math.floor(percentage / 5));
1026
+ const progress = `${ANSI.BLUE}${ANSI.BOLD}Analyzing packages... ${ANSI.GREEN}[${progressBar}] ${percentage}%${ANSI.RESET} ${ANSI.YELLOW}(${completed}/${total})${ANSI.RESET}`;
1027
+ const current = currentPackage ? ` - Currently: ${currentPackage}` : '';
1028
+ process.stdout.write(progress + current + '\n');
1029
+ isFirstProgress = false;
1030
+ };
1031
+ // Function to process a single package
1032
+ const processPackage = async (packageName)=>{
1033
+ const packageInfo = dependencyGraph.packages.get(packageName);
1034
+ try {
1035
+ // Process git status and consumers in parallel
1036
+ const [gitStatus, consumers] = await Promise.all([
1037
+ getGitStatusSummary(packageInfo.path),
1038
+ findConsumingPackagesForBranches(packageName, dependencyGraph.packages, storage)
1039
+ ]);
1040
+ // Check if this package is globally linked (available to be linked to)
1041
+ const isGloballyLinked = globallyLinkedPackages.has(packageName);
1042
+ const linkedText = isGloballyLinked ? '✓' : '';
1043
+ // Add asterisk to consumers that are actively linking to globally linked packages
1044
+ // and check for link problems to highlight in red
1045
+ const consumersWithLinkStatus = await Promise.all(consumers.map(async (consumer)=>{
1046
+ // Extract the base consumer name from the format "package-name (^P)" or "./scoped-name (^m)"
1047
+ const baseConsumerName = consumer.replace(/ \([^)]+\)$/, ''); // Remove version scope indicator
1048
+ // Get the original package name from display name (remove scope substitution)
1049
+ const originalConsumerName = baseConsumerName.startsWith('./') ? baseConsumerName.replace('./', packageName.split('/')[0] + '/') : baseConsumerName;
1050
+ // Find the consumer package info to get its path
1051
+ const consumerPackageInfo = Array.from(dependencyGraph.packages.values()).find((pkg)=>pkg.name === originalConsumerName);
1052
+ if (consumerPackageInfo) {
1053
+ const [consumerLinkedDeps, linkProblems] = await Promise.all([
1054
+ getLinkedDependencies(consumerPackageInfo.path),
1055
+ getLinkCompatibilityProblems(consumerPackageInfo.path, dependencyGraph.packages)
1056
+ ]);
1057
+ let consumerDisplay = consumer;
1058
+ // Add asterisk if this consumer is actively linking to this package
1059
+ if (consumerLinkedDeps.has(packageName)) {
1060
+ consumerDisplay += '*';
1061
+ }
1062
+ // Check if this consumer has link problems with the current package
1063
+ if (linkProblems.has(packageName)) {
1064
+ // Highlight in red using ANSI escape codes (only if terminal supports it)
1065
+ if (supportsAnsi) {
1066
+ consumerDisplay = `\x1b[31m${consumerDisplay}\x1b[0m`;
1067
+ } else {
1068
+ // Fallback for terminals that don't support ANSI colors
1069
+ consumerDisplay += ' [LINK PROBLEM]';
1070
+ }
1071
+ }
1072
+ return consumerDisplay;
1073
+ }
1074
+ return consumer;
1075
+ }));
1076
+ return {
1077
+ name: packageName,
1078
+ branch: gitStatus.branch,
1079
+ version: packageInfo.version,
1080
+ status: gitStatus.status,
1081
+ linked: linkedText,
1082
+ consumers: consumersWithLinkStatus
1083
+ };
1084
+ } catch (error) {
1085
+ logger.warn(`Failed to get git status for ${packageName}: ${error.message}`);
1086
+ return {
1087
+ name: packageName,
1088
+ branch: 'error',
1089
+ version: packageInfo.version,
1090
+ status: 'error',
1091
+ linked: '✗',
1092
+ consumers: [
1093
+ 'error'
1094
+ ]
1095
+ };
1096
+ }
1097
+ };
1098
+ // Process packages in batches with progress updates
1099
+ updateProgress('Starting...', 0, totalPackages);
1100
+ for(let i = 0; i < buildOrder.length; i += concurrency){
1101
+ const batch = buildOrder.slice(i, i + concurrency);
1102
+ // Update progress to show current batch
1103
+ const currentBatchStr = batch.length === 1 ? batch[0] : `${batch[0]} + ${batch.length - 1} others`;
1104
+ updateProgress(currentBatchStr, completedCount, totalPackages);
1105
+ // Process batch in parallel
1106
+ const batchResults = await Promise.all(batch.map((packageName)=>processPackage(packageName)));
1107
+ // Add results and update column widths
1108
+ for (const result of batchResults){
1109
+ branchInfos.push(result);
1110
+ maxNameLength = Math.max(maxNameLength, result.name.length);
1111
+ maxBranchLength = Math.max(maxBranchLength, result.branch.length);
1112
+ maxVersionLength = Math.max(maxVersionLength, result.version.length);
1113
+ maxStatusLength = Math.max(maxStatusLength, result.status.length);
1114
+ maxLinkLength = Math.max(maxLinkLength, result.linked.length);
1115
+ // For consumers, calculate the width based on the longest consumer name
1116
+ const maxConsumerLength = result.consumers.length > 0 ? Math.max(...result.consumers.map((c)=>c.length)) : 0;
1117
+ maxConsumersLength = Math.max(maxConsumersLength, maxConsumerLength);
1118
+ }
1119
+ completedCount += batch.length;
1120
+ updateProgress('', completedCount, totalPackages);
1121
+ }
1122
+ // Clear progress line and add spacing
1123
+ if (supportsAnsi && !isFirstProgress) {
1124
+ process.stdout.write(ANSI.CURSOR_UP + ANSI.CURSOR_TO_START + ANSI.CLEAR_LINE);
1125
+ }
1126
+ logger.info(`${ANSI.GREEN}✅ Analysis complete!${ANSI.RESET} Processed ${totalPackages} packages in batches of ${concurrency}.`);
1127
+ logger.info('');
1128
+ // Print header (new order: Package | Branch | Version | Status | Linked | Consumers)
1129
+ const nameHeader = 'Package'.padEnd(maxNameLength);
1130
+ const branchHeader = 'Branch'.padEnd(maxBranchLength);
1131
+ const versionHeader = 'Version'.padEnd(maxVersionLength);
1132
+ const statusHeader = 'Status'.padEnd(maxStatusLength);
1133
+ const linkHeader = 'Linked'.padEnd(maxLinkLength);
1134
+ const consumersHeader = 'Consumers';
1135
+ logger.info(`${nameHeader} | ${branchHeader} | ${versionHeader} | ${statusHeader} | ${linkHeader} | ${consumersHeader}`);
1136
+ logger.info(`${'-'.repeat(maxNameLength)} | ${'-'.repeat(maxBranchLength)} | ${'-'.repeat(maxVersionLength)} | ${'-'.repeat(maxStatusLength)} | ${'-'.repeat(maxLinkLength)} | ${'-'.repeat(9)}`);
1137
+ // Print data rows with multi-line consumers
1138
+ for (const info of branchInfos){
1139
+ const nameCol = info.name.padEnd(maxNameLength);
1140
+ const branchCol = info.branch.padEnd(maxBranchLength);
1141
+ const versionCol = info.version.padEnd(maxVersionLength);
1142
+ const statusCol = info.status.padEnd(maxStatusLength);
1143
+ const linkCol = info.linked.padEnd(maxLinkLength);
1144
+ if (info.consumers.length === 0) {
1145
+ // No consumers - single line
1146
+ logger.info(`${nameCol} | ${branchCol} | ${versionCol} | ${statusCol} | ${linkCol} | `);
1147
+ } else if (info.consumers.length === 1) {
1148
+ // Single consumer - single line
1149
+ logger.info(`${nameCol} | ${branchCol} | ${versionCol} | ${statusCol} | ${linkCol} | ${info.consumers[0]}`);
1150
+ } else {
1151
+ // Multiple consumers - first consumer on same line, rest on new lines with continuous column separators
1152
+ logger.info(`${nameCol} | ${branchCol} | ${versionCol} | ${statusCol} | ${linkCol} | ${info.consumers[0]}`);
1153
+ // Additional consumers on separate lines with proper column separators
1154
+ const emptyNameCol = ' '.repeat(maxNameLength);
1155
+ const emptyBranchCol = ' '.repeat(maxBranchLength);
1156
+ const emptyVersionCol = ' '.repeat(maxVersionLength);
1157
+ const emptyStatusCol = ' '.repeat(maxStatusLength);
1158
+ const emptyLinkCol = ' '.repeat(maxLinkLength);
1159
+ for(let i = 1; i < info.consumers.length; i++){
1160
+ logger.info(`${emptyNameCol} | ${emptyBranchCol} | ${emptyVersionCol} | ${emptyStatusCol} | ${emptyLinkCol} | ${info.consumers[i]}`);
1161
+ }
1162
+ }
1163
+ }
1164
+ logger.info('');
1165
+ // Add legend explaining the symbols and colors
1166
+ logger.info('Legend:');
1167
+ logger.info(' * = Consumer is actively linking to this package');
1168
+ logger.info(' (^P) = Patch-level dependency (e.g., "^4.4.32")');
1169
+ logger.info(' (^m) = Minor-level dependency (e.g., "^4.4")');
1170
+ logger.info(' (^M) = Major-level dependency (e.g., "^4")');
1171
+ logger.info(' (~P), (>=M), etc. = Other version prefixes preserved');
1172
+ if (supportsAnsi) {
1173
+ logger.info(' \x1b[31mRed text\x1b[0m = Consumer has link problems (version mismatches) with this package');
1174
+ } else {
1175
+ logger.info(' [LINK PROBLEM] = Consumer has link problems (version mismatches) with this package');
1176
+ }
1177
+ logger.info('');
1178
+ return `Branch status summary for ${branchInfos.length} packages completed.`;
1179
+ }
784
1180
  // Display results
785
1181
  logger.info(`${isDryRun ? 'DRY RUN: ' : ''}Build order determined:`);
786
1182
  let returnOutput = '';
787
1183
  if (runConfig.verbose || runConfig.debug) {
788
1184
  // Verbose mode: Skip simple format, show detailed format before command execution
789
1185
  logger.info(''); // Add spacing
790
- logger.info(`Detailed Build Order for ${buildOrder.length} packages${startFrom ? ` (starting from ${startFrom})` : ''}:`);
1186
+ const rangeInfo = [];
1187
+ if (startFrom) rangeInfo.push(`starting from ${startFrom}`);
1188
+ if (stopAt) rangeInfo.push(`stopping before ${stopAt}`);
1189
+ const rangeStr = rangeInfo.length > 0 ? ` (${rangeInfo.join(', ')})` : '';
1190
+ logger.info(`Detailed Build Order for ${buildOrder.length} packages${rangeStr}:`);
791
1191
  logger.info('==========================================');
792
1192
  buildOrder.forEach((packageName, index)=>{
793
1193
  const packageInfo = dependencyGraph.packages.get(packageName);
@@ -819,14 +1219,37 @@ const execute = async (runConfig)=>{
819
1219
  returnOutput = `\nBuild order: ${buildOrder.join(' → ')}\n`;
820
1220
  }
821
1221
  // Execute command if provided (custom command or built-in command)
822
- const cmd = (_runConfig_tree5 = runConfig.tree) === null || _runConfig_tree5 === void 0 ? void 0 : _runConfig_tree5.cmd;
823
- const useParallel = ((_runConfig_tree6 = runConfig.tree) === null || _runConfig_tree6 === void 0 ? void 0 : _runConfig_tree6.parallel) || false;
1222
+ const cmd = (_runConfig_tree6 = runConfig.tree) === null || _runConfig_tree6 === void 0 ? void 0 : _runConfig_tree6.cmd;
1223
+ const useParallel = ((_runConfig_tree7 = runConfig.tree) === null || _runConfig_tree7 === void 0 ? void 0 : _runConfig_tree7.parallel) || false;
824
1224
  // Determine command to execute
825
1225
  let commandToRun;
826
1226
  let isBuiltInCommand = false;
827
1227
  if (builtInCommand) {
1228
+ var _runConfig_tree8, _runConfig_tree9;
828
1229
  // Built-in command mode: shell out to kodrdriv subprocess
829
- commandToRun = `kodrdriv ${builtInCommand}`;
1230
+ // Build command with propagated global options
1231
+ const globalOptions = [];
1232
+ // Propagate global flags that should be inherited by subprocesses
1233
+ if (runConfig.debug) globalOptions.push('--debug');
1234
+ if (runConfig.verbose) globalOptions.push('--verbose');
1235
+ if (runConfig.dryRun) globalOptions.push('--dry-run');
1236
+ if (runConfig.overrides) globalOptions.push('--overrides');
1237
+ // Propagate global options with values
1238
+ if (runConfig.model) globalOptions.push(`--model "${runConfig.model}"`);
1239
+ if (runConfig.configDirectory) globalOptions.push(`--config-dir "${runConfig.configDirectory}"`);
1240
+ if (runConfig.outputDirectory) globalOptions.push(`--output-dir "${runConfig.outputDirectory}"`);
1241
+ if (runConfig.preferencesDirectory) globalOptions.push(`--preferences-dir "${runConfig.preferencesDirectory}"`);
1242
+ // Build the command with global options
1243
+ const optionsString = globalOptions.length > 0 ? ` ${globalOptions.join(' ')}` : '';
1244
+ // Add package argument for link/unlink commands
1245
+ const packageArg = (_runConfig_tree8 = runConfig.tree) === null || _runConfig_tree8 === void 0 ? void 0 : _runConfig_tree8.packageArgument;
1246
+ const packageArgString = packageArg && (builtInCommand === 'link' || builtInCommand === 'unlink') ? ` "${packageArg}"` : '';
1247
+ // Add command-specific options
1248
+ let commandSpecificOptions = '';
1249
+ if (builtInCommand === 'unlink' && ((_runConfig_tree9 = runConfig.tree) === null || _runConfig_tree9 === void 0 ? void 0 : _runConfig_tree9.cleanNodeModules)) {
1250
+ commandSpecificOptions += ' --clean-node-modules';
1251
+ }
1252
+ commandToRun = `kodrdriv ${builtInCommand}${optionsString}${packageArgString}${commandSpecificOptions}`;
830
1253
  isBuiltInCommand = true;
831
1254
  } else if (cmd) {
832
1255
  // Custom command mode
@@ -1051,6 +1474,9 @@ const execute = async (runConfig)=>{
1051
1474
  const errorMessage = `Failed to analyze workspace: ${error.message}`;
1052
1475
  logger.error(errorMessage);
1053
1476
  throw new Error(errorMessage);
1477
+ } finally{
1478
+ // Clean up mutex resources to prevent memory leaks
1479
+ globalStateMutex.destroy();
1054
1480
  }
1055
1481
  };
1056
1482