@eldrforge/kodrdriv 1.2.25 → 1.2.27

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.
@@ -493,6 +493,8 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
493
493
  // Basic progress info even without flags
494
494
  logger.info(`[${index + 1}/${total}] ${packageName}: Running ${commandToRun}...`);
495
495
  }
496
+ // Track if publish was skipped due to no changes
497
+ let publishWasSkipped = false;
496
498
  try {
497
499
  if (isDryRun && !isBuiltInCommand) {
498
500
  // Handle inter-project dependency updates for publish commands in dry run mode
@@ -604,7 +606,6 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
604
606
  }
605
607
  // For built-in commands, shell out to a separate kodrdriv process
606
608
  // This preserves individual project configurations
607
- let publishWasSkipped;
608
609
  if (isBuiltInCommand) {
609
610
  // Extract the command name from "kodrdriv <command>"
610
611
  const builtInCommandName = commandToRun.replace('kodrdriv ', '');
@@ -701,13 +702,22 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
701
702
  }
702
703
  // Show completion status
703
704
  if (runConfig.debug || runConfig.verbose) {
704
- packageLogger.info(`✅ Completed successfully`);
705
+ if (publishWasSkipped) {
706
+ packageLogger.info(`⊘ Skipped (no code changes)`);
707
+ } else {
708
+ packageLogger.info(`✅ Completed successfully`);
709
+ }
705
710
  } else if (isPublishCommand) {
706
711
  // For publish commands, always show completion even without verbose
707
- logger.info(`[${index + 1}/${total}] ${packageName}: ✅ Completed`);
712
+ if (publishWasSkipped) {
713
+ logger.info(`[${index + 1}/${total}] ${packageName}: ⊘ Skipped (no code changes)`);
714
+ } else {
715
+ logger.info(`[${index + 1}/${total}] ${packageName}: ✅ Completed`);
716
+ }
708
717
  }
709
718
  return {
710
- success: true
719
+ success: true,
720
+ skippedNoChanges: publishWasSkipped
711
721
  };
712
722
  } catch (error) {
713
723
  var _error_message;
@@ -726,6 +736,86 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
726
736
  };
727
737
  }
728
738
  };
739
+ /**
740
+ * Generate a dry-run preview showing what would happen without executing
741
+ */ const generateDryRunPreview = async (dependencyGraph, buildOrder, command, runConfig)=>{
742
+ var _runConfig_tree;
743
+ const lines = [];
744
+ lines.push('');
745
+ lines.push('🔍 DRY RUN MODE - No changes will be made');
746
+ lines.push('');
747
+ lines.push('Build order determined:');
748
+ lines.push('');
749
+ // Group packages by dependency level
750
+ const levels = [];
751
+ const packageLevels = new Map();
752
+ for (const pkg of buildOrder){
753
+ const deps = dependencyGraph.edges.get(pkg) || new Set();
754
+ let maxDepLevel = -1;
755
+ for (const dep of deps){
756
+ var _packageLevels_get;
757
+ const depLevel = (_packageLevels_get = packageLevels.get(dep)) !== null && _packageLevels_get !== void 0 ? _packageLevels_get : 0;
758
+ maxDepLevel = Math.max(maxDepLevel, depLevel);
759
+ }
760
+ const pkgLevel = maxDepLevel + 1;
761
+ packageLevels.set(pkg, pkgLevel);
762
+ if (!levels[pkgLevel]) {
763
+ levels[pkgLevel] = [];
764
+ }
765
+ levels[pkgLevel].push(pkg);
766
+ }
767
+ // Show packages grouped by level
768
+ for(let i = 0; i < levels.length; i++){
769
+ const levelPackages = levels[i];
770
+ lines.push(`Level ${i + 1}: (${levelPackages.length} package${levelPackages.length === 1 ? '' : 's'})`);
771
+ for (const pkg of levelPackages){
772
+ const pkgInfo = dependencyGraph.packages.get(pkg);
773
+ if (!pkgInfo) continue;
774
+ // Check if package has changes (for publish command)
775
+ const isPublish = command.includes('publish');
776
+ let status = '📝 Has changes, will execute';
777
+ if (isPublish) {
778
+ try {
779
+ // Check git diff to see if there are code changes
780
+ const { stdout } = await runSecure('git', [
781
+ 'diff',
782
+ '--name-only',
783
+ 'origin/main...HEAD'
784
+ ], {
785
+ cwd: pkgInfo.path
786
+ });
787
+ const changedFiles = stdout.split('\n').filter(Boolean);
788
+ const nonVersionFiles = changedFiles.filter((f)=>f !== 'package.json' && f !== 'package-lock.json');
789
+ if (changedFiles.length === 0) {
790
+ status = '⊘ No changes, will skip';
791
+ } else if (nonVersionFiles.length === 0) {
792
+ status = '⊘ Only version bump, will skip';
793
+ } else {
794
+ status = `📝 Has changes (${nonVersionFiles.length} files), will publish`;
795
+ }
796
+ } catch {
797
+ // If we can't check git status, assume changes
798
+ status = '📝 Will execute';
799
+ }
800
+ }
801
+ lines.push(` ${pkg}`);
802
+ lines.push(` Status: ${status}`);
803
+ lines.push(` Path: ${pkgInfo.path}`);
804
+ }
805
+ lines.push('');
806
+ }
807
+ lines.push('Summary:');
808
+ lines.push(` Total packages: ${buildOrder.length}`);
809
+ lines.push(` Dependency levels: ${levels.length}`);
810
+ lines.push(` Command: ${command}`);
811
+ if ((_runConfig_tree = runConfig.tree) === null || _runConfig_tree === void 0 ? void 0 : _runConfig_tree.maxConcurrency) {
812
+ lines.push(` Max concurrency: ${runConfig.tree.maxConcurrency}`);
813
+ }
814
+ lines.push('');
815
+ lines.push('To execute for real, run the same command without --dry-run');
816
+ lines.push('');
817
+ return lines.join('\n');
818
+ };
729
819
  // Add a simple status check function
730
820
  const checkTreePublishStatus = async ()=>{
731
821
  const logger = getLogger();
@@ -880,35 +970,46 @@ const execute = async (runConfig)=>{
880
970
  }
881
971
  // Handle continue mode
882
972
  if (isContinue) {
883
- const savedContext = await loadExecutionContext(runConfig.outputDirectory);
884
- if (savedContext) {
885
- logger.info('Continuing previous tree execution...');
886
- logger.info(`Original command: ${savedContext.command}`);
887
- logger.info(`Started: ${savedContext.startTime.toISOString()}`);
888
- logger.info(`Previously completed: ${savedContext.completedPackages.length}/${savedContext.buildOrder.length} packages`);
889
- // Restore state safely
890
- let mutexLocked = false;
891
- try {
892
- await globalStateMutex.lock();
893
- mutexLocked = true;
894
- publishedVersions = savedContext.publishedVersions;
895
- globalStateMutex.unlock();
896
- mutexLocked = false;
897
- } catch (error) {
898
- if (mutexLocked) {
973
+ var _runConfig_tree19;
974
+ // For parallel execution, the checkpoint is managed by DynamicTaskPool/CheckpointManager
975
+ // For sequential execution, we use the legacy context file
976
+ const isParallelMode = (_runConfig_tree19 = runConfig.tree) === null || _runConfig_tree19 === void 0 ? void 0 : _runConfig_tree19.parallel;
977
+ if (!isParallelMode) {
978
+ // Sequential execution: load legacy context
979
+ const savedContext = await loadExecutionContext(runConfig.outputDirectory);
980
+ if (savedContext) {
981
+ logger.info('Continuing previous tree execution...');
982
+ logger.info(`Original command: ${savedContext.command}`);
983
+ logger.info(`Started: ${savedContext.startTime.toISOString()}`);
984
+ logger.info(`Previously completed: ${savedContext.completedPackages.length}/${savedContext.buildOrder.length} packages`);
985
+ // Restore state safely
986
+ let mutexLocked = false;
987
+ try {
988
+ await globalStateMutex.lock();
989
+ mutexLocked = true;
990
+ publishedVersions = savedContext.publishedVersions;
899
991
  globalStateMutex.unlock();
992
+ mutexLocked = false;
993
+ } catch (error) {
994
+ if (mutexLocked) {
995
+ globalStateMutex.unlock();
996
+ }
997
+ throw error;
900
998
  }
901
- throw error;
999
+ executionContext = savedContext;
1000
+ // Use original config but allow some overrides (like dry run)
1001
+ runConfig = {
1002
+ ...savedContext.originalConfig,
1003
+ dryRun: runConfig.dryRun,
1004
+ outputDirectory: runConfig.outputDirectory || savedContext.originalConfig.outputDirectory
1005
+ };
1006
+ } else {
1007
+ logger.warn('No previous execution context found. Starting new execution...');
902
1008
  }
903
- executionContext = savedContext;
904
- // Use original config but allow some overrides (like dry run)
905
- runConfig = {
906
- ...savedContext.originalConfig,
907
- dryRun: runConfig.dryRun,
908
- outputDirectory: runConfig.outputDirectory || savedContext.originalConfig.outputDirectory
909
- };
910
1009
  } else {
911
- logger.warn('No previous execution context found. Starting new execution...');
1010
+ // Parallel execution: checkpoint is managed by DynamicTaskPool
1011
+ // Just log that we're continuing - the actual checkpoint loading happens in DynamicTaskPool
1012
+ logger.info('Continuing previous parallel execution...');
912
1013
  }
913
1014
  } else {
914
1015
  // Reset published versions tracking for new tree execution
@@ -933,8 +1034,8 @@ const execute = async (runConfig)=>{
933
1034
  }
934
1035
  // Handle run subcommand - convert space-separated scripts to npm run commands
935
1036
  if (builtInCommand === 'run') {
936
- var _runConfig_tree19;
937
- const packageArgument = (_runConfig_tree19 = runConfig.tree) === null || _runConfig_tree19 === void 0 ? void 0 : _runConfig_tree19.packageArgument;
1037
+ var _runConfig_tree20;
1038
+ const packageArgument = (_runConfig_tree20 = runConfig.tree) === null || _runConfig_tree20 === void 0 ? void 0 : _runConfig_tree20.packageArgument;
938
1039
  if (!packageArgument) {
939
1040
  throw new Error('run subcommand requires script names. Usage: kodrdriv tree run "clean build test"');
940
1041
  }
@@ -1006,9 +1107,9 @@ const execute = async (runConfig)=>{
1006
1107
  logger.info(`${isDryRun ? 'DRY RUN: ' : ''}Analyzing workspaces at: ${directories.join(', ')}`);
1007
1108
  }
1008
1109
  try {
1009
- var _runConfig_tree20, _runConfig_tree21, _runConfig_tree22, _runConfig_tree23;
1110
+ var _runConfig_tree21, _runConfig_tree22, _runConfig_tree23, _runConfig_tree24;
1010
1111
  // Get exclusion patterns from config, fallback to empty array
1011
- const excludedPatterns = ((_runConfig_tree20 = runConfig.tree) === null || _runConfig_tree20 === void 0 ? void 0 : _runConfig_tree20.exclude) || [];
1112
+ const excludedPatterns = ((_runConfig_tree21 = runConfig.tree) === null || _runConfig_tree21 === void 0 ? void 0 : _runConfig_tree21.exclude) || [];
1012
1113
  if (excludedPatterns.length > 0) {
1013
1114
  logger.verbose(`${isDryRun ? 'DRY RUN: ' : ''}Using exclusion patterns: ${excludedPatterns.join(', ')}`);
1014
1115
  }
@@ -1035,7 +1136,7 @@ const execute = async (runConfig)=>{
1035
1136
  logger.verbose(`${isDryRun ? 'DRY RUN: ' : ''}Determining build order...`);
1036
1137
  let buildOrder = topologicalSort(dependencyGraph);
1037
1138
  // Handle start-from functionality if specified
1038
- const startFrom = (_runConfig_tree21 = runConfig.tree) === null || _runConfig_tree21 === void 0 ? void 0 : _runConfig_tree21.startFrom;
1139
+ const startFrom = (_runConfig_tree22 = runConfig.tree) === null || _runConfig_tree22 === void 0 ? void 0 : _runConfig_tree22.startFrom;
1039
1140
  if (startFrom) {
1040
1141
  logger.verbose(`${isDryRun ? 'DRY RUN: ' : ''}Looking for start package: ${startFrom}`);
1041
1142
  // Resolve the actual package name (can be package name or directory name)
@@ -1092,7 +1193,7 @@ const execute = async (runConfig)=>{
1092
1193
  logger.info(`${isDryRun ? 'DRY RUN: ' : ''}Starting execution from package '${startFrom}' (${buildOrder.length} of ${originalLength} packages remaining).`);
1093
1194
  }
1094
1195
  // Handle stop-at functionality if specified
1095
- const stopAt = (_runConfig_tree22 = runConfig.tree) === null || _runConfig_tree22 === void 0 ? void 0 : _runConfig_tree22.stopAt;
1196
+ const stopAt = (_runConfig_tree23 = runConfig.tree) === null || _runConfig_tree23 === void 0 ? void 0 : _runConfig_tree23.stopAt;
1096
1197
  if (stopAt) {
1097
1198
  logger.verbose(`${isDryRun ? 'DRY RUN: ' : ''}Looking for stop package: ${stopAt}`);
1098
1199
  // Find the package that matches the stopAt directory name
@@ -1411,8 +1512,8 @@ const execute = async (runConfig)=>{
1411
1512
  }
1412
1513
  // Handle special "checkout" command that switches all packages to specified branch
1413
1514
  if (builtInCommand === 'checkout') {
1414
- var _runConfig_tree24;
1415
- const targetBranch = (_runConfig_tree24 = runConfig.tree) === null || _runConfig_tree24 === void 0 ? void 0 : _runConfig_tree24.packageArgument;
1515
+ var _runConfig_tree25;
1516
+ const targetBranch = (_runConfig_tree25 = runConfig.tree) === null || _runConfig_tree25 === void 0 ? void 0 : _runConfig_tree25.packageArgument;
1416
1517
  if (!targetBranch) {
1417
1518
  throw new Error('checkout subcommand requires a branch name. Usage: kodrdriv tree checkout <branch-name>');
1418
1519
  }
@@ -1591,12 +1692,12 @@ const execute = async (runConfig)=>{
1591
1692
  returnOutput = `\nBuild order: ${buildOrder.join(' → ')}\n`;
1592
1693
  }
1593
1694
  // Execute command if provided (custom command or built-in command)
1594
- const cmd = (_runConfig_tree23 = runConfig.tree) === null || _runConfig_tree23 === void 0 ? void 0 : _runConfig_tree23.cmd;
1695
+ const cmd = (_runConfig_tree24 = runConfig.tree) === null || _runConfig_tree24 === void 0 ? void 0 : _runConfig_tree24.cmd;
1595
1696
  // Determine command to execute
1596
1697
  let commandToRun;
1597
1698
  let isBuiltInCommand = false;
1598
1699
  if (builtInCommand) {
1599
- var _runConfig_tree25, _runConfig_tree26, _runConfig_tree27;
1700
+ var _runConfig_tree26, _runConfig_tree27, _runConfig_tree28;
1600
1701
  // Built-in command mode: shell out to kodrdriv subprocess
1601
1702
  // Build command with propagated global options
1602
1703
  const globalOptions = [];
@@ -1613,14 +1714,14 @@ const execute = async (runConfig)=>{
1613
1714
  // Build the command with global options
1614
1715
  const optionsString = globalOptions.length > 0 ? ` ${globalOptions.join(' ')}` : '';
1615
1716
  // Add package argument for link/unlink/updates commands
1616
- const packageArg = (_runConfig_tree25 = runConfig.tree) === null || _runConfig_tree25 === void 0 ? void 0 : _runConfig_tree25.packageArgument;
1717
+ const packageArg = (_runConfig_tree26 = runConfig.tree) === null || _runConfig_tree26 === void 0 ? void 0 : _runConfig_tree26.packageArgument;
1617
1718
  const packageArgString = packageArg && (builtInCommand === 'link' || builtInCommand === 'unlink' || builtInCommand === 'updates') ? ` "${packageArg}"` : '';
1618
1719
  // Add command-specific options
1619
1720
  let commandSpecificOptions = '';
1620
- if (builtInCommand === 'unlink' && ((_runConfig_tree26 = runConfig.tree) === null || _runConfig_tree26 === void 0 ? void 0 : _runConfig_tree26.cleanNodeModules)) {
1721
+ if (builtInCommand === 'unlink' && ((_runConfig_tree27 = runConfig.tree) === null || _runConfig_tree27 === void 0 ? void 0 : _runConfig_tree27.cleanNodeModules)) {
1621
1722
  commandSpecificOptions += ' --clean-node-modules';
1622
1723
  }
1623
- if ((builtInCommand === 'link' || builtInCommand === 'unlink') && ((_runConfig_tree27 = runConfig.tree) === null || _runConfig_tree27 === void 0 ? void 0 : _runConfig_tree27.externals) && runConfig.tree.externals.length > 0) {
1724
+ if ((builtInCommand === 'link' || builtInCommand === 'unlink') && ((_runConfig_tree28 = runConfig.tree) === null || _runConfig_tree28 === void 0 ? void 0 : _runConfig_tree28.externals) && runConfig.tree.externals.length > 0) {
1624
1725
  commandSpecificOptions += ` --externals ${runConfig.tree.externals.join(' ')}`;
1625
1726
  }
1626
1727
  commandToRun = `kodrdriv ${builtInCommand}${optionsString}${packageArgString}${commandSpecificOptions}`;
@@ -1630,7 +1731,7 @@ const execute = async (runConfig)=>{
1630
1731
  commandToRun = cmd;
1631
1732
  }
1632
1733
  if (commandToRun) {
1633
- var _runConfig_tree28, _runConfig_tree29;
1734
+ var _runConfig_tree29, _runConfig_tree30;
1634
1735
  // Validate scripts for run command before execution
1635
1736
  const scriptsToValidate = runConfig.__scriptsToValidate;
1636
1737
  if (scriptsToValidate && scriptsToValidate.length > 0) {
@@ -1649,7 +1750,7 @@ const execute = async (runConfig)=>{
1649
1750
  }
1650
1751
  }
1651
1752
  // Validate command for parallel execution if parallel mode is enabled
1652
- if ((_runConfig_tree28 = runConfig.tree) === null || _runConfig_tree28 === void 0 ? void 0 : _runConfig_tree28.parallel) {
1753
+ if ((_runConfig_tree29 = runConfig.tree) === null || _runConfig_tree29 === void 0 ? void 0 : _runConfig_tree29.parallel) {
1653
1754
  const { CommandValidator } = await import('../execution/CommandValidator.js');
1654
1755
  const validation = CommandValidator.validateForParallel(commandToRun, builtInCommand);
1655
1756
  CommandValidator.logValidation(validation);
@@ -1700,9 +1801,14 @@ const execute = async (runConfig)=>{
1700
1801
  // If continuing, start from where we left off
1701
1802
  const startIndex = isContinue && executionContext ? executionContext.completedPackages.length : 0;
1702
1803
  // Check if parallel execution is enabled
1703
- if ((_runConfig_tree29 = runConfig.tree) === null || _runConfig_tree29 === void 0 ? void 0 : _runConfig_tree29.parallel) {
1804
+ if ((_runConfig_tree30 = runConfig.tree) === null || _runConfig_tree30 === void 0 ? void 0 : _runConfig_tree30.parallel) {
1704
1805
  var _runConfig_tree_retry1, _runConfig_tree_retry2, _runConfig_tree_retry3, _runConfig_tree_retry4;
1705
1806
  logger.info('🚀 Using parallel execution mode');
1807
+ // If dry run, show preview instead of executing
1808
+ if (isDryRun) {
1809
+ const preview = await generateDryRunPreview(dependencyGraph, buildOrder, commandToRun, runConfig);
1810
+ return preview;
1811
+ }
1706
1812
  // Import parallel execution components
1707
1813
  const { TreeExecutionAdapter, createParallelProgressLogger, formatParallelResult } = await import('../execution/TreeExecutionAdapter.js');
1708
1814
  const os = await import('os');