@eldrforge/kodrdriv 1.2.28 → 1.2.123

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 (82) hide show
  1. package/dist/application.js +16 -13
  2. package/dist/application.js.map +1 -1
  3. package/dist/arguments.js +5 -5
  4. package/dist/arguments.js.map +1 -1
  5. package/dist/commands/audio-review.js +2 -5
  6. package/dist/commands/audio-review.js.map +1 -1
  7. package/dist/commands/clean.js +2 -4
  8. package/dist/commands/clean.js.map +1 -1
  9. package/dist/commands/commit.js +3 -6
  10. package/dist/commands/commit.js.map +1 -1
  11. package/dist/commands/development.js +7 -7
  12. package/dist/commands/development.js.map +1 -1
  13. package/dist/commands/link.js +3 -7
  14. package/dist/commands/link.js.map +1 -1
  15. package/dist/commands/precommit.js +99 -0
  16. package/dist/commands/precommit.js.map +1 -0
  17. package/dist/commands/publish.js +47 -32
  18. package/dist/commands/publish.js.map +1 -1
  19. package/dist/commands/release.js +3 -7
  20. package/dist/commands/release.js.map +1 -1
  21. package/dist/commands/review.js +4 -6
  22. package/dist/commands/review.js.map +1 -1
  23. package/dist/commands/tree.js +271 -88
  24. package/dist/commands/tree.js.map +1 -1
  25. package/dist/commands/unlink.js +3 -7
  26. package/dist/commands/unlink.js.map +1 -1
  27. package/dist/commands/updates.js +2 -4
  28. package/dist/commands/updates.js.map +1 -1
  29. package/dist/commands/versions.js +3 -7
  30. package/dist/commands/versions.js.map +1 -1
  31. package/dist/constants.js +4 -2
  32. package/dist/constants.js.map +1 -1
  33. package/dist/content/files.js +2 -4
  34. package/dist/content/files.js.map +1 -1
  35. package/dist/execution/CommandValidator.js +33 -1
  36. package/dist/execution/CommandValidator.js.map +1 -1
  37. package/dist/execution/DynamicTaskPool.js +96 -9
  38. package/dist/execution/DynamicTaskPool.js.map +1 -1
  39. package/dist/execution/ResourceMonitor.js +26 -1
  40. package/dist/execution/ResourceMonitor.js.map +1 -1
  41. package/dist/execution/TreeExecutionAdapter.js +6 -3
  42. package/dist/execution/TreeExecutionAdapter.js.map +1 -1
  43. package/dist/util/checkpointManager.js +2 -4
  44. package/dist/util/checkpointManager.js.map +1 -1
  45. package/dist/util/dependencyGraph.js +2 -4
  46. package/dist/util/dependencyGraph.js.map +1 -1
  47. package/dist/util/general.js +7 -107
  48. package/dist/util/general.js.map +1 -1
  49. package/dist/util/gitMutex.js +63 -18
  50. package/dist/util/gitMutex.js.map +1 -1
  51. package/dist/util/precommitOptimizations.js +310 -0
  52. package/dist/util/precommitOptimizations.js.map +1 -0
  53. package/dist/util/storageAdapter.js +2 -6
  54. package/dist/util/storageAdapter.js.map +1 -1
  55. package/dist/utils/branchState.js +178 -45
  56. package/dist/utils/branchState.js.map +1 -1
  57. package/package.json +6 -5
  58. package/AI-FRIENDLY-LOGGING-GUIDE.md +0 -237
  59. package/AI-LOGGING-MIGRATION-COMPLETE.md +0 -371
  60. package/ALREADY-PUBLISHED-PACKAGES-FIX.md +0 -264
  61. package/AUDIT-BRANCHES-PROGRESS-FIX.md +0 -90
  62. package/AUDIT-EXAMPLE-OUTPUT.md +0 -113
  63. package/CHECKPOINT-RECOVERY-FIX.md +0 -450
  64. package/LOGGING-MIGRATION-STATUS.md +0 -186
  65. package/MONOREPO-PUBLISH-IMPROVEMENTS.md +0 -281
  66. package/PARALLEL-EXECUTION-FIXES.md +0 -132
  67. package/PARALLEL-PUBLISH-FIXES-IMPLEMENTED.md +0 -405
  68. package/PARALLEL-PUBLISH-IMPROVEMENTS-IMPLEMENTED.md +0 -439
  69. package/PARALLEL-PUBLISH-QUICK-REFERENCE.md +0 -375
  70. package/PARALLEL_EXECUTION_FIX.md +0 -146
  71. package/PUBLISH_IMPROVEMENTS_IMPLEMENTED.md +0 -294
  72. package/RECOVERY-FIXES.md +0 -72
  73. package/SUBMODULE-LOCK-FIX.md +0 -132
  74. package/VERSION-AUDIT-FIX.md +0 -333
  75. package/WORKFLOW-PRECHECK-IMPLEMENTATION.md +0 -239
  76. package/WORKFLOW-SKIP-SUMMARY.md +0 -121
  77. package/dist/util/safety.js +0 -166
  78. package/dist/util/safety.js.map +0 -1
  79. package/dist/util/stdin.js +0 -133
  80. package/dist/util/stdin.js.map +0 -1
  81. package/dist/util/storage.js +0 -187
  82. package/dist/util/storage.js.map +0 -1
@@ -5,15 +5,17 @@ import { exec } from 'child_process';
5
5
  import { safeJsonParse, validatePackageJson, getGloballyLinkedPackages, getGitStatusSummary, getLinkedDependencies, getLinkCompatibilityProblems, runSecure, run } from '@eldrforge/git-tools';
6
6
  import util from 'util';
7
7
  import { getLogger } from '../logging.js';
8
- import { create } from '../util/storage.js';
8
+ import { createStorage } from '@eldrforge/shared';
9
9
  import { getOutputPath } from '../util/general.js';
10
10
  import { DEFAULT_OUTPUT_DIRECTORY } from '../constants.js';
11
11
  import { execute as execute$3 } from './commit.js';
12
12
  import { execute as execute$1 } from './link.js';
13
13
  import { execute as execute$2 } from './unlink.js';
14
14
  import { execute as execute$4 } from './updates.js';
15
- import { runGitWithLock } from '../util/gitMutex.js';
15
+ import { isInGitRepository, runGitWithLock } from '../util/gitMutex.js';
16
16
  import { scanForPackageJsonFiles, buildDependencyGraph, topologicalSort, parsePackageJson, shouldExclude } from '../util/dependencyGraph.js';
17
+ import { optimizePrecommitCommand, recordTestRun } from '../util/precommitOptimizations.js';
18
+ import { PerformanceTimer } from '../util/performance.js';
17
19
  import { SimpleMutex } from '../util/mutex.js';
18
20
 
19
21
  // Global state to track published versions during tree execution - protected by mutex
@@ -22,9 +24,7 @@ let executionContext = null;
22
24
  const globalStateMutex = new SimpleMutex();
23
25
  // Update inter-project dependencies in package.json based on published versions
24
26
  const updateInterProjectDependencies = async (packageDir, publishedVersions, allPackageNames, packageLogger, isDryRun)=>{
25
- const storage = create({
26
- log: packageLogger.info
27
- });
27
+ const storage = createStorage();
28
28
  const packageJsonPath = path__default.join(packageDir, 'package.json');
29
29
  if (!await storage.exists(packageJsonPath)) {
30
30
  packageLogger.verbose('No package.json found, skipping dependency updates');
@@ -83,9 +83,7 @@ const updateInterProjectDependencies = async (packageDir, publishedVersions, all
83
83
  };
84
84
  // Detect scoped dependencies from package.json and run updates for them
85
85
  const updateScopedDependencies = async (packageDir, packageLogger, isDryRun, runConfig)=>{
86
- const storage = create({
87
- log: packageLogger.info
88
- });
86
+ const storage = createStorage();
89
87
  const packageJsonPath = path__default.join(packageDir, 'package.json');
90
88
  if (!await storage.exists(packageJsonPath)) {
91
89
  packageLogger.verbose('No package.json found, skipping scoped dependency updates');
@@ -167,9 +165,7 @@ const getContextFilePath = (outputDirectory)=>{
167
165
  };
168
166
  // Save execution context to file
169
167
  const saveExecutionContext = async (context, outputDirectory)=>{
170
- const storage = create({
171
- log: ()=>{}
172
- }); // Silent storage for context operations
168
+ const storage = createStorage(); // Silent storage for context operations
173
169
  const contextFilePath = getContextFilePath(outputDirectory);
174
170
  try {
175
171
  // Ensure output directory exists
@@ -193,9 +189,7 @@ const saveExecutionContext = async (context, outputDirectory)=>{
193
189
  };
194
190
  // Load execution context from file
195
191
  const loadExecutionContext = async (outputDirectory)=>{
196
- const storage = create({
197
- log: ()=>{}
198
- }); // Silent storage for context operations
192
+ const storage = createStorage(); // Silent storage for context operations
199
193
  const contextFilePath = getContextFilePath(outputDirectory);
200
194
  try {
201
195
  if (!await storage.exists(contextFilePath)) {
@@ -221,9 +215,7 @@ const loadExecutionContext = async (outputDirectory)=>{
221
215
  };
222
216
  // Clean up context file
223
217
  const cleanupContext = async (outputDirectory)=>{
224
- const storage = create({
225
- log: ()=>{}
226
- }); // Silent storage for context operations
218
+ const storage = createStorage(); // Silent storage for context operations
227
219
  const contextFilePath = getContextFilePath(outputDirectory);
228
220
  try {
229
221
  if (await storage.exists(contextFilePath)) {
@@ -237,9 +229,7 @@ const cleanupContext = async (outputDirectory)=>{
237
229
  };
238
230
  // Helper function to promote a package to completed status in the context
239
231
  const promotePackageToCompleted = async (packageName, outputDirectory)=>{
240
- const storage = create({
241
- log: ()=>{}
242
- });
232
+ const storage = createStorage();
243
233
  const contextFilePath = getContextFilePath(outputDirectory);
244
234
  try {
245
235
  if (!await storage.exists(contextFilePath)) {
@@ -272,9 +262,7 @@ const promotePackageToCompleted = async (packageName, outputDirectory)=>{
272
262
  const validateScripts = async (packages, scripts)=>{
273
263
  const logger = getLogger();
274
264
  const missingScripts = new Map();
275
- const storage = create({
276
- log: ()=>{}
277
- });
265
+ const storage = createStorage();
278
266
  logger.debug(`Validating scripts: ${scripts.join(', ')}`);
279
267
  for (const [packageName, packageInfo] of packages){
280
268
  const packageJsonPath = path__default.join(packageInfo.path, 'package.json');
@@ -317,9 +305,7 @@ const validateScripts = async (packages, scripts)=>{
317
305
  // After kodrdriv publish, the release version is captured in the git tag,
318
306
  // while package.json contains the next dev version
319
307
  const extractPublishedVersion = async (packageDir, packageLogger)=>{
320
- const storage = create({
321
- log: packageLogger.info
322
- });
308
+ const storage = createStorage();
323
309
  const packageJsonPath = path__default.join(packageDir, 'package.json');
324
310
  try {
325
311
  // Get package name from package.json
@@ -363,7 +349,7 @@ const extractPublishedVersion = async (packageDir, packageLogger)=>{
363
349
  }
364
350
  };
365
351
  // Enhanced run function that can show output based on log level
366
- const runWithLogging = async (command, packageLogger, options = {}, showOutput = 'none')=>{
352
+ const runWithLogging = async (command, packageLogger, options = {}, showOutput = 'none', logFilePath)=>{
367
353
  const execPromise = util.promisify(exec);
368
354
  // Ensure encoding is set to 'utf8' to get string output instead of Buffer
369
355
  const execOptions = {
@@ -377,6 +363,24 @@ const runWithLogging = async (command, packageLogger, options = {}, showOutput =
377
363
  } else if (showOutput === 'minimal') {
378
364
  packageLogger.verbose(`Running: ${command}`);
379
365
  }
366
+ // Helper to write to log file
367
+ const writeToLogFile = async (content)=>{
368
+ if (!logFilePath) return;
369
+ try {
370
+ const logDir = path__default.dirname(logFilePath);
371
+ await fs.mkdir(logDir, {
372
+ recursive: true
373
+ });
374
+ await fs.appendFile(logFilePath, content + '\n', 'utf-8');
375
+ } catch (err) {
376
+ packageLogger.warn(`Failed to write to log file ${logFilePath}: ${err.message}`);
377
+ }
378
+ };
379
+ // Write command to log file
380
+ if (logFilePath) {
381
+ const timestamp = new Date().toISOString();
382
+ await writeToLogFile(`[${timestamp}] Executing: ${command}\n`);
383
+ }
380
384
  try {
381
385
  const result = await execPromise(command, execOptions);
382
386
  if (showOutput === 'full') {
@@ -401,29 +405,62 @@ const runWithLogging = async (command, packageLogger, options = {}, showOutput =
401
405
  });
402
406
  }
403
407
  }
408
+ // Write output to log file
409
+ if (logFilePath) {
410
+ const stdout = String(result.stdout);
411
+ const stderr = String(result.stderr);
412
+ if (stdout.trim()) {
413
+ await writeToLogFile(`\n=== STDOUT ===\n${stdout}`);
414
+ }
415
+ if (stderr.trim()) {
416
+ await writeToLogFile(`\n=== STDERR ===\n${stderr}`);
417
+ }
418
+ await writeToLogFile(`\n[${new Date().toISOString()}] Command completed successfully\n`);
419
+ }
404
420
  // Ensure result is properly typed as strings
405
421
  return {
406
422
  stdout: String(result.stdout),
407
423
  stderr: String(result.stderr)
408
424
  };
409
425
  } catch (error) {
426
+ // Always show error message
427
+ packageLogger.error(`Command failed: ${command}`);
428
+ // Always show stderr on failure (contains important error details like coverage failures)
429
+ if (error.stderr && error.stderr.trim()) {
430
+ packageLogger.error(`❌ STDERR:`);
431
+ error.stderr.split('\n').forEach((line)=>{
432
+ if (line.trim()) packageLogger.error(`${line}`);
433
+ });
434
+ }
435
+ // Show stdout on failure if available (may contain error context)
436
+ if (error.stdout && error.stdout.trim() && (showOutput === 'full' || showOutput === 'minimal')) {
437
+ packageLogger.info(`📤 STDOUT:`);
438
+ error.stdout.split('\n').forEach((line)=>{
439
+ if (line.trim()) packageLogger.info(`${line}`);
440
+ });
441
+ }
442
+ // Show full output in debug/verbose mode
410
443
  if (showOutput === 'full' || showOutput === 'minimal') {
411
- packageLogger.error(`Command failed: ${command}`);
412
- if (error.stdout && showOutput === 'full') {
444
+ if (error.stdout && error.stdout.trim() && showOutput === 'full') {
413
445
  packageLogger.debug('STDOUT:');
414
446
  packageLogger.debug(error.stdout);
415
- packageLogger.info(`📤 STDOUT:`);
416
- error.stdout.split('\n').forEach((line)=>{
417
- if (line.trim()) packageLogger.info(`${line}`);
418
- });
419
447
  }
420
- if (error.stderr && showOutput === 'full') {
448
+ if (error.stderr && error.stderr.trim() && showOutput === 'full') {
421
449
  packageLogger.debug('STDERR:');
422
450
  packageLogger.debug(error.stderr);
423
- packageLogger.info(`❌ STDERR:`);
424
- error.stderr.split('\n').forEach((line)=>{
425
- if (line.trim()) packageLogger.info(`${line}`);
426
- });
451
+ }
452
+ }
453
+ // Write error output to log file
454
+ if (logFilePath) {
455
+ await writeToLogFile(`\n[${new Date().toISOString()}] Command failed: ${error.message}`);
456
+ if (error.stdout) {
457
+ await writeToLogFile(`\n=== STDOUT ===\n${error.stdout}`);
458
+ }
459
+ if (error.stderr) {
460
+ await writeToLogFile(`\n=== STDERR ===\n${error.stderr}`);
461
+ }
462
+ if (error.stack) {
463
+ await writeToLogFile(`\n=== STACK TRACE ===\n${error.stack}`);
427
464
  }
428
465
  }
429
466
  throw error;
@@ -473,6 +510,15 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
473
510
  const packageLogger = createPackageLogger(packageName, index + 1, total, isDryRun);
474
511
  const packageDir = packageInfo.path;
475
512
  const logger = getLogger();
513
+ // Create log file path for publish commands
514
+ let logFilePath;
515
+ if (isBuiltInCommand && commandToRun.includes('publish')) {
516
+ var _commandToRun_split_;
517
+ const outputDir = runConfig.outputDirectory || 'output/kodrdriv';
518
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').split('.')[0];
519
+ const commandName = ((_commandToRun_split_ = commandToRun.split(' ')[1]) === null || _commandToRun_split_ === void 0 ? void 0 : _commandToRun_split_.split(' ')[0]) || 'command';
520
+ logFilePath = path__default.join(packageDir, outputDir, `${commandName}_${timestamp}.log`);
521
+ }
476
522
  // Determine output level based on flags
477
523
  // For publish commands, default to full output to show OpenAI progress and other details
478
524
  // For other commands, require --verbose or --debug for output
@@ -495,6 +541,9 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
495
541
  }
496
542
  // Track if publish was skipped due to no changes
497
543
  let publishWasSkipped = false;
544
+ // Track execution timing
545
+ const executionTimer = PerformanceTimer.start(packageLogger, `Package ${packageName} execution`);
546
+ let executionDuration;
498
547
  try {
499
548
  if (isDryRun && !isBuiltInCommand) {
500
549
  // Handle inter-project dependency updates for publish commands in dry run mode
@@ -597,18 +646,53 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
597
646
  }
598
647
  }, `${packageName}: dependency updates`);
599
648
  }
649
+ // Optimize precommit commands for custom commands (not built-in)
650
+ let effectiveCommandToRun = commandToRun;
651
+ let optimizationInfo = null;
652
+ if (!isBuiltInCommand && !isDryRun) {
653
+ const isPrecommitCommand = commandToRun.includes('precommit') || commandToRun.includes('pre-commit');
654
+ if (isPrecommitCommand) {
655
+ try {
656
+ const optimization = await optimizePrecommitCommand(packageDir, commandToRun);
657
+ effectiveCommandToRun = optimization.optimizedCommand;
658
+ optimizationInfo = {
659
+ skipped: optimization.skipped,
660
+ reasons: optimization.reasons
661
+ };
662
+ if (optimization.skipped.clean || optimization.skipped.test) {
663
+ const skippedParts = [];
664
+ if (optimization.skipped.clean) {
665
+ skippedParts.push(`clean (${optimization.reasons.clean})`);
666
+ }
667
+ if (optimization.skipped.test) {
668
+ skippedParts.push(`test (${optimization.reasons.test})`);
669
+ }
670
+ packageLogger.info(`⚡ Optimized: Skipped ${skippedParts.join(', ')}`);
671
+ if (runConfig.verbose || runConfig.debug) {
672
+ packageLogger.info(` Original: ${commandToRun}`);
673
+ packageLogger.info(` Optimized: ${effectiveCommandToRun}`);
674
+ }
675
+ }
676
+ } catch (error) {
677
+ // If optimization fails, fall back to original command
678
+ logger.debug(`Precommit optimization failed for ${packageName}: ${error.message}`);
679
+ }
680
+ }
681
+ }
600
682
  if (runConfig.debug || runConfig.verbose) {
601
683
  if (isBuiltInCommand) {
602
684
  packageLogger.info(`Executing built-in command: ${commandToRun}`);
603
685
  } else {
604
- packageLogger.info(`Executing command: ${commandToRun}`);
686
+ packageLogger.info(`Executing command: ${effectiveCommandToRun}`);
605
687
  }
606
688
  }
607
689
  // For built-in commands, shell out to a separate kodrdriv process
608
690
  // This preserves individual project configurations
609
691
  if (isBuiltInCommand) {
610
- // Extract the command name from "kodrdriv <command>"
611
- const builtInCommandName = commandToRun.replace('kodrdriv ', '');
692
+ // Extract the command name from "kodrdriv <command> [args...]"
693
+ // Split by space and take the second element (after "kodrdriv")
694
+ const commandParts = commandToRun.replace(/^kodrdriv\s+/, '').split(/\s+/);
695
+ const builtInCommandName = commandParts[0];
612
696
  if (runConfig.debug) {
613
697
  packageLogger.debug(`Shelling out to separate kodrdriv process for ${builtInCommandName} command`);
614
698
  }
@@ -618,21 +702,35 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
618
702
  packageLogger.info('⏱️ This may take several minutes (AI processing, PR creation, etc.)');
619
703
  }
620
704
  // Ensure dry-run propagates to subprocess even during overall dry-run mode
621
- const effectiveCommand = runConfig.dryRun && !commandToRun.includes('--dry-run') ? `${commandToRun} --dry-run` : commandToRun;
622
- // Add timeout wrapper for publish commands
623
- const commandTimeoutMs = 1800000; // 30 minutes for publish commands
705
+ let effectiveCommand = runConfig.dryRun && !commandToRun.includes('--dry-run') ? `${commandToRun} --dry-run` : commandToRun;
706
+ // For commit commands, ensure --sendit is used to avoid interactive prompts
707
+ // This prevents hanging when running via tree command
708
+ if (builtInCommandName === 'commit' && !effectiveCommand.includes('--sendit') && !runConfig.dryRun) {
709
+ effectiveCommand = `${effectiveCommand} --sendit`;
710
+ packageLogger.info('💡 Auto-adding --sendit flag to avoid interactive prompts in tree mode');
711
+ }
712
+ // Set timeout based on command type
713
+ let commandTimeoutMs;
624
714
  if (builtInCommandName === 'publish') {
715
+ commandTimeoutMs = 1800000; // 30 minutes for publish commands
625
716
  packageLogger.info(`⏰ Setting timeout of ${commandTimeoutMs / 60000} minutes for publish command`);
717
+ } else if (builtInCommandName === 'commit') {
718
+ commandTimeoutMs = 600000; // 10 minutes for commit commands (AI processing can take time)
719
+ packageLogger.info(`⏰ Setting timeout of ${commandTimeoutMs / 60000} minutes for commit command`);
720
+ } else {
721
+ commandTimeoutMs = 300000; // 5 minutes default for other commands
626
722
  }
627
- const commandPromise = runWithLogging(effectiveCommand, packageLogger, {}, showOutput);
723
+ const commandPromise = runWithLogging(effectiveCommand, packageLogger, {}, showOutput, logFilePath);
628
724
  const commandTimeoutPromise = new Promise((_, reject)=>{
629
725
  setTimeout(()=>reject(new Error(`Command timed out after ${commandTimeoutMs / 60000} minutes`)), commandTimeoutMs);
630
726
  });
631
727
  try {
728
+ const startTime = Date.now();
632
729
  const { stdout, stderr } = await Promise.race([
633
730
  commandPromise,
634
731
  commandTimeoutPromise
635
732
  ]);
733
+ executionDuration = Date.now() - startTime;
636
734
  // Detect explicit skip marker from publish to avoid propagating versions
637
735
  // Check both stdout (where we now write it) and stderr (winston logger output, for backward compat)
638
736
  if (builtInCommandName === 'publish' && (stdout && stdout.includes('KODRDRIV_PUBLISH_SKIPPED') || stderr && stderr.includes('KODRDRIV_PUBLISH_SKIPPED'))) {
@@ -649,7 +747,9 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
649
747
  }
650
748
  } else {
651
749
  // For custom commands, use the existing logic
652
- await runWithLogging(commandToRun, packageLogger, {}, showOutput);
750
+ const startTime = Date.now();
751
+ await runWithLogging(effectiveCommandToRun, packageLogger, {}, showOutput, logFilePath);
752
+ executionDuration = Date.now() - startTime;
653
753
  }
654
754
  // Track published version after successful publish (skip during dry run)
655
755
  if (!isDryRun && isBuiltInCommand && commandToRun.includes('publish')) {
@@ -677,11 +777,32 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
677
777
  }
678
778
  }
679
779
  }
680
- if (runConfig.debug || runConfig.verbose) {
681
- packageLogger.info(`Command completed successfully`);
780
+ // Record test run if tests were executed (not skipped)
781
+ if (!isDryRun && !isBuiltInCommand && effectiveCommandToRun.includes('test') && (!optimizationInfo || !optimizationInfo.skipped.test)) {
782
+ try {
783
+ await recordTestRun(packageDir);
784
+ } catch (error) {
785
+ logger.debug(`Failed to record test run for ${packageName}: ${error.message}`);
786
+ }
787
+ }
788
+ // End timing and show duration
789
+ if (executionDuration !== undefined) {
790
+ executionTimer.end(`Package ${packageName} execution`);
791
+ const seconds = (executionDuration / 1000).toFixed(1);
792
+ if (runConfig.debug || runConfig.verbose) {
793
+ packageLogger.info(`⏱️ Execution time: ${seconds}s`);
794
+ } else if (!isPublishCommand) {
795
+ // Show timing in completion message (publish commands have their own completion message)
796
+ logger.info(`[${index + 1}/${total}] ${packageName}: ✅ Completed (${seconds}s)`);
797
+ }
682
798
  } else {
683
- // Basic completion info
684
- logger.info(`[${index + 1}/${total}] ${packageName}: ✅ Completed`);
799
+ executionTimer.end(`Package ${packageName} execution`);
800
+ if (runConfig.debug || runConfig.verbose) {
801
+ packageLogger.info(`Command completed successfully`);
802
+ } else if (!isPublishCommand) {
803
+ // Basic completion info (publish commands have their own completion message)
804
+ logger.info(`[${index + 1}/${total}] ${packageName}: ✅ Completed`);
805
+ }
685
806
  }
686
807
  } finally{
687
808
  // Safely restore working directory
@@ -701,7 +822,7 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
701
822
  }
702
823
  }
703
824
  }
704
- // Show completion status
825
+ // Show completion status (for publish commands, this supplements the timing message above)
705
826
  if (runConfig.debug || runConfig.verbose) {
706
827
  if (publishWasSkipped) {
707
828
  packageLogger.info(`⊘ Skipped (no code changes)`);
@@ -710,30 +831,65 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
710
831
  }
711
832
  } else if (isPublishCommand) {
712
833
  // For publish commands, always show completion even without verbose
834
+ // Include timing if available
835
+ const timeStr = executionDuration !== undefined ? ` (${(executionDuration / 1000).toFixed(1)}s)` : '';
713
836
  if (publishWasSkipped) {
714
837
  logger.info(`[${index + 1}/${total}] ${packageName}: ⊘ Skipped (no code changes)`);
715
838
  } else {
716
- logger.info(`[${index + 1}/${total}] ${packageName}: ✅ Completed`);
839
+ logger.info(`[${index + 1}/${total}] ${packageName}: ✅ Completed${timeStr}`);
717
840
  }
718
841
  }
842
+ // Ensure timing is recorded even if there was an early return
843
+ if (executionDuration === undefined) {
844
+ executionDuration = executionTimer.end(`Package ${packageName} execution`);
845
+ }
719
846
  return {
720
847
  success: true,
721
- skippedNoChanges: publishWasSkipped
848
+ skippedNoChanges: publishWasSkipped,
849
+ logFile: logFilePath
722
850
  };
723
851
  } catch (error) {
724
852
  var _error_message;
853
+ // Record timing even on error
854
+ if (executionDuration === undefined) {
855
+ executionDuration = executionTimer.end(`Package ${packageName} execution`);
856
+ const seconds = (executionDuration / 1000).toFixed(1);
857
+ if (runConfig.debug || runConfig.verbose) {
858
+ packageLogger.error(`⏱️ Execution time before failure: ${seconds}s`);
859
+ }
860
+ }
725
861
  if (runConfig.debug || runConfig.verbose) {
726
862
  packageLogger.error(`❌ Execution failed: ${error.message}`);
727
863
  } else {
728
864
  logger.error(`[${index + 1}/${total}] ${packageName}: ❌ Failed - ${error.message}`);
729
865
  }
866
+ // Always show stderr if available (contains important error details)
867
+ // Note: runWithLogging already logs stderr, but we show it here too for visibility
868
+ // when error is caught at this level (e.g., from timeout wrapper)
869
+ if (error.stderr && error.stderr.trim() && !runConfig.debug && !runConfig.verbose) {
870
+ // Extract key error lines from stderr (coverage failures, test failures, etc.)
871
+ const stderrLines = error.stderr.split('\n').filter((line)=>{
872
+ const trimmed = line.trim();
873
+ return trimmed && (trimmed.includes('ERROR:') || trimmed.includes('FAIL') || trimmed.includes('coverage') || trimmed.includes('threshold') || trimmed.includes('fatal:') || trimmed.startsWith('❌'));
874
+ });
875
+ if (stderrLines.length > 0) {
876
+ logger.error(` Error details:`);
877
+ stderrLines.slice(0, 10).forEach((line)=>{
878
+ logger.error(` ${line.trim()}`);
879
+ });
880
+ if (stderrLines.length > 10) {
881
+ logger.error(` ... and ${stderrLines.length - 10} more error lines (use --verbose to see full output)`);
882
+ }
883
+ }
884
+ }
730
885
  // Check if this is a timeout error
731
886
  const errorMessage = ((_error_message = error.message) === null || _error_message === void 0 ? void 0 : _error_message.toLowerCase()) || '';
732
887
  const isTimeoutError = errorMessage && (errorMessage.includes('timeout waiting for pr') || errorMessage.includes('timeout waiting for release workflows') || errorMessage.includes('timeout reached') || errorMessage.includes('timeout') || errorMessage.includes('timed out') || errorMessage.includes('timed_out'));
733
888
  return {
734
889
  success: false,
735
890
  error,
736
- isTimeoutError
891
+ isTimeoutError,
892
+ logFile: logFilePath
737
893
  };
738
894
  }
739
895
  };
@@ -868,7 +1024,7 @@ const execute = async (runConfig)=>{
868
1024
  }
869
1025
  // Handle audit-branches command
870
1026
  if ((_runConfig_tree4 = runConfig.tree) === null || _runConfig_tree4 === void 0 ? void 0 : _runConfig_tree4.auditBranches) {
871
- var _runConfig_tree16, _runConfig_tree17, _runConfig_publish;
1027
+ var _runConfig_tree16, _runConfig_tree17, _runConfig_publish, _runConfig_tree18;
872
1028
  logger.info('🔍 Auditing branch state across all packages...');
873
1029
  const directories = ((_runConfig_tree16 = runConfig.tree) === null || _runConfig_tree16 === void 0 ? void 0 : _runConfig_tree16.directories) || [
874
1030
  process.cwd()
@@ -888,14 +1044,30 @@ const execute = async (runConfig)=>{
888
1044
  path: pkg.path
889
1045
  }));
890
1046
  const { auditBranchState, formatAuditResults } = await import('../utils/branchState.js');
1047
+ const { getRemoteDefaultBranch } = await import('@eldrforge/git-tools');
891
1048
  // For publish workflows, check branch consistency, merge conflicts, and existing PRs
892
1049
  // Don't pass an expected branch - let the audit find the most common branch
893
- const targetBranch = ((_runConfig_publish = runConfig.publish) === null || _runConfig_publish === void 0 ? void 0 : _runConfig_publish.targetBranch) || 'main';
1050
+ let targetBranch = (_runConfig_publish = runConfig.publish) === null || _runConfig_publish === void 0 ? void 0 : _runConfig_publish.targetBranch;
1051
+ if (!targetBranch) {
1052
+ // Try to detect default branch from the first package that is a git repo
1053
+ const firstGitPkg = packages.find((pkg)=>isInGitRepository(pkg.path));
1054
+ if (firstGitPkg) {
1055
+ try {
1056
+ // Cast to any to avoid type mismatch with node_modules version
1057
+ targetBranch = await getRemoteDefaultBranch(firstGitPkg.path) || 'main';
1058
+ } catch {
1059
+ targetBranch = 'main';
1060
+ }
1061
+ } else {
1062
+ targetBranch = 'main';
1063
+ }
1064
+ }
894
1065
  logger.info(`Checking for merge conflicts with '${targetBranch}' and existing pull requests...`);
895
1066
  const auditResult = await auditBranchState(packages, undefined, {
896
1067
  targetBranch,
897
1068
  checkPR: true,
898
- checkConflicts: true
1069
+ checkConflicts: true,
1070
+ concurrency: ((_runConfig_tree18 = runConfig.tree) === null || _runConfig_tree18 === void 0 ? void 0 : _runConfig_tree18.maxConcurrency) || 10
899
1071
  });
900
1072
  const formatted = formatAuditResults(auditResult);
901
1073
  logger.info('\n' + formatted);
@@ -910,13 +1082,13 @@ const execute = async (runConfig)=>{
910
1082
  const { loadRecoveryManager } = await import('../execution/RecoveryManager.js');
911
1083
  // Handle status-parallel command
912
1084
  if ((_runConfig_tree5 = runConfig.tree) === null || _runConfig_tree5 === void 0 ? void 0 : _runConfig_tree5.statusParallel) {
913
- var _runConfig_tree18, _runConfig_tree19;
1085
+ var _runConfig_tree19, _runConfig_tree20;
914
1086
  logger.info('📊 Checking parallel execution status...');
915
1087
  // Need to build dependency graph first
916
- const directories = ((_runConfig_tree18 = runConfig.tree) === null || _runConfig_tree18 === void 0 ? void 0 : _runConfig_tree18.directories) || [
1088
+ const directories = ((_runConfig_tree19 = runConfig.tree) === null || _runConfig_tree19 === void 0 ? void 0 : _runConfig_tree19.directories) || [
917
1089
  process.cwd()
918
1090
  ];
919
- const excludedPatterns = ((_runConfig_tree19 = runConfig.tree) === null || _runConfig_tree19 === void 0 ? void 0 : _runConfig_tree19.exclude) || [];
1091
+ const excludedPatterns = ((_runConfig_tree20 = runConfig.tree) === null || _runConfig_tree20 === void 0 ? void 0 : _runConfig_tree20.exclude) || [];
920
1092
  let allPackageJsonPaths = [];
921
1093
  for (const targetDirectory of directories){
922
1094
  const packageJsonPaths = await scanForPackageJsonFiles(targetDirectory, excludedPatterns);
@@ -937,12 +1109,12 @@ const execute = async (runConfig)=>{
937
1109
  }
938
1110
  // Handle validate-state command
939
1111
  if ((_runConfig_tree6 = runConfig.tree) === null || _runConfig_tree6 === void 0 ? void 0 : _runConfig_tree6.validateState) {
940
- var _runConfig_tree20, _runConfig_tree21;
1112
+ var _runConfig_tree21, _runConfig_tree22;
941
1113
  logger.info('🔍 Validating checkpoint state...');
942
- const directories = ((_runConfig_tree20 = runConfig.tree) === null || _runConfig_tree20 === void 0 ? void 0 : _runConfig_tree20.directories) || [
1114
+ const directories = ((_runConfig_tree21 = runConfig.tree) === null || _runConfig_tree21 === void 0 ? void 0 : _runConfig_tree21.directories) || [
943
1115
  process.cwd()
944
1116
  ];
945
- const excludedPatterns = ((_runConfig_tree21 = runConfig.tree) === null || _runConfig_tree21 === void 0 ? void 0 : _runConfig_tree21.exclude) || [];
1117
+ const excludedPatterns = ((_runConfig_tree22 = runConfig.tree) === null || _runConfig_tree22 === void 0 ? void 0 : _runConfig_tree22.exclude) || [];
946
1118
  let allPackageJsonPaths = [];
947
1119
  for (const targetDirectory of directories){
948
1120
  const packageJsonPaths = await scanForPackageJsonFiles(targetDirectory, excludedPatterns);
@@ -1011,10 +1183,10 @@ const execute = async (runConfig)=>{
1011
1183
  }
1012
1184
  // Handle continue mode
1013
1185
  if (isContinue) {
1014
- var _runConfig_tree22;
1186
+ var _runConfig_tree23;
1015
1187
  // For parallel execution, the checkpoint is managed by DynamicTaskPool/CheckpointManager
1016
1188
  // For sequential execution, we use the legacy context file
1017
- const isParallelMode = (_runConfig_tree22 = runConfig.tree) === null || _runConfig_tree22 === void 0 ? void 0 : _runConfig_tree22.parallel;
1189
+ const isParallelMode = (_runConfig_tree23 = runConfig.tree) === null || _runConfig_tree23 === void 0 ? void 0 : _runConfig_tree23.parallel;
1018
1190
  if (!isParallelMode) {
1019
1191
  // Sequential execution: load legacy context
1020
1192
  const savedContext = await loadExecutionContext(runConfig.outputDirectory);
@@ -1068,15 +1240,16 @@ const execute = async (runConfig)=>{
1068
1240
  'branches',
1069
1241
  'run',
1070
1242
  'checkout',
1071
- 'updates'
1243
+ 'updates',
1244
+ 'precommit'
1072
1245
  ];
1073
1246
  if (builtInCommand && !supportedBuiltInCommands.includes(builtInCommand)) {
1074
1247
  throw new Error(`Unsupported built-in command: ${builtInCommand}. Supported commands: ${supportedBuiltInCommands.join(', ')}`);
1075
1248
  }
1076
1249
  // Handle run subcommand - convert space-separated scripts to npm run commands
1077
1250
  if (builtInCommand === 'run') {
1078
- var _runConfig_tree23;
1079
- const packageArgument = (_runConfig_tree23 = runConfig.tree) === null || _runConfig_tree23 === void 0 ? void 0 : _runConfig_tree23.packageArgument;
1251
+ var _runConfig_tree24;
1252
+ const packageArgument = (_runConfig_tree24 = runConfig.tree) === null || _runConfig_tree24 === void 0 ? void 0 : _runConfig_tree24.packageArgument;
1080
1253
  if (!packageArgument) {
1081
1254
  throw new Error('run subcommand requires script names. Usage: kodrdriv tree run "clean build test"');
1082
1255
  }
@@ -1148,9 +1321,9 @@ const execute = async (runConfig)=>{
1148
1321
  logger.info(`${isDryRun ? 'DRY RUN: ' : ''}Analyzing workspaces at: ${directories.join(', ')}`);
1149
1322
  }
1150
1323
  try {
1151
- var _runConfig_tree24, _runConfig_tree25, _runConfig_tree26, _runConfig_tree27;
1324
+ var _runConfig_tree25, _runConfig_tree26, _runConfig_tree27, _runConfig_tree28;
1152
1325
  // Get exclusion patterns from config, fallback to empty array
1153
- const excludedPatterns = ((_runConfig_tree24 = runConfig.tree) === null || _runConfig_tree24 === void 0 ? void 0 : _runConfig_tree24.exclude) || [];
1326
+ const excludedPatterns = ((_runConfig_tree25 = runConfig.tree) === null || _runConfig_tree25 === void 0 ? void 0 : _runConfig_tree25.exclude) || [];
1154
1327
  if (excludedPatterns.length > 0) {
1155
1328
  logger.verbose(`${isDryRun ? 'DRY RUN: ' : ''}Using exclusion patterns: ${excludedPatterns.join(', ')}`);
1156
1329
  }
@@ -1177,7 +1350,7 @@ const execute = async (runConfig)=>{
1177
1350
  logger.verbose(`${isDryRun ? 'DRY RUN: ' : ''}Determining build order...`);
1178
1351
  let buildOrder = topologicalSort(dependencyGraph);
1179
1352
  // Handle start-from functionality if specified
1180
- const startFrom = (_runConfig_tree25 = runConfig.tree) === null || _runConfig_tree25 === void 0 ? void 0 : _runConfig_tree25.startFrom;
1353
+ const startFrom = (_runConfig_tree26 = runConfig.tree) === null || _runConfig_tree26 === void 0 ? void 0 : _runConfig_tree26.startFrom;
1181
1354
  if (startFrom) {
1182
1355
  logger.verbose(`${isDryRun ? 'DRY RUN: ' : ''}Looking for start package: ${startFrom}`);
1183
1356
  // Resolve the actual package name (can be package name or directory name)
@@ -1234,7 +1407,7 @@ const execute = async (runConfig)=>{
1234
1407
  logger.info(`${isDryRun ? 'DRY RUN: ' : ''}Starting execution from package '${startFrom}' (${buildOrder.length} of ${originalLength} packages remaining).`);
1235
1408
  }
1236
1409
  // Handle stop-at functionality if specified
1237
- const stopAt = (_runConfig_tree26 = runConfig.tree) === null || _runConfig_tree26 === void 0 ? void 0 : _runConfig_tree26.stopAt;
1410
+ const stopAt = (_runConfig_tree27 = runConfig.tree) === null || _runConfig_tree27 === void 0 ? void 0 : _runConfig_tree27.stopAt;
1238
1411
  if (stopAt) {
1239
1412
  logger.verbose(`${isDryRun ? 'DRY RUN: ' : ''}Looking for stop package: ${stopAt}`);
1240
1413
  // Find the package that matches the stopAt directory name
@@ -1366,9 +1539,7 @@ const execute = async (runConfig)=>{
1366
1539
  let maxConsumersLength = 'Consumers'.length;
1367
1540
  const branchInfos = [];
1368
1541
  // Create storage instance for consumer lookup
1369
- const storage = create({
1370
- log: ()=>{}
1371
- });
1542
+ const storage = createStorage();
1372
1543
  // Get globally linked packages once at the beginning
1373
1544
  const globallyLinkedPackages = await getGloballyLinkedPackages();
1374
1545
  // ANSI escape codes for progress display
@@ -1553,8 +1724,8 @@ const execute = async (runConfig)=>{
1553
1724
  }
1554
1725
  // Handle special "checkout" command that switches all packages to specified branch
1555
1726
  if (builtInCommand === 'checkout') {
1556
- var _runConfig_tree28;
1557
- const targetBranch = (_runConfig_tree28 = runConfig.tree) === null || _runConfig_tree28 === void 0 ? void 0 : _runConfig_tree28.packageArgument;
1727
+ var _runConfig_tree29;
1728
+ const targetBranch = (_runConfig_tree29 = runConfig.tree) === null || _runConfig_tree29 === void 0 ? void 0 : _runConfig_tree29.packageArgument;
1558
1729
  if (!targetBranch) {
1559
1730
  throw new Error('checkout subcommand requires a branch name. Usage: kodrdriv tree checkout <branch-name>');
1560
1731
  }
@@ -1733,12 +1904,12 @@ const execute = async (runConfig)=>{
1733
1904
  returnOutput = `\nBuild order: ${buildOrder.join(' → ')}\n`;
1734
1905
  }
1735
1906
  // Execute command if provided (custom command or built-in command)
1736
- const cmd = (_runConfig_tree27 = runConfig.tree) === null || _runConfig_tree27 === void 0 ? void 0 : _runConfig_tree27.cmd;
1907
+ const cmd = (_runConfig_tree28 = runConfig.tree) === null || _runConfig_tree28 === void 0 ? void 0 : _runConfig_tree28.cmd;
1737
1908
  // Determine command to execute
1738
1909
  let commandToRun;
1739
1910
  let isBuiltInCommand = false;
1740
1911
  if (builtInCommand) {
1741
- var _runConfig_tree29, _runConfig_tree30, _runConfig_tree31;
1912
+ var _runConfig_tree30, _runConfig_tree31, _runConfig_tree32;
1742
1913
  // Built-in command mode: shell out to kodrdriv subprocess
1743
1914
  // Build command with propagated global options
1744
1915
  const globalOptions = [];
@@ -1755,14 +1926,14 @@ const execute = async (runConfig)=>{
1755
1926
  // Build the command with global options
1756
1927
  const optionsString = globalOptions.length > 0 ? ` ${globalOptions.join(' ')}` : '';
1757
1928
  // Add package argument for link/unlink/updates commands
1758
- const packageArg = (_runConfig_tree29 = runConfig.tree) === null || _runConfig_tree29 === void 0 ? void 0 : _runConfig_tree29.packageArgument;
1929
+ const packageArg = (_runConfig_tree30 = runConfig.tree) === null || _runConfig_tree30 === void 0 ? void 0 : _runConfig_tree30.packageArgument;
1759
1930
  const packageArgString = packageArg && (builtInCommand === 'link' || builtInCommand === 'unlink' || builtInCommand === 'updates') ? ` "${packageArg}"` : '';
1760
1931
  // Add command-specific options
1761
1932
  let commandSpecificOptions = '';
1762
- if (builtInCommand === 'unlink' && ((_runConfig_tree30 = runConfig.tree) === null || _runConfig_tree30 === void 0 ? void 0 : _runConfig_tree30.cleanNodeModules)) {
1933
+ if (builtInCommand === 'unlink' && ((_runConfig_tree31 = runConfig.tree) === null || _runConfig_tree31 === void 0 ? void 0 : _runConfig_tree31.cleanNodeModules)) {
1763
1934
  commandSpecificOptions += ' --clean-node-modules';
1764
1935
  }
1765
- if ((builtInCommand === 'link' || builtInCommand === 'unlink') && ((_runConfig_tree31 = runConfig.tree) === null || _runConfig_tree31 === void 0 ? void 0 : _runConfig_tree31.externals) && runConfig.tree.externals.length > 0) {
1936
+ if ((builtInCommand === 'link' || builtInCommand === 'unlink') && ((_runConfig_tree32 = runConfig.tree) === null || _runConfig_tree32 === void 0 ? void 0 : _runConfig_tree32.externals) && runConfig.tree.externals.length > 0) {
1766
1937
  commandSpecificOptions += ` --externals ${runConfig.tree.externals.join(' ')}`;
1767
1938
  }
1768
1939
  commandToRun = `kodrdriv ${builtInCommand}${optionsString}${packageArgString}${commandSpecificOptions}`;
@@ -1772,7 +1943,7 @@ const execute = async (runConfig)=>{
1772
1943
  commandToRun = cmd;
1773
1944
  }
1774
1945
  if (commandToRun) {
1775
- var _runConfig_tree32, _runConfig_tree33;
1946
+ var _runConfig_tree33, _runConfig_tree34;
1776
1947
  // Validate scripts for run command before execution
1777
1948
  const scriptsToValidate = runConfig.__scriptsToValidate;
1778
1949
  if (scriptsToValidate && scriptsToValidate.length > 0) {
@@ -1791,7 +1962,7 @@ const execute = async (runConfig)=>{
1791
1962
  }
1792
1963
  }
1793
1964
  // Validate command for parallel execution if parallel mode is enabled
1794
- if ((_runConfig_tree32 = runConfig.tree) === null || _runConfig_tree32 === void 0 ? void 0 : _runConfig_tree32.parallel) {
1965
+ if ((_runConfig_tree33 = runConfig.tree) === null || _runConfig_tree33 === void 0 ? void 0 : _runConfig_tree33.parallel) {
1795
1966
  const { CommandValidator } = await import('../execution/CommandValidator.js');
1796
1967
  const validation = CommandValidator.validateForParallel(commandToRun, builtInCommand);
1797
1968
  CommandValidator.logValidation(validation);
@@ -1802,11 +1973,12 @@ const execute = async (runConfig)=>{
1802
1973
  throw new Error('Command validation failed for parallel execution');
1803
1974
  }
1804
1975
  // Apply recommended concurrency if not explicitly set
1805
- if (!runConfig.tree.maxConcurrency && builtInCommand) {
1976
+ if (!runConfig.tree.maxConcurrency) {
1806
1977
  const os = await import('os');
1807
- const recommended = CommandValidator.getRecommendedConcurrency(builtInCommand, os.cpus().length);
1978
+ const recommended = CommandValidator.getRecommendedConcurrency(builtInCommand, os.cpus().length, commandToRun);
1808
1979
  if (recommended !== os.cpus().length) {
1809
- logger.info(`💡 Using recommended concurrency for ${builtInCommand}: ${recommended}`);
1980
+ const reason = builtInCommand ? builtInCommand : `custom command "${commandToRun}"`;
1981
+ logger.info(`💡 Using recommended concurrency for ${reason}: ${recommended}`);
1810
1982
  runConfig.tree.maxConcurrency = recommended;
1811
1983
  }
1812
1984
  }
@@ -1842,7 +2014,7 @@ const execute = async (runConfig)=>{
1842
2014
  // If continuing, start from where we left off
1843
2015
  const startIndex = isContinue && executionContext ? executionContext.completedPackages.length : 0;
1844
2016
  // Check if parallel execution is enabled
1845
- if ((_runConfig_tree33 = runConfig.tree) === null || _runConfig_tree33 === void 0 ? void 0 : _runConfig_tree33.parallel) {
2017
+ if ((_runConfig_tree34 = runConfig.tree) === null || _runConfig_tree34 === void 0 ? void 0 : _runConfig_tree34.parallel) {
1846
2018
  var _runConfig_tree_retry1, _runConfig_tree_retry2, _runConfig_tree_retry3, _runConfig_tree_retry4;
1847
2019
  logger.info('🚀 Using parallel execution mode');
1848
2020
  // If dry run, show preview instead of executing
@@ -1875,6 +2047,7 @@ const execute = async (runConfig)=>{
1875
2047
  return formattedResult;
1876
2048
  }
1877
2049
  // Sequential execution
2050
+ const executionStartTime = Date.now();
1878
2051
  for(let i = startIndex; i < buildOrder.length; i++){
1879
2052
  const packageName = buildOrder[i];
1880
2053
  // Skip if already completed (in continue mode)
@@ -1977,8 +2150,18 @@ const execute = async (runConfig)=>{
1977
2150
  }
1978
2151
  }
1979
2152
  if (!failedPackage) {
2153
+ const totalExecutionTime = Date.now() - executionStartTime;
2154
+ const totalSeconds = (totalExecutionTime / 1000).toFixed(1);
2155
+ const totalMinutes = (totalExecutionTime / 60000).toFixed(1);
2156
+ const timeDisplay = totalExecutionTime < 60000 ? `${totalSeconds}s` : `${totalMinutes}min (${totalSeconds}s)`;
2157
+ logger.info('');
2158
+ logger.info('═══════════════════════════════════════════════════════════');
1980
2159
  const summary = `${isDryRun ? 'DRY RUN: ' : ''}All ${buildOrder.length} packages completed successfully! 🎉`;
1981
2160
  logger.info(summary);
2161
+ logger.info(`⏱️ Total execution time: ${timeDisplay}`);
2162
+ logger.info(`📦 Packages processed: ${successCount}/${buildOrder.length}`);
2163
+ logger.info('═══════════════════════════════════════════════════════════');
2164
+ logger.info('');
1982
2165
  // Clean up context on successful completion
1983
2166
  if (isBuiltInCommand && (builtInCommand === 'publish' || builtInCommand === 'run') && !isDryRun) {
1984
2167
  await cleanupContext(runConfig.outputDirectory);