@eldrforge/kodrdriv 1.2.5 → 1.2.7

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.
@@ -2,6 +2,7 @@
2
2
  import path__default from 'path';
3
3
  import fs__default from 'fs/promises';
4
4
  import { exec } from 'child_process';
5
+ import { runSecure } from '../util/child.js';
5
6
  import util from 'util';
6
7
  import { getLogger } from '../logging.js';
7
8
  import { create } from '../util/storage.js';
@@ -227,6 +228,84 @@ const cleanupContext = async (outputDirectory)=>{
227
228
  logger.warn(`Warning: Failed to cleanup execution context: ${error.message}`);
228
229
  }
229
230
  };
231
+ // Helper function to promote a package to completed status in the context
232
+ const promotePackageToCompleted = async (packageName, outputDirectory)=>{
233
+ const storage = create({
234
+ log: ()=>{}
235
+ });
236
+ const contextFilePath = getContextFilePath(outputDirectory);
237
+ try {
238
+ if (!await storage.exists(contextFilePath)) {
239
+ return;
240
+ }
241
+ const contextContent = await storage.readFile(contextFilePath, 'utf-8');
242
+ const contextData = safeJsonParse(contextContent, contextFilePath);
243
+ // Restore dates from ISO strings
244
+ const context = {
245
+ ...contextData,
246
+ startTime: new Date(contextData.startTime),
247
+ lastUpdateTime: new Date(contextData.lastUpdateTime),
248
+ publishedVersions: contextData.publishedVersions.map((v)=>({
249
+ ...v,
250
+ publishTime: new Date(v.publishTime)
251
+ }))
252
+ };
253
+ // Add package to completed list if not already there
254
+ if (!context.completedPackages.includes(packageName)) {
255
+ context.completedPackages.push(packageName);
256
+ context.lastUpdateTime = new Date();
257
+ await saveExecutionContext(context, outputDirectory);
258
+ }
259
+ } catch (error) {
260
+ const logger = getLogger();
261
+ logger.warn(`Warning: Failed to promote package to completed: ${error.message}`);
262
+ }
263
+ };
264
+ // Helper function to validate that all packages have the required scripts
265
+ const validateScripts = async (packages, scripts)=>{
266
+ const logger = getLogger();
267
+ const missingScripts = new Map();
268
+ const storage = create({
269
+ log: ()=>{}
270
+ });
271
+ logger.debug(`Validating scripts: ${scripts.join(', ')}`);
272
+ for (const [packageName, packageInfo] of packages){
273
+ const packageJsonPath = path__default.join(packageInfo.path, 'package.json');
274
+ const missingForPackage = [];
275
+ try {
276
+ const packageJsonContent = await storage.readFile(packageJsonPath, 'utf-8');
277
+ const packageJson = safeJsonParse(packageJsonContent, packageJsonPath);
278
+ const validated = validatePackageJson(packageJson, packageJsonPath);
279
+ // Check if each required script exists
280
+ for (const script of scripts){
281
+ if (!validated.scripts || !validated.scripts[script]) {
282
+ missingForPackage.push(script);
283
+ }
284
+ }
285
+ if (missingForPackage.length > 0) {
286
+ missingScripts.set(packageName, missingForPackage);
287
+ logger.debug(`Package ${packageName} missing scripts: ${missingForPackage.join(', ')}`);
288
+ }
289
+ } catch (error) {
290
+ logger.debug(`Error reading package.json for ${packageName}: ${error.message}`);
291
+ // If we can't read the package.json, assume all scripts are missing
292
+ missingScripts.set(packageName, scripts);
293
+ }
294
+ }
295
+ const valid = missingScripts.size === 0;
296
+ if (valid) {
297
+ logger.info(`✅ All packages have the required scripts: ${scripts.join(', ')}`);
298
+ } else {
299
+ logger.error(`❌ Script validation failed. Missing scripts:`);
300
+ for (const [packageName, missing] of missingScripts){
301
+ logger.error(` ${packageName}: ${missing.join(', ')}`);
302
+ }
303
+ }
304
+ return {
305
+ valid,
306
+ missingScripts
307
+ };
308
+ };
230
309
  // Extract published version from package.json after successful publish
231
310
  const extractPublishedVersion = async (packageDir, packageLogger)=>{
232
311
  const storage = create({
@@ -318,7 +397,7 @@ const createPackageLogger = (packageName, sequenceNumber, totalCount, isDryRun =
318
397
  };
319
398
  };
320
399
  // Helper function to format subproject error output
321
- const formatSubprojectError = (packageName, error)=>{
400
+ const formatSubprojectError = (packageName, error, _packageInfo, _position, _total)=>{
322
401
  const lines = [];
323
402
  lines.push(`❌ Command failed in package ${packageName}:`);
324
403
  // Format the main error message with indentation
@@ -576,16 +655,44 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
576
655
  if (hasUpdates) {
577
656
  // Commit the dependency updates using kodrdriv commit
578
657
  packageLogger.info('Committing inter-project dependency updates...');
658
+ packageLogger.info('⏱️ This step may take a few minutes as it generates a commit message using AI...');
659
+ // Add timeout wrapper around commit execution
660
+ const commitTimeoutMs = 300000; // 5 minutes
661
+ const commitPromise = execute$3({
662
+ ...runConfig,
663
+ dryRun: false
664
+ });
665
+ const timeoutPromise = new Promise((_, reject)=>{
666
+ setTimeout(()=>reject(new Error(`Commit operation timed out after ${commitTimeoutMs / 1000} seconds`)), commitTimeoutMs);
667
+ });
668
+ // Add progress indicator
669
+ let progressInterval = null;
579
670
  try {
580
- await execute$3({
581
- ...runConfig,
582
- dryRun: false
583
- });
584
- packageLogger.info('Inter-project dependency updates committed successfully');
671
+ // Start progress indicator
672
+ progressInterval = setInterval(()=>{
673
+ packageLogger.info('⏳ Still generating commit message... (this can take 1-3 minutes)');
674
+ }, 30000); // Every 30 seconds
675
+ await Promise.race([
676
+ commitPromise,
677
+ timeoutPromise
678
+ ]);
679
+ packageLogger.info('✅ Inter-project dependency updates committed successfully');
585
680
  } catch (commitError) {
586
- packageLogger.warn(`Failed to commit inter-project dependency updates: ${commitError.message}`);
681
+ if (commitError.message.includes('timed out')) {
682
+ packageLogger.error(`❌ Commit operation timed out after ${commitTimeoutMs / 1000} seconds`);
683
+ packageLogger.error('This usually indicates an issue with the AI service or very large changes');
684
+ packageLogger.error('You may need to manually commit the dependency updates');
685
+ } else {
686
+ packageLogger.warn(`Failed to commit inter-project dependency updates: ${commitError.message}`);
687
+ }
587
688
  // Continue with publish anyway - the updates are still in place
689
+ } finally{
690
+ if (progressInterval) {
691
+ clearInterval(progressInterval);
692
+ }
588
693
  }
694
+ } else {
695
+ packageLogger.info('No inter-project dependency updates needed');
589
696
  }
590
697
  }
591
698
  if (runConfig.debug || runConfig.verbose) {
@@ -604,14 +711,39 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
604
711
  if (runConfig.debug) {
605
712
  packageLogger.debug(`Shelling out to separate kodrdriv process for ${builtInCommandName} command`);
606
713
  }
714
+ // Add progress indication for publish commands
715
+ if (builtInCommandName === 'publish') {
716
+ packageLogger.info('🚀 Starting publish process...');
717
+ packageLogger.info('⏱️ This may take several minutes (AI processing, PR creation, etc.)');
718
+ }
607
719
  // Ensure dry-run propagates to subprocess even during overall dry-run mode
608
720
  const effectiveCommand = runConfig.dryRun && !commandToRun.includes('--dry-run') ? `${commandToRun} --dry-run` : commandToRun;
609
- // Use runWithLogging for built-in commands to capture all output
610
- const { stdout } = await runWithLogging(effectiveCommand, packageLogger, {}, showOutput);
611
- // Detect explicit skip marker from publish to avoid propagating versions
612
- if (builtInCommandName === 'publish' && stdout && stdout.includes('KODRDRIV_PUBLISH_SKIPPED')) {
613
- packageLogger.info('Publish skipped for this package; will not record or propagate a version.');
614
- publishWasSkipped = true;
721
+ // Add timeout wrapper for publish commands
722
+ const commandTimeoutMs = 1800000; // 30 minutes for publish commands
723
+ if (builtInCommandName === 'publish') {
724
+ packageLogger.info(`⏰ Setting timeout of ${commandTimeoutMs / 60000} minutes for publish command`);
725
+ }
726
+ const commandPromise = runWithLogging(effectiveCommand, packageLogger, {}, showOutput);
727
+ const commandTimeoutPromise = new Promise((_, reject)=>{
728
+ setTimeout(()=>reject(new Error(`Command timed out after ${commandTimeoutMs / 60000} minutes`)), commandTimeoutMs);
729
+ });
730
+ try {
731
+ const { stdout } = await Promise.race([
732
+ commandPromise,
733
+ commandTimeoutPromise
734
+ ]);
735
+ // Detect explicit skip marker from publish to avoid propagating versions
736
+ if (builtInCommandName === 'publish' && stdout && stdout.includes('KODRDRIV_PUBLISH_SKIPPED')) {
737
+ packageLogger.info('Publish skipped for this package; will not record or propagate a version.');
738
+ publishWasSkipped = true;
739
+ }
740
+ } catch (error) {
741
+ if (error.message.includes('timed out')) {
742
+ packageLogger.error(`❌ ${builtInCommandName} command timed out after ${commandTimeoutMs / 60000} minutes`);
743
+ packageLogger.error('This usually indicates the command is stuck waiting for user input or an external service');
744
+ throw error;
745
+ }
746
+ throw error;
615
747
  }
616
748
  } else {
617
749
  // For custom commands, use the existing logic
@@ -671,22 +803,71 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
671
803
  success: true
672
804
  };
673
805
  } catch (error) {
806
+ var _error_message;
674
807
  if (runConfig.debug || runConfig.verbose) {
675
808
  packageLogger.error(`❌ Execution failed: ${error.message}`);
676
809
  } else {
677
810
  logger.error(`[${index + 1}/${total}] ${packageName}: ❌ Failed - ${error.message}`);
678
811
  }
812
+ // Check if this is a timeout error
813
+ const errorMessage = ((_error_message = error.message) === null || _error_message === void 0 ? void 0 : _error_message.toLowerCase()) || '';
814
+ 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'));
679
815
  return {
680
816
  success: false,
681
- error
817
+ error,
818
+ isTimeoutError
682
819
  };
683
820
  }
684
821
  };
822
+ // Add a simple status check function
823
+ const checkTreePublishStatus = async ()=>{
824
+ const logger = getLogger();
825
+ try {
826
+ // Check for running kodrdriv processes
827
+ const { stdout } = await runSecure('ps', [
828
+ 'aux'
829
+ ], {});
830
+ const kodrdrivProcesses = stdout.split('\n').filter((line)=>line.includes('kodrdriv') && !line.includes('grep') && !line.includes('ps aux') && !line.includes('tree --status') // Exclude the current status command
831
+ );
832
+ if (kodrdrivProcesses.length > 0) {
833
+ logger.info('🔍 Found running kodrdriv processes:');
834
+ kodrdrivProcesses.forEach((process1)=>{
835
+ const parts = process1.trim().split(/\s+/);
836
+ const pid = parts[1];
837
+ const command = parts.slice(10).join(' ');
838
+ logger.info(` PID ${pid}: ${command}`);
839
+ });
840
+ } else {
841
+ logger.info('No kodrdriv processes currently running');
842
+ }
843
+ } catch (error) {
844
+ logger.warn('Could not check process status:', error);
845
+ }
846
+ };
685
847
  const execute = async (runConfig)=>{
686
- var _runConfig_tree, _runConfig_tree1, _runConfig_tree2, _runConfig_tree3, _runConfig_tree4;
848
+ var _runConfig_tree, _runConfig_tree1, _runConfig_tree2, _runConfig_tree3, _runConfig_tree4, _runConfig_tree5, _runConfig_tree6, _runConfig_tree7;
687
849
  const logger = getLogger();
688
850
  const isDryRun = runConfig.dryRun || false;
689
851
  const isContinue = ((_runConfig_tree = runConfig.tree) === null || _runConfig_tree === void 0 ? void 0 : _runConfig_tree.continue) || false;
852
+ const promotePackage = (_runConfig_tree1 = runConfig.tree) === null || _runConfig_tree1 === void 0 ? void 0 : _runConfig_tree1.promote;
853
+ // Debug logging
854
+ logger.debug('Tree config:', JSON.stringify(runConfig.tree, null, 2));
855
+ logger.debug('Status flag:', (_runConfig_tree2 = runConfig.tree) === null || _runConfig_tree2 === void 0 ? void 0 : _runConfig_tree2.status);
856
+ logger.debug('Full runConfig:', JSON.stringify(runConfig, null, 2));
857
+ // Handle status check
858
+ if ((_runConfig_tree3 = runConfig.tree) === null || _runConfig_tree3 === void 0 ? void 0 : _runConfig_tree3.status) {
859
+ logger.info('🔍 Checking for running kodrdriv processes...');
860
+ await checkTreePublishStatus();
861
+ return 'Status check completed';
862
+ }
863
+ // Handle promote mode
864
+ if (promotePackage) {
865
+ logger.info(`Promoting package '${promotePackage}' to completed status...`);
866
+ await promotePackageToCompleted(promotePackage, runConfig.outputDirectory);
867
+ logger.info(`✅ Package '${promotePackage}' has been marked as completed.`);
868
+ logger.info('You can now run the tree command with --continue to resume from the next package.');
869
+ return `Package '${promotePackage}' promoted to completed status.`;
870
+ }
690
871
  // Handle continue mode
691
872
  if (isContinue) {
692
873
  const savedContext = await loadExecutionContext(runConfig.outputDirectory);
@@ -725,24 +906,50 @@ const execute = async (runConfig)=>{
725
906
  executionContext = null;
726
907
  }
727
908
  // Check if we're in built-in command mode (tree command with second argument)
728
- const builtInCommand = (_runConfig_tree1 = runConfig.tree) === null || _runConfig_tree1 === void 0 ? void 0 : _runConfig_tree1.builtInCommand;
909
+ const builtInCommand = (_runConfig_tree4 = runConfig.tree) === null || _runConfig_tree4 === void 0 ? void 0 : _runConfig_tree4.builtInCommand;
729
910
  const supportedBuiltInCommands = [
730
911
  'commit',
731
912
  'publish',
732
913
  'link',
733
914
  'unlink',
734
915
  'development',
735
- 'branches'
916
+ 'branches',
917
+ 'run'
736
918
  ];
737
919
  if (builtInCommand && !supportedBuiltInCommands.includes(builtInCommand)) {
738
920
  throw new Error(`Unsupported built-in command: ${builtInCommand}. Supported commands: ${supportedBuiltInCommands.join(', ')}`);
739
921
  }
922
+ // Handle run subcommand - convert space-separated scripts to npm run commands
923
+ if (builtInCommand === 'run') {
924
+ var _runConfig_tree8;
925
+ const packageArgument = (_runConfig_tree8 = runConfig.tree) === null || _runConfig_tree8 === void 0 ? void 0 : _runConfig_tree8.packageArgument;
926
+ if (!packageArgument) {
927
+ throw new Error('run subcommand requires script names. Usage: kodrdriv tree run "clean build test"');
928
+ }
929
+ // Split the package argument by spaces to get individual script names
930
+ const scripts = packageArgument.trim().split(/\s+/).filter((script)=>script.length > 0);
931
+ if (scripts.length === 0) {
932
+ throw new Error('run subcommand requires at least one script name. Usage: kodrdriv tree run "clean build test"');
933
+ }
934
+ // Convert to npm run commands joined with &&
935
+ const npmCommands = scripts.map((script)=>`npm run ${script}`).join(' && ');
936
+ // Set this as the custom command to run
937
+ runConfig.tree = {
938
+ ...runConfig.tree,
939
+ cmd: npmCommands
940
+ };
941
+ // Clear the built-in command since we're now using custom command mode
942
+ runConfig.tree.builtInCommand = undefined;
943
+ logger.info(`Converting run subcommand to: ${npmCommands}`);
944
+ // Store scripts for later validation
945
+ runConfig.__scriptsToValidate = scripts;
946
+ }
740
947
  // Determine the target directories - either specified or current working directory
741
- const directories = ((_runConfig_tree2 = runConfig.tree) === null || _runConfig_tree2 === void 0 ? void 0 : _runConfig_tree2.directories) || [
948
+ const directories = ((_runConfig_tree5 = runConfig.tree) === null || _runConfig_tree5 === void 0 ? void 0 : _runConfig_tree5.directories) || [
742
949
  process.cwd()
743
950
  ];
744
951
  // Handle link status subcommand
745
- if (builtInCommand === 'link' && ((_runConfig_tree3 = runConfig.tree) === null || _runConfig_tree3 === void 0 ? void 0 : _runConfig_tree3.packageArgument) === 'status') {
952
+ if (builtInCommand === 'link' && ((_runConfig_tree6 = runConfig.tree) === null || _runConfig_tree6 === void 0 ? void 0 : _runConfig_tree6.packageArgument) === 'status') {
746
953
  // For tree link status, we want to show status across all packages
747
954
  logger.info(`${isDryRun ? 'DRY RUN: ' : ''}Running link status across workspace...`);
748
955
  // Create a config that will be passed to the link command
@@ -762,7 +969,7 @@ const execute = async (runConfig)=>{
762
969
  }
763
970
  }
764
971
  // Handle unlink status subcommand
765
- if (builtInCommand === 'unlink' && ((_runConfig_tree4 = runConfig.tree) === null || _runConfig_tree4 === void 0 ? void 0 : _runConfig_tree4.packageArgument) === 'status') {
972
+ if (builtInCommand === 'unlink' && ((_runConfig_tree7 = runConfig.tree) === null || _runConfig_tree7 === void 0 ? void 0 : _runConfig_tree7.packageArgument) === 'status') {
766
973
  // For tree unlink status, we want to show status across all packages
767
974
  logger.info(`${isDryRun ? 'DRY RUN: ' : ''}Running unlink status across workspace...`);
768
975
  // Create a config that will be passed to the unlink command
@@ -787,9 +994,9 @@ const execute = async (runConfig)=>{
787
994
  logger.info(`${isDryRun ? 'DRY RUN: ' : ''}Analyzing workspaces at: ${directories.join(', ')}`);
788
995
  }
789
996
  try {
790
- var _runConfig_tree5, _runConfig_tree6, _runConfig_tree7, _runConfig_tree8;
997
+ var _runConfig_tree9, _runConfig_tree10, _runConfig_tree11, _runConfig_tree12;
791
998
  // Get exclusion patterns from config, fallback to empty array
792
- const excludedPatterns = ((_runConfig_tree5 = runConfig.tree) === null || _runConfig_tree5 === void 0 ? void 0 : _runConfig_tree5.exclude) || [];
999
+ const excludedPatterns = ((_runConfig_tree9 = runConfig.tree) === null || _runConfig_tree9 === void 0 ? void 0 : _runConfig_tree9.exclude) || [];
793
1000
  if (excludedPatterns.length > 0) {
794
1001
  logger.verbose(`${isDryRun ? 'DRY RUN: ' : ''}Using exclusion patterns: ${excludedPatterns.join(', ')}`);
795
1002
  }
@@ -816,7 +1023,7 @@ const execute = async (runConfig)=>{
816
1023
  logger.verbose(`${isDryRun ? 'DRY RUN: ' : ''}Determining build order...`);
817
1024
  let buildOrder = topologicalSort(dependencyGraph);
818
1025
  // Handle start-from functionality if specified
819
- const startFrom = (_runConfig_tree6 = runConfig.tree) === null || _runConfig_tree6 === void 0 ? void 0 : _runConfig_tree6.startFrom;
1026
+ const startFrom = (_runConfig_tree10 = runConfig.tree) === null || _runConfig_tree10 === void 0 ? void 0 : _runConfig_tree10.startFrom;
820
1027
  if (startFrom) {
821
1028
  logger.verbose(`${isDryRun ? 'DRY RUN: ' : ''}Looking for start package: ${startFrom}`);
822
1029
  // Resolve the actual package name (can be package name or directory name)
@@ -862,63 +1069,18 @@ const execute = async (runConfig)=>{
862
1069
  throw new Error(`Package directory '${startFrom}' not found. Available packages: ${availablePackages}`);
863
1070
  }
864
1071
  }
865
- // Build reverse dependency map (who depends on whom)
866
- const reverseEdges = new Map();
867
- for (const [pkg, deps] of dependencyGraph.edges){
868
- for (const dep of deps){
869
- if (!reverseEdges.has(dep)) reverseEdges.set(dep, new Set());
870
- reverseEdges.get(dep).add(pkg);
871
- }
872
- if (!reverseEdges.has(pkg)) reverseEdges.set(pkg, new Set());
873
- }
874
- // Step 1: collect the start package and all its transitive dependents (consumers)
875
- const dependentsClosure = new Set();
876
- const queueDependents = [
877
- startPackageName
878
- ];
879
- while(queueDependents.length > 0){
880
- const current = queueDependents.shift();
881
- if (dependentsClosure.has(current)) continue;
882
- dependentsClosure.add(current);
883
- const consumers = reverseEdges.get(current) || new Set();
884
- for (const consumer of consumers){
885
- if (!dependentsClosure.has(consumer)) queueDependents.push(consumer);
886
- }
887
- }
888
- // Step 2: expand to include all forward dependencies required to build those packages
889
- const relevantPackages = new Set(dependentsClosure);
890
- const queueDependencies = Array.from(relevantPackages);
891
- while(queueDependencies.length > 0){
892
- const current = queueDependencies.shift();
893
- const deps = dependencyGraph.edges.get(current) || new Set();
894
- for (const dep of deps){
895
- if (!relevantPackages.has(dep)) {
896
- relevantPackages.add(dep);
897
- queueDependencies.push(dep);
898
- }
899
- }
900
- }
901
- // Filter graph to only relevant packages
902
- const filteredGraph = {
903
- packages: new Map(),
904
- edges: new Map()
905
- };
906
- for (const pkgName of relevantPackages){
907
- const info = dependencyGraph.packages.get(pkgName);
908
- filteredGraph.packages.set(pkgName, info);
909
- const deps = dependencyGraph.edges.get(pkgName) || new Set();
910
- const filteredDeps = new Set();
911
- for (const dep of deps){
912
- if (relevantPackages.has(dep)) filteredDeps.add(dep);
913
- }
914
- filteredGraph.edges.set(pkgName, filteredDeps);
1072
+ // Find the start package in the build order and start execution from there
1073
+ const startIndex = buildOrder.findIndex((pkgName)=>pkgName === startPackageName);
1074
+ if (startIndex === -1) {
1075
+ throw new Error(`Package '${startFrom}' not found in build order. This should not happen.`);
915
1076
  }
916
- // Recompute build order for the filtered subgraph
917
- buildOrder = topologicalSort(filteredGraph);
918
- logger.info(`${isDryRun ? 'DRY RUN: ' : ''}Limiting scope to '${startFrom}' and its dependencies (${buildOrder.length} package${buildOrder.length === 1 ? '' : 's'}).`);
1077
+ // Filter build order to start from the specified package
1078
+ const originalLength = buildOrder.length;
1079
+ buildOrder = buildOrder.slice(startIndex);
1080
+ logger.info(`${isDryRun ? 'DRY RUN: ' : ''}Starting execution from package '${startFrom}' (${buildOrder.length} of ${originalLength} packages remaining).`);
919
1081
  }
920
1082
  // Handle stop-at functionality if specified
921
- const stopAt = (_runConfig_tree7 = runConfig.tree) === null || _runConfig_tree7 === void 0 ? void 0 : _runConfig_tree7.stopAt;
1083
+ const stopAt = (_runConfig_tree11 = runConfig.tree) === null || _runConfig_tree11 === void 0 ? void 0 : _runConfig_tree11.stopAt;
922
1084
  if (stopAt) {
923
1085
  logger.verbose(`${isDryRun ? 'DRY RUN: ' : ''}Looking for stop package: ${stopAt}`);
924
1086
  // Find the package that matches the stopAt directory name
@@ -1277,12 +1439,12 @@ const execute = async (runConfig)=>{
1277
1439
  returnOutput = `\nBuild order: ${buildOrder.join(' → ')}\n`;
1278
1440
  }
1279
1441
  // Execute command if provided (custom command or built-in command)
1280
- const cmd = (_runConfig_tree8 = runConfig.tree) === null || _runConfig_tree8 === void 0 ? void 0 : _runConfig_tree8.cmd;
1442
+ const cmd = (_runConfig_tree12 = runConfig.tree) === null || _runConfig_tree12 === void 0 ? void 0 : _runConfig_tree12.cmd;
1281
1443
  // Determine command to execute
1282
1444
  let commandToRun;
1283
1445
  let isBuiltInCommand = false;
1284
1446
  if (builtInCommand) {
1285
- var _runConfig_tree9, _runConfig_tree10, _runConfig_tree11;
1447
+ var _runConfig_tree13, _runConfig_tree14, _runConfig_tree15;
1286
1448
  // Built-in command mode: shell out to kodrdriv subprocess
1287
1449
  // Build command with propagated global options
1288
1450
  const globalOptions = [];
@@ -1299,14 +1461,14 @@ const execute = async (runConfig)=>{
1299
1461
  // Build the command with global options
1300
1462
  const optionsString = globalOptions.length > 0 ? ` ${globalOptions.join(' ')}` : '';
1301
1463
  // Add package argument for link/unlink commands
1302
- const packageArg = (_runConfig_tree9 = runConfig.tree) === null || _runConfig_tree9 === void 0 ? void 0 : _runConfig_tree9.packageArgument;
1464
+ const packageArg = (_runConfig_tree13 = runConfig.tree) === null || _runConfig_tree13 === void 0 ? void 0 : _runConfig_tree13.packageArgument;
1303
1465
  const packageArgString = packageArg && (builtInCommand === 'link' || builtInCommand === 'unlink') ? ` "${packageArg}"` : '';
1304
1466
  // Add command-specific options
1305
1467
  let commandSpecificOptions = '';
1306
- if (builtInCommand === 'unlink' && ((_runConfig_tree10 = runConfig.tree) === null || _runConfig_tree10 === void 0 ? void 0 : _runConfig_tree10.cleanNodeModules)) {
1468
+ if (builtInCommand === 'unlink' && ((_runConfig_tree14 = runConfig.tree) === null || _runConfig_tree14 === void 0 ? void 0 : _runConfig_tree14.cleanNodeModules)) {
1307
1469
  commandSpecificOptions += ' --clean-node-modules';
1308
1470
  }
1309
- if ((builtInCommand === 'link' || builtInCommand === 'unlink') && ((_runConfig_tree11 = runConfig.tree) === null || _runConfig_tree11 === void 0 ? void 0 : _runConfig_tree11.externals) && runConfig.tree.externals.length > 0) {
1471
+ if ((builtInCommand === 'link' || builtInCommand === 'unlink') && ((_runConfig_tree15 = runConfig.tree) === null || _runConfig_tree15 === void 0 ? void 0 : _runConfig_tree15.externals) && runConfig.tree.externals.length > 0) {
1310
1472
  commandSpecificOptions += ` --externals ${runConfig.tree.externals.join(' ')}`;
1311
1473
  }
1312
1474
  commandToRun = `kodrdriv ${builtInCommand}${optionsString}${packageArgString}${commandSpecificOptions}`;
@@ -1316,6 +1478,23 @@ const execute = async (runConfig)=>{
1316
1478
  commandToRun = cmd;
1317
1479
  }
1318
1480
  if (commandToRun) {
1481
+ // Validate scripts for run command before execution
1482
+ const scriptsToValidate = runConfig.__scriptsToValidate;
1483
+ if (scriptsToValidate && scriptsToValidate.length > 0) {
1484
+ logger.info(`🔍 Validating scripts before execution: ${scriptsToValidate.join(', ')}`);
1485
+ const validation = await validateScripts(dependencyGraph.packages, scriptsToValidate);
1486
+ if (!validation.valid) {
1487
+ logger.error('');
1488
+ logger.error('❌ Script validation failed. Cannot proceed with execution.');
1489
+ logger.error('');
1490
+ logger.error('💡 To fix this:');
1491
+ logger.error(' 1. Add the missing scripts to the package.json files');
1492
+ logger.error(' 2. Or exclude packages that don\'t need these scripts using --exclude');
1493
+ logger.error(' 3. Or run individual packages that have the required scripts');
1494
+ logger.error('');
1495
+ throw new Error('Script validation failed. See details above.');
1496
+ }
1497
+ }
1319
1498
  // Create set of all package names for inter-project dependency detection
1320
1499
  const allPackageNames = new Set(Array.from(dependencyGraph.packages.keys()));
1321
1500
  // Initialize execution context if not continuing
@@ -1329,8 +1508,8 @@ const execute = async (runConfig)=>{
1329
1508
  startTime: new Date(),
1330
1509
  lastUpdateTime: new Date()
1331
1510
  };
1332
- // Save initial context
1333
- if (isBuiltInCommand && builtInCommand === 'publish' && !isDryRun) {
1511
+ // Save initial context for commands that support continuation
1512
+ if (isBuiltInCommand && (builtInCommand === 'publish' || builtInCommand === 'run') && !isDryRun) {
1334
1513
  await saveExecutionContext(executionContext, runConfig.outputDirectory);
1335
1514
  }
1336
1515
  }
@@ -1360,7 +1539,7 @@ const execute = async (runConfig)=>{
1360
1539
  if (result.success) {
1361
1540
  successCount++;
1362
1541
  // Update context
1363
- if (executionContext && isBuiltInCommand && builtInCommand === 'publish' && !isDryRun) {
1542
+ if (executionContext && isBuiltInCommand && (builtInCommand === 'publish' || builtInCommand === 'run') && !isDryRun) {
1364
1543
  executionContext.completedPackages.push(packageName);
1365
1544
  executionContext.publishedVersions = publishedVersions;
1366
1545
  executionContext.lastUpdateTime = new Date();
@@ -1373,17 +1552,76 @@ const execute = async (runConfig)=>{
1373
1552
  }
1374
1553
  } else {
1375
1554
  failedPackage = packageName;
1376
- const formattedError = formatSubprojectError(packageName, result.error);
1555
+ const formattedError = formatSubprojectError(packageName, result.error, packageInfo, i + 1, buildOrder.length);
1377
1556
  if (!isDryRun) {
1557
+ var _result_error;
1378
1558
  packageLogger.error(`Execution failed`);
1379
1559
  logger.error(formattedError);
1380
1560
  logger.error(`Failed after ${successCount} successful packages.`);
1561
+ // Special handling for timeout errors
1562
+ if (result.isTimeoutError) {
1563
+ logger.error('');
1564
+ logger.error('⏰ TIMEOUT DETECTED: This appears to be a timeout error.');
1565
+ logger.error(' This commonly happens when PR checks take longer than expected.');
1566
+ logger.error(' The execution context has been saved for recovery.');
1567
+ logger.error('');
1568
+ // Save context even on timeout for recovery
1569
+ if (executionContext && isBuiltInCommand && (builtInCommand === 'publish' || builtInCommand === 'run')) {
1570
+ executionContext.completedPackages.push(packageName);
1571
+ executionContext.publishedVersions = publishedVersions;
1572
+ executionContext.lastUpdateTime = new Date();
1573
+ await saveExecutionContext(executionContext, runConfig.outputDirectory);
1574
+ logger.info('💾 Execution context saved for recovery.');
1575
+ }
1576
+ // For publish commands, provide specific guidance about CI/CD setup
1577
+ if (builtInCommand === 'publish') {
1578
+ logger.error('');
1579
+ logger.error('💡 PUBLISH TIMEOUT TROUBLESHOOTING:');
1580
+ logger.error(' This project may not have CI/CD workflows configured.');
1581
+ logger.error(' Common solutions:');
1582
+ logger.error(' 1. Set up GitHub Actions workflows for this repository');
1583
+ logger.error(' 2. Use --sendit flag to skip user confirmation:');
1584
+ logger.error(` kodrdriv tree publish --sendit`);
1585
+ logger.error(' 3. Or manually promote this package:');
1586
+ logger.error(` kodrdriv tree publish --promote ${packageName}`);
1587
+ logger.error('');
1588
+ }
1589
+ }
1381
1590
  logger.error(`To resume from this point, run:`);
1382
1591
  if (isBuiltInCommand) {
1383
1592
  logger.error(` kodrdriv tree ${builtInCommand} --continue`);
1384
1593
  } else {
1385
1594
  logger.error(` kodrdriv tree --continue --cmd "${commandToRun}"`);
1386
1595
  }
1596
+ // For timeout errors, provide additional recovery instructions
1597
+ if (result.isTimeoutError) {
1598
+ logger.error('');
1599
+ logger.error('🔧 RECOVERY OPTIONS:');
1600
+ if (builtInCommand === 'publish') {
1601
+ logger.error(' 1. Wait for the PR checks to complete, then run:');
1602
+ logger.error(` cd ${packageInfo.path}`);
1603
+ logger.error(` kodrdriv publish`);
1604
+ logger.error(' 2. After the individual publish completes, run:');
1605
+ logger.error(` kodrdriv tree ${builtInCommand} --continue`);
1606
+ } else {
1607
+ logger.error(' 1. Fix any issues in the package, then run:');
1608
+ logger.error(` cd ${packageInfo.path}`);
1609
+ logger.error(` ${commandToRun}`);
1610
+ logger.error(' 2. After the command completes successfully, run:');
1611
+ logger.error(` kodrdriv tree ${builtInCommand} --continue`);
1612
+ }
1613
+ logger.error(' 3. Or promote this package to completed status:');
1614
+ logger.error(` kodrdriv tree ${builtInCommand} --promote ${packageName}`);
1615
+ logger.error(' 4. Or manually edit .kodrdriv-context to mark this package as completed');
1616
+ }
1617
+ // Add clear error summary at the very end
1618
+ logger.error('');
1619
+ logger.error('📋 ERROR SUMMARY:');
1620
+ logger.error(` Project that failed: ${packageName}`);
1621
+ logger.error(` Location: ${packageInfo.path}`);
1622
+ logger.error(` Position in tree: ${i + 1} of ${buildOrder.length} packages`);
1623
+ logger.error(` What failed: ${((_result_error = result.error) === null || _result_error === void 0 ? void 0 : _result_error.message) || 'Unknown error'}`);
1624
+ logger.error('');
1387
1625
  throw new Error(`Command failed in package ${packageName}`);
1388
1626
  }
1389
1627
  break;
@@ -1393,7 +1631,7 @@ const execute = async (runConfig)=>{
1393
1631
  const summary = `${isDryRun ? 'DRY RUN: ' : ''}All ${buildOrder.length} packages completed successfully! 🎉`;
1394
1632
  logger.info(summary);
1395
1633
  // Clean up context on successful completion
1396
- if (isBuiltInCommand && builtInCommand === 'publish' && !isDryRun) {
1634
+ if (isBuiltInCommand && (builtInCommand === 'publish' || builtInCommand === 'run') && !isDryRun) {
1397
1635
  await cleanupContext(runConfig.outputDirectory);
1398
1636
  }
1399
1637
  return returnOutput; // Don't duplicate the summary in return string