@eldrforge/kodrdriv 1.2.4 → 1.2.6

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.
@@ -114,6 +114,11 @@ const updateInterProjectDependencies = async (packageDir, publishedVersions, all
114
114
  ];
115
115
  for (const publishedVersion of publishedVersions){
116
116
  const { packageName, version } = publishedVersion;
117
+ // Do not propagate prerelease versions to consumers (often not available on registry)
118
+ if (typeof version === 'string' && version.includes('-')) {
119
+ packageLogger.verbose(`Skipping prerelease version for ${packageName}: ${version}`);
120
+ continue;
121
+ }
117
122
  // Only update if this is an inter-project dependency (exists in our build tree)
118
123
  if (!allPackageNames.has(packageName)) {
119
124
  continue;
@@ -222,6 +227,84 @@ const cleanupContext = async (outputDirectory)=>{
222
227
  logger.warn(`Warning: Failed to cleanup execution context: ${error.message}`);
223
228
  }
224
229
  };
230
+ // Helper function to promote a package to completed status in the context
231
+ const promotePackageToCompleted = async (packageName, outputDirectory)=>{
232
+ const storage = create({
233
+ log: ()=>{}
234
+ });
235
+ const contextFilePath = getContextFilePath(outputDirectory);
236
+ try {
237
+ if (!await storage.exists(contextFilePath)) {
238
+ return;
239
+ }
240
+ const contextContent = await storage.readFile(contextFilePath, 'utf-8');
241
+ const contextData = safeJsonParse(contextContent, contextFilePath);
242
+ // Restore dates from ISO strings
243
+ const context = {
244
+ ...contextData,
245
+ startTime: new Date(contextData.startTime),
246
+ lastUpdateTime: new Date(contextData.lastUpdateTime),
247
+ publishedVersions: contextData.publishedVersions.map((v)=>({
248
+ ...v,
249
+ publishTime: new Date(v.publishTime)
250
+ }))
251
+ };
252
+ // Add package to completed list if not already there
253
+ if (!context.completedPackages.includes(packageName)) {
254
+ context.completedPackages.push(packageName);
255
+ context.lastUpdateTime = new Date();
256
+ await saveExecutionContext(context, outputDirectory);
257
+ }
258
+ } catch (error) {
259
+ const logger = getLogger();
260
+ logger.warn(`Warning: Failed to promote package to completed: ${error.message}`);
261
+ }
262
+ };
263
+ // Helper function to validate that all packages have the required scripts
264
+ const validateScripts = async (packages, scripts)=>{
265
+ const logger = getLogger();
266
+ const missingScripts = new Map();
267
+ const storage = create({
268
+ log: ()=>{}
269
+ });
270
+ logger.debug(`Validating scripts: ${scripts.join(', ')}`);
271
+ for (const [packageName, packageInfo] of packages){
272
+ const packageJsonPath = path__default.join(packageInfo.path, 'package.json');
273
+ const missingForPackage = [];
274
+ try {
275
+ const packageJsonContent = await storage.readFile(packageJsonPath, 'utf-8');
276
+ const packageJson = safeJsonParse(packageJsonContent, packageJsonPath);
277
+ const validated = validatePackageJson(packageJson, packageJsonPath);
278
+ // Check if each required script exists
279
+ for (const script of scripts){
280
+ if (!validated.scripts || !validated.scripts[script]) {
281
+ missingForPackage.push(script);
282
+ }
283
+ }
284
+ if (missingForPackage.length > 0) {
285
+ missingScripts.set(packageName, missingForPackage);
286
+ logger.debug(`Package ${packageName} missing scripts: ${missingForPackage.join(', ')}`);
287
+ }
288
+ } catch (error) {
289
+ logger.debug(`Error reading package.json for ${packageName}: ${error.message}`);
290
+ // If we can't read the package.json, assume all scripts are missing
291
+ missingScripts.set(packageName, scripts);
292
+ }
293
+ }
294
+ const valid = missingScripts.size === 0;
295
+ if (valid) {
296
+ logger.info(`✅ All packages have the required scripts: ${scripts.join(', ')}`);
297
+ } else {
298
+ logger.error(`❌ Script validation failed. Missing scripts:`);
299
+ for (const [packageName, missing] of missingScripts){
300
+ logger.error(` ${packageName}: ${missing.join(', ')}`);
301
+ }
302
+ }
303
+ return {
304
+ valid,
305
+ missingScripts
306
+ };
307
+ };
225
308
  // Extract published version from package.json after successful publish
226
309
  const extractPublishedVersion = async (packageDir, packageLogger)=>{
227
310
  const storage = create({
@@ -313,7 +396,7 @@ const createPackageLogger = (packageName, sequenceNumber, totalCount, isDryRun =
313
396
  };
314
397
  };
315
398
  // Helper function to format subproject error output
316
- const formatSubprojectError = (packageName, error)=>{
399
+ const formatSubprojectError = (packageName, error, _packageInfo, _position, _total)=>{
317
400
  const lines = [];
318
401
  lines.push(`❌ Command failed in package ${packageName}:`);
319
402
  // Format the main error message with indentation
@@ -592,6 +675,7 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
592
675
  }
593
676
  // For built-in commands, shell out to a separate kodrdriv process
594
677
  // This preserves individual project configurations
678
+ let publishWasSkipped;
595
679
  if (isBuiltInCommand) {
596
680
  // Extract the command name from "kodrdriv <command>"
597
681
  const builtInCommandName = commandToRun.replace('kodrdriv ', '');
@@ -601,28 +685,39 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
601
685
  // Ensure dry-run propagates to subprocess even during overall dry-run mode
602
686
  const effectiveCommand = runConfig.dryRun && !commandToRun.includes('--dry-run') ? `${commandToRun} --dry-run` : commandToRun;
603
687
  // Use runWithLogging for built-in commands to capture all output
604
- await runWithLogging(effectiveCommand, packageLogger, {}, showOutput);
688
+ const { stdout } = await runWithLogging(effectiveCommand, packageLogger, {}, showOutput);
689
+ // Detect explicit skip marker from publish to avoid propagating versions
690
+ if (builtInCommandName === 'publish' && stdout && stdout.includes('KODRDRIV_PUBLISH_SKIPPED')) {
691
+ packageLogger.info('Publish skipped for this package; will not record or propagate a version.');
692
+ publishWasSkipped = true;
693
+ }
605
694
  } else {
606
695
  // For custom commands, use the existing logic
607
696
  await runWithLogging(commandToRun, packageLogger, {}, showOutput);
608
697
  }
609
698
  // Track published version after successful publish (skip during dry run)
610
699
  if (!isDryRun && isBuiltInCommand && commandToRun.includes('publish')) {
611
- const publishedVersion = await extractPublishedVersion(packageDir, packageLogger);
612
- if (publishedVersion) {
613
- let mutexLocked = false;
614
- try {
615
- await globalStateMutex.lock();
616
- mutexLocked = true;
617
- publishedVersions.push(publishedVersion);
618
- packageLogger.info(`Tracked published version: ${publishedVersion.packageName}@${publishedVersion.version}`);
619
- globalStateMutex.unlock();
620
- mutexLocked = false;
621
- } catch (error) {
622
- if (mutexLocked) {
700
+ // If publish was skipped, do not record a version
701
+ if (publishWasSkipped) {
702
+ packageLogger.verbose('Skipping version tracking due to earlier skip.');
703
+ } else {
704
+ // Only record a published version if a new tag exists (avoid recording for skipped publishes)
705
+ const publishedVersion = await extractPublishedVersion(packageDir, packageLogger);
706
+ if (publishedVersion) {
707
+ let mutexLocked = false;
708
+ try {
709
+ await globalStateMutex.lock();
710
+ mutexLocked = true;
711
+ publishedVersions.push(publishedVersion);
712
+ packageLogger.info(`Tracked published version: ${publishedVersion.packageName}@${publishedVersion.version}`);
623
713
  globalStateMutex.unlock();
714
+ mutexLocked = false;
715
+ } catch (error) {
716
+ if (mutexLocked) {
717
+ globalStateMutex.unlock();
718
+ }
719
+ throw error;
624
720
  }
625
- throw error;
626
721
  }
627
722
  }
628
723
  }
@@ -654,22 +749,36 @@ const executePackage = async (packageName, packageInfo, commandToRun, runConfig,
654
749
  success: true
655
750
  };
656
751
  } catch (error) {
752
+ var _error_message;
657
753
  if (runConfig.debug || runConfig.verbose) {
658
754
  packageLogger.error(`❌ Execution failed: ${error.message}`);
659
755
  } else {
660
756
  logger.error(`[${index + 1}/${total}] ${packageName}: ❌ Failed - ${error.message}`);
661
757
  }
758
+ // Check if this is a timeout error
759
+ const errorMessage = ((_error_message = error.message) === null || _error_message === void 0 ? void 0 : _error_message.toLowerCase()) || '';
760
+ 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'));
662
761
  return {
663
762
  success: false,
664
- error
763
+ error,
764
+ isTimeoutError
665
765
  };
666
766
  }
667
767
  };
668
768
  const execute = async (runConfig)=>{
669
- var _runConfig_tree, _runConfig_tree1, _runConfig_tree2, _runConfig_tree3, _runConfig_tree4;
769
+ var _runConfig_tree, _runConfig_tree1, _runConfig_tree2, _runConfig_tree3, _runConfig_tree4, _runConfig_tree5;
670
770
  const logger = getLogger();
671
771
  const isDryRun = runConfig.dryRun || false;
672
772
  const isContinue = ((_runConfig_tree = runConfig.tree) === null || _runConfig_tree === void 0 ? void 0 : _runConfig_tree.continue) || false;
773
+ const promotePackage = (_runConfig_tree1 = runConfig.tree) === null || _runConfig_tree1 === void 0 ? void 0 : _runConfig_tree1.promote;
774
+ // Handle promote mode
775
+ if (promotePackage) {
776
+ logger.info(`Promoting package '${promotePackage}' to completed status...`);
777
+ await promotePackageToCompleted(promotePackage, runConfig.outputDirectory);
778
+ logger.info(`✅ Package '${promotePackage}' has been marked as completed.`);
779
+ logger.info('You can now run the tree command with --continue to resume from the next package.');
780
+ return `Package '${promotePackage}' promoted to completed status.`;
781
+ }
673
782
  // Handle continue mode
674
783
  if (isContinue) {
675
784
  const savedContext = await loadExecutionContext(runConfig.outputDirectory);
@@ -708,24 +817,50 @@ const execute = async (runConfig)=>{
708
817
  executionContext = null;
709
818
  }
710
819
  // Check if we're in built-in command mode (tree command with second argument)
711
- const builtInCommand = (_runConfig_tree1 = runConfig.tree) === null || _runConfig_tree1 === void 0 ? void 0 : _runConfig_tree1.builtInCommand;
820
+ const builtInCommand = (_runConfig_tree2 = runConfig.tree) === null || _runConfig_tree2 === void 0 ? void 0 : _runConfig_tree2.builtInCommand;
712
821
  const supportedBuiltInCommands = [
713
822
  'commit',
714
823
  'publish',
715
824
  'link',
716
825
  'unlink',
717
826
  'development',
718
- 'branches'
827
+ 'branches',
828
+ 'run'
719
829
  ];
720
830
  if (builtInCommand && !supportedBuiltInCommands.includes(builtInCommand)) {
721
831
  throw new Error(`Unsupported built-in command: ${builtInCommand}. Supported commands: ${supportedBuiltInCommands.join(', ')}`);
722
832
  }
833
+ // Handle run subcommand - convert space-separated scripts to npm run commands
834
+ if (builtInCommand === 'run') {
835
+ var _runConfig_tree6;
836
+ const packageArgument = (_runConfig_tree6 = runConfig.tree) === null || _runConfig_tree6 === void 0 ? void 0 : _runConfig_tree6.packageArgument;
837
+ if (!packageArgument) {
838
+ throw new Error('run subcommand requires script names. Usage: kodrdriv tree run "clean build test"');
839
+ }
840
+ // Split the package argument by spaces to get individual script names
841
+ const scripts = packageArgument.trim().split(/\s+/).filter((script)=>script.length > 0);
842
+ if (scripts.length === 0) {
843
+ throw new Error('run subcommand requires at least one script name. Usage: kodrdriv tree run "clean build test"');
844
+ }
845
+ // Convert to npm run commands joined with &&
846
+ const npmCommands = scripts.map((script)=>`npm run ${script}`).join(' && ');
847
+ // Set this as the custom command to run
848
+ runConfig.tree = {
849
+ ...runConfig.tree,
850
+ cmd: npmCommands
851
+ };
852
+ // Clear the built-in command since we're now using custom command mode
853
+ runConfig.tree.builtInCommand = undefined;
854
+ logger.info(`Converting run subcommand to: ${npmCommands}`);
855
+ // Store scripts for later validation
856
+ runConfig.__scriptsToValidate = scripts;
857
+ }
723
858
  // Determine the target directories - either specified or current working directory
724
- const directories = ((_runConfig_tree2 = runConfig.tree) === null || _runConfig_tree2 === void 0 ? void 0 : _runConfig_tree2.directories) || [
859
+ const directories = ((_runConfig_tree3 = runConfig.tree) === null || _runConfig_tree3 === void 0 ? void 0 : _runConfig_tree3.directories) || [
725
860
  process.cwd()
726
861
  ];
727
862
  // Handle link status subcommand
728
- if (builtInCommand === 'link' && ((_runConfig_tree3 = runConfig.tree) === null || _runConfig_tree3 === void 0 ? void 0 : _runConfig_tree3.packageArgument) === 'status') {
863
+ if (builtInCommand === 'link' && ((_runConfig_tree4 = runConfig.tree) === null || _runConfig_tree4 === void 0 ? void 0 : _runConfig_tree4.packageArgument) === 'status') {
729
864
  // For tree link status, we want to show status across all packages
730
865
  logger.info(`${isDryRun ? 'DRY RUN: ' : ''}Running link status across workspace...`);
731
866
  // Create a config that will be passed to the link command
@@ -745,7 +880,7 @@ const execute = async (runConfig)=>{
745
880
  }
746
881
  }
747
882
  // Handle unlink status subcommand
748
- if (builtInCommand === 'unlink' && ((_runConfig_tree4 = runConfig.tree) === null || _runConfig_tree4 === void 0 ? void 0 : _runConfig_tree4.packageArgument) === 'status') {
883
+ if (builtInCommand === 'unlink' && ((_runConfig_tree5 = runConfig.tree) === null || _runConfig_tree5 === void 0 ? void 0 : _runConfig_tree5.packageArgument) === 'status') {
749
884
  // For tree unlink status, we want to show status across all packages
750
885
  logger.info(`${isDryRun ? 'DRY RUN: ' : ''}Running unlink status across workspace...`);
751
886
  // Create a config that will be passed to the unlink command
@@ -770,9 +905,9 @@ const execute = async (runConfig)=>{
770
905
  logger.info(`${isDryRun ? 'DRY RUN: ' : ''}Analyzing workspaces at: ${directories.join(', ')}`);
771
906
  }
772
907
  try {
773
- var _runConfig_tree5, _runConfig_tree6, _runConfig_tree7, _runConfig_tree8;
908
+ var _runConfig_tree7, _runConfig_tree8, _runConfig_tree9, _runConfig_tree10;
774
909
  // Get exclusion patterns from config, fallback to empty array
775
- const excludedPatterns = ((_runConfig_tree5 = runConfig.tree) === null || _runConfig_tree5 === void 0 ? void 0 : _runConfig_tree5.exclude) || [];
910
+ const excludedPatterns = ((_runConfig_tree7 = runConfig.tree) === null || _runConfig_tree7 === void 0 ? void 0 : _runConfig_tree7.exclude) || [];
776
911
  if (excludedPatterns.length > 0) {
777
912
  logger.verbose(`${isDryRun ? 'DRY RUN: ' : ''}Using exclusion patterns: ${excludedPatterns.join(', ')}`);
778
913
  }
@@ -799,16 +934,19 @@ const execute = async (runConfig)=>{
799
934
  logger.verbose(`${isDryRun ? 'DRY RUN: ' : ''}Determining build order...`);
800
935
  let buildOrder = topologicalSort(dependencyGraph);
801
936
  // Handle start-from functionality if specified
802
- const startFrom = (_runConfig_tree6 = runConfig.tree) === null || _runConfig_tree6 === void 0 ? void 0 : _runConfig_tree6.startFrom;
937
+ const startFrom = (_runConfig_tree8 = runConfig.tree) === null || _runConfig_tree8 === void 0 ? void 0 : _runConfig_tree8.startFrom;
803
938
  if (startFrom) {
804
939
  logger.verbose(`${isDryRun ? 'DRY RUN: ' : ''}Looking for start package: ${startFrom}`);
805
- // Find the package that matches the startFrom directory name
806
- const startIndex = buildOrder.findIndex((packageName)=>{
807
- const packageInfo = dependencyGraph.packages.get(packageName);
808
- const dirName = path__default.basename(packageInfo.path);
809
- return dirName === startFrom || packageName === startFrom;
810
- });
811
- if (startIndex === -1) {
940
+ // Resolve the actual package name (can be package name or directory name)
941
+ let startPackageName = null;
942
+ for (const [pkgName, pkgInfo] of dependencyGraph.packages){
943
+ const dirName = path__default.basename(pkgInfo.path);
944
+ if (dirName === startFrom || pkgName === startFrom) {
945
+ startPackageName = pkgName;
946
+ break;
947
+ }
948
+ }
949
+ if (!startPackageName) {
812
950
  // Check if the package exists but was excluded across all directories
813
951
  let allPackageJsonPathsForCheck = [];
814
952
  for (const targetDirectory of directories){
@@ -842,14 +980,18 @@ const execute = async (runConfig)=>{
842
980
  throw new Error(`Package directory '${startFrom}' not found. Available packages: ${availablePackages}`);
843
981
  }
844
982
  }
845
- const skippedCount = startIndex;
846
- buildOrder = buildOrder.slice(startIndex);
847
- if (skippedCount > 0) {
848
- logger.info(`${isDryRun ? 'DRY RUN: ' : ''}Resuming from '${startFrom}' - skipping ${skippedCount} package${skippedCount === 1 ? '' : 's'}`);
983
+ // Find the start package in the build order and start execution from there
984
+ const startIndex = buildOrder.findIndex((pkgName)=>pkgName === startPackageName);
985
+ if (startIndex === -1) {
986
+ throw new Error(`Package '${startFrom}' not found in build order. This should not happen.`);
849
987
  }
988
+ // Filter build order to start from the specified package
989
+ const originalLength = buildOrder.length;
990
+ buildOrder = buildOrder.slice(startIndex);
991
+ logger.info(`${isDryRun ? 'DRY RUN: ' : ''}Starting execution from package '${startFrom}' (${buildOrder.length} of ${originalLength} packages remaining).`);
850
992
  }
851
993
  // Handle stop-at functionality if specified
852
- const stopAt = (_runConfig_tree7 = runConfig.tree) === null || _runConfig_tree7 === void 0 ? void 0 : _runConfig_tree7.stopAt;
994
+ const stopAt = (_runConfig_tree9 = runConfig.tree) === null || _runConfig_tree9 === void 0 ? void 0 : _runConfig_tree9.stopAt;
853
995
  if (stopAt) {
854
996
  logger.verbose(`${isDryRun ? 'DRY RUN: ' : ''}Looking for stop package: ${stopAt}`);
855
997
  // Find the package that matches the stopAt directory name
@@ -1208,12 +1350,12 @@ const execute = async (runConfig)=>{
1208
1350
  returnOutput = `\nBuild order: ${buildOrder.join(' → ')}\n`;
1209
1351
  }
1210
1352
  // Execute command if provided (custom command or built-in command)
1211
- const cmd = (_runConfig_tree8 = runConfig.tree) === null || _runConfig_tree8 === void 0 ? void 0 : _runConfig_tree8.cmd;
1353
+ const cmd = (_runConfig_tree10 = runConfig.tree) === null || _runConfig_tree10 === void 0 ? void 0 : _runConfig_tree10.cmd;
1212
1354
  // Determine command to execute
1213
1355
  let commandToRun;
1214
1356
  let isBuiltInCommand = false;
1215
1357
  if (builtInCommand) {
1216
- var _runConfig_tree9, _runConfig_tree10, _runConfig_tree11;
1358
+ var _runConfig_tree11, _runConfig_tree12, _runConfig_tree13;
1217
1359
  // Built-in command mode: shell out to kodrdriv subprocess
1218
1360
  // Build command with propagated global options
1219
1361
  const globalOptions = [];
@@ -1230,14 +1372,14 @@ const execute = async (runConfig)=>{
1230
1372
  // Build the command with global options
1231
1373
  const optionsString = globalOptions.length > 0 ? ` ${globalOptions.join(' ')}` : '';
1232
1374
  // Add package argument for link/unlink commands
1233
- const packageArg = (_runConfig_tree9 = runConfig.tree) === null || _runConfig_tree9 === void 0 ? void 0 : _runConfig_tree9.packageArgument;
1375
+ const packageArg = (_runConfig_tree11 = runConfig.tree) === null || _runConfig_tree11 === void 0 ? void 0 : _runConfig_tree11.packageArgument;
1234
1376
  const packageArgString = packageArg && (builtInCommand === 'link' || builtInCommand === 'unlink') ? ` "${packageArg}"` : '';
1235
1377
  // Add command-specific options
1236
1378
  let commandSpecificOptions = '';
1237
- if (builtInCommand === 'unlink' && ((_runConfig_tree10 = runConfig.tree) === null || _runConfig_tree10 === void 0 ? void 0 : _runConfig_tree10.cleanNodeModules)) {
1379
+ if (builtInCommand === 'unlink' && ((_runConfig_tree12 = runConfig.tree) === null || _runConfig_tree12 === void 0 ? void 0 : _runConfig_tree12.cleanNodeModules)) {
1238
1380
  commandSpecificOptions += ' --clean-node-modules';
1239
1381
  }
1240
- if ((builtInCommand === 'link' || builtInCommand === 'unlink') && ((_runConfig_tree11 = runConfig.tree) === null || _runConfig_tree11 === void 0 ? void 0 : _runConfig_tree11.externals) && runConfig.tree.externals.length > 0) {
1382
+ if ((builtInCommand === 'link' || builtInCommand === 'unlink') && ((_runConfig_tree13 = runConfig.tree) === null || _runConfig_tree13 === void 0 ? void 0 : _runConfig_tree13.externals) && runConfig.tree.externals.length > 0) {
1241
1383
  commandSpecificOptions += ` --externals ${runConfig.tree.externals.join(' ')}`;
1242
1384
  }
1243
1385
  commandToRun = `kodrdriv ${builtInCommand}${optionsString}${packageArgString}${commandSpecificOptions}`;
@@ -1247,6 +1389,23 @@ const execute = async (runConfig)=>{
1247
1389
  commandToRun = cmd;
1248
1390
  }
1249
1391
  if (commandToRun) {
1392
+ // Validate scripts for run command before execution
1393
+ const scriptsToValidate = runConfig.__scriptsToValidate;
1394
+ if (scriptsToValidate && scriptsToValidate.length > 0) {
1395
+ logger.info(`🔍 Validating scripts before execution: ${scriptsToValidate.join(', ')}`);
1396
+ const validation = await validateScripts(dependencyGraph.packages, scriptsToValidate);
1397
+ if (!validation.valid) {
1398
+ logger.error('');
1399
+ logger.error('❌ Script validation failed. Cannot proceed with execution.');
1400
+ logger.error('');
1401
+ logger.error('💡 To fix this:');
1402
+ logger.error(' 1. Add the missing scripts to the package.json files');
1403
+ logger.error(' 2. Or exclude packages that don\'t need these scripts using --exclude');
1404
+ logger.error(' 3. Or run individual packages that have the required scripts');
1405
+ logger.error('');
1406
+ throw new Error('Script validation failed. See details above.');
1407
+ }
1408
+ }
1250
1409
  // Create set of all package names for inter-project dependency detection
1251
1410
  const allPackageNames = new Set(Array.from(dependencyGraph.packages.keys()));
1252
1411
  // Initialize execution context if not continuing
@@ -1260,8 +1419,8 @@ const execute = async (runConfig)=>{
1260
1419
  startTime: new Date(),
1261
1420
  lastUpdateTime: new Date()
1262
1421
  };
1263
- // Save initial context
1264
- if (isBuiltInCommand && builtInCommand === 'publish' && !isDryRun) {
1422
+ // Save initial context for commands that support continuation
1423
+ if (isBuiltInCommand && (builtInCommand === 'publish' || builtInCommand === 'run') && !isDryRun) {
1265
1424
  await saveExecutionContext(executionContext, runConfig.outputDirectory);
1266
1425
  }
1267
1426
  }
@@ -1291,7 +1450,7 @@ const execute = async (runConfig)=>{
1291
1450
  if (result.success) {
1292
1451
  successCount++;
1293
1452
  // Update context
1294
- if (executionContext && isBuiltInCommand && builtInCommand === 'publish' && !isDryRun) {
1453
+ if (executionContext && isBuiltInCommand && (builtInCommand === 'publish' || builtInCommand === 'run') && !isDryRun) {
1295
1454
  executionContext.completedPackages.push(packageName);
1296
1455
  executionContext.publishedVersions = publishedVersions;
1297
1456
  executionContext.lastUpdateTime = new Date();
@@ -1304,17 +1463,76 @@ const execute = async (runConfig)=>{
1304
1463
  }
1305
1464
  } else {
1306
1465
  failedPackage = packageName;
1307
- const formattedError = formatSubprojectError(packageName, result.error);
1466
+ const formattedError = formatSubprojectError(packageName, result.error, packageInfo, i + 1, buildOrder.length);
1308
1467
  if (!isDryRun) {
1468
+ var _result_error;
1309
1469
  packageLogger.error(`Execution failed`);
1310
1470
  logger.error(formattedError);
1311
1471
  logger.error(`Failed after ${successCount} successful packages.`);
1472
+ // Special handling for timeout errors
1473
+ if (result.isTimeoutError) {
1474
+ logger.error('');
1475
+ logger.error('⏰ TIMEOUT DETECTED: This appears to be a timeout error.');
1476
+ logger.error(' This commonly happens when PR checks take longer than expected.');
1477
+ logger.error(' The execution context has been saved for recovery.');
1478
+ logger.error('');
1479
+ // Save context even on timeout for recovery
1480
+ if (executionContext && isBuiltInCommand && (builtInCommand === 'publish' || builtInCommand === 'run')) {
1481
+ executionContext.completedPackages.push(packageName);
1482
+ executionContext.publishedVersions = publishedVersions;
1483
+ executionContext.lastUpdateTime = new Date();
1484
+ await saveExecutionContext(executionContext, runConfig.outputDirectory);
1485
+ logger.info('💾 Execution context saved for recovery.');
1486
+ }
1487
+ // For publish commands, provide specific guidance about CI/CD setup
1488
+ if (builtInCommand === 'publish') {
1489
+ logger.error('');
1490
+ logger.error('💡 PUBLISH TIMEOUT TROUBLESHOOTING:');
1491
+ logger.error(' This project may not have CI/CD workflows configured.');
1492
+ logger.error(' Common solutions:');
1493
+ logger.error(' 1. Set up GitHub Actions workflows for this repository');
1494
+ logger.error(' 2. Use --sendit flag to skip user confirmation:');
1495
+ logger.error(` kodrdriv tree publish --sendit`);
1496
+ logger.error(' 3. Or manually promote this package:');
1497
+ logger.error(` kodrdriv tree publish --promote ${packageName}`);
1498
+ logger.error('');
1499
+ }
1500
+ }
1312
1501
  logger.error(`To resume from this point, run:`);
1313
1502
  if (isBuiltInCommand) {
1314
1503
  logger.error(` kodrdriv tree ${builtInCommand} --continue`);
1315
1504
  } else {
1316
1505
  logger.error(` kodrdriv tree --continue --cmd "${commandToRun}"`);
1317
1506
  }
1507
+ // For timeout errors, provide additional recovery instructions
1508
+ if (result.isTimeoutError) {
1509
+ logger.error('');
1510
+ logger.error('🔧 RECOVERY OPTIONS:');
1511
+ if (builtInCommand === 'publish') {
1512
+ logger.error(' 1. Wait for the PR checks to complete, then run:');
1513
+ logger.error(` cd ${packageInfo.path}`);
1514
+ logger.error(` kodrdriv publish`);
1515
+ logger.error(' 2. After the individual publish completes, run:');
1516
+ logger.error(` kodrdriv tree ${builtInCommand} --continue`);
1517
+ } else {
1518
+ logger.error(' 1. Fix any issues in the package, then run:');
1519
+ logger.error(` cd ${packageInfo.path}`);
1520
+ logger.error(` ${commandToRun}`);
1521
+ logger.error(' 2. After the command completes successfully, run:');
1522
+ logger.error(` kodrdriv tree ${builtInCommand} --continue`);
1523
+ }
1524
+ logger.error(' 3. Or promote this package to completed status:');
1525
+ logger.error(` kodrdriv tree ${builtInCommand} --promote ${packageName}`);
1526
+ logger.error(' 4. Or manually edit .kodrdriv-context to mark this package as completed');
1527
+ }
1528
+ // Add clear error summary at the very end
1529
+ logger.error('');
1530
+ logger.error('📋 ERROR SUMMARY:');
1531
+ logger.error(` Project that failed: ${packageName}`);
1532
+ logger.error(` Location: ${packageInfo.path}`);
1533
+ logger.error(` Position in tree: ${i + 1} of ${buildOrder.length} packages`);
1534
+ logger.error(` What failed: ${((_result_error = result.error) === null || _result_error === void 0 ? void 0 : _result_error.message) || 'Unknown error'}`);
1535
+ logger.error('');
1318
1536
  throw new Error(`Command failed in package ${packageName}`);
1319
1537
  }
1320
1538
  break;
@@ -1324,7 +1542,7 @@ const execute = async (runConfig)=>{
1324
1542
  const summary = `${isDryRun ? 'DRY RUN: ' : ''}All ${buildOrder.length} packages completed successfully! 🎉`;
1325
1543
  logger.info(summary);
1326
1544
  // Clean up context on successful completion
1327
- if (isBuiltInCommand && builtInCommand === 'publish' && !isDryRun) {
1545
+ if (isBuiltInCommand && (builtInCommand === 'publish' || builtInCommand === 'run') && !isDryRun) {
1328
1546
  await cleanupContext(runConfig.outputDirectory);
1329
1547
  }
1330
1548
  return returnOutput; // Don't duplicate the summary in return string