@lumpcode/core 0.0.15 → 0.1.0

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 (52) hide show
  1. package/README.md +7 -1
  2. package/dist/helpers/execBinary/main.d.ts +19 -5
  3. package/dist/helpers/execBinary/main.d.ts.map +1 -1
  4. package/dist/helpers/executeStepsForContextList/main.d.ts +10 -3
  5. package/dist/helpers/executeStepsForContextList/main.d.ts.map +1 -1
  6. package/dist/helpers/getContextStatus/main.d.ts +5 -0
  7. package/dist/helpers/getContextStatus/main.d.ts.map +1 -1
  8. package/dist/helpers/getToDoContextList/main.d.ts +6 -0
  9. package/dist/helpers/getToDoContextList/main.d.ts.map +1 -1
  10. package/dist/helpers/index.d.ts +1 -0
  11. package/dist/helpers/index.d.ts.map +1 -1
  12. package/dist/helpers/refreshRemoteTrackingRefs/index.d.ts +2 -0
  13. package/dist/helpers/refreshRemoteTrackingRefs/index.d.ts.map +1 -0
  14. package/dist/helpers/refreshRemoteTrackingRefs/main.d.ts +11 -0
  15. package/dist/helpers/refreshRemoteTrackingRefs/main.d.ts.map +1 -0
  16. package/dist/index.cjs +469 -172
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.js +467 -174
  19. package/dist/index.js.map +1 -1
  20. package/dist/testing/processTreeChild.d.cts +2 -0
  21. package/dist/testing/processTreeChild.d.cts.map +1 -0
  22. package/dist/testing/processTreeTestHelpers.d.ts +12 -0
  23. package/dist/testing/processTreeTestHelpers.d.ts.map +1 -0
  24. package/dist/testing/sigtermIgnorantTreeChild.d.cts +2 -0
  25. package/dist/testing/sigtermIgnorantTreeChild.d.cts.map +1 -0
  26. package/dist/types/ExecShellFn.d.ts +16 -0
  27. package/dist/types/ExecShellFn.d.ts.map +1 -0
  28. package/dist/types/ExecuteStepsFailureData.d.ts +6 -0
  29. package/dist/types/ExecuteStepsFailureData.d.ts.map +1 -0
  30. package/dist/types/ExecuteStepsFailureReason.d.ts +2 -0
  31. package/dist/types/ExecuteStepsFailureReason.d.ts.map +1 -0
  32. package/dist/types/GitAddCommandFn.d.ts +7 -1
  33. package/dist/types/GitAddCommandFn.d.ts.map +1 -1
  34. package/dist/types/GitCommitCommandFn.d.ts +7 -1
  35. package/dist/types/GitCommitCommandFn.d.ts.map +1 -1
  36. package/dist/types/GitPushCommandFn.d.ts +7 -1
  37. package/dist/types/GitPushCommandFn.d.ts.map +1 -1
  38. package/dist/types/index.d.ts +2 -0
  39. package/dist/types/index.d.ts.map +1 -1
  40. package/dist/usages/runLump/main.d.ts +10 -5
  41. package/dist/usages/runLump/main.d.ts.map +1 -1
  42. package/dist/utils/index.d.ts +3 -0
  43. package/dist/utils/index.d.ts.map +1 -1
  44. package/dist/utils/isProcessAlive/index.d.ts +2 -1
  45. package/dist/utils/isProcessAlive/index.d.ts.map +1 -1
  46. package/dist/utils/killProcessTree/index.d.ts +2 -0
  47. package/dist/utils/killProcessTree/index.d.ts.map +1 -0
  48. package/dist/utils/killProcessTree/main.d.ts +11 -0
  49. package/dist/utils/killProcessTree/main.d.ts.map +1 -0
  50. package/dist/utils/nodeErrnoCode/index.d.ts +1 -1
  51. package/dist/utils/nodeErrnoCode/index.d.ts.map +1 -1
  52. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -3424,6 +3424,152 @@ function parseGitLogHashSubjectLines(stdout) {
3424
3424
  });
3425
3425
  }
3426
3426
 
3427
+ /** Node-style `code` from a caught unknown error (e.g. `ENOENT`), or undefined. */
3428
+ function nodeErrnoCode(error) {
3429
+ if (error && typeof error === 'object' && 'code' in error) {
3430
+ const code = error.code;
3431
+ return typeof code === 'string' ? code : undefined;
3432
+ }
3433
+ return undefined;
3434
+ }
3435
+
3436
+ /** Returns whether a process id is still running (signal 0 probe). */
3437
+ function isProcessAlive(pid, options) {
3438
+ const onProbeError = options?.onProbeError ?? 'throw';
3439
+ try {
3440
+ process.kill(pid, 0);
3441
+ return true;
3442
+ }
3443
+ catch (error) {
3444
+ if (nodeErrnoCode(error) === 'ESRCH')
3445
+ return false;
3446
+ if (onProbeError === 'throw')
3447
+ throw error;
3448
+ return onProbeError === 'alive';
3449
+ }
3450
+ }
3451
+
3452
+ const execFileAsync = node_util.promisify(node_child_process.execFile);
3453
+ function sleep(ms) {
3454
+ return new Promise((resolve) => setTimeout(resolve, ms));
3455
+ }
3456
+ async function listUnixProcessTreePids(rootPid) {
3457
+ const { stdout } = await execFileAsync('ps', ['-eo', 'pid=,ppid=']);
3458
+ const childrenByParent = new Map();
3459
+ for (const line of stdout.split('\n')) {
3460
+ const trimmed = line.trim();
3461
+ if (!trimmed)
3462
+ continue;
3463
+ const parts = trimmed.split(/\s+/);
3464
+ if (parts.length < 2)
3465
+ continue;
3466
+ const pid = Number.parseInt(parts[0] ?? '', 10);
3467
+ const ppid = Number.parseInt(parts[1] ?? '', 10);
3468
+ if (Number.isNaN(pid) || Number.isNaN(ppid))
3469
+ continue;
3470
+ const siblings = childrenByParent.get(ppid) ?? [];
3471
+ siblings.push(pid);
3472
+ childrenByParent.set(ppid, siblings);
3473
+ }
3474
+ const ordered = [];
3475
+ const queue = [rootPid];
3476
+ while (queue.length > 0) {
3477
+ const pid = queue.shift();
3478
+ if (pid === undefined)
3479
+ break;
3480
+ ordered.push(pid);
3481
+ const children = childrenByParent.get(pid) ?? [];
3482
+ for (const childPid of children) {
3483
+ queue.push(childPid);
3484
+ }
3485
+ }
3486
+ return ordered;
3487
+ }
3488
+ function killPid(pid, signal) {
3489
+ try {
3490
+ process.kill(pid, signal);
3491
+ }
3492
+ catch (error) {
3493
+ const code = nodeErrnoCode(error);
3494
+ if (code !== 'ESRCH') {
3495
+ throw error;
3496
+ }
3497
+ }
3498
+ }
3499
+ async function killUnixProcessTreeImmediate(rootPid) {
3500
+ const pids = await listUnixProcessTreePids(rootPid);
3501
+ for (let index = pids.length - 1; index >= 0; index -= 1) {
3502
+ killPid(pids[index], 'SIGKILL');
3503
+ }
3504
+ }
3505
+ async function killUnixProcessTreeWithGrace(rootPid, graceMs) {
3506
+ const pids = await listUnixProcessTreePids(rootPid);
3507
+ for (let index = pids.length - 1; index >= 0; index -= 1) {
3508
+ killPid(pids[index], 'SIGTERM');
3509
+ }
3510
+ const deadline = Date.now() + graceMs;
3511
+ while (Date.now() < deadline) {
3512
+ const anyAlive = pids.some((pid) => isProcessAlive(pid, { onProbeError: 'dead' }));
3513
+ if (!anyAlive) {
3514
+ return;
3515
+ }
3516
+ await sleep(25);
3517
+ }
3518
+ const remaining = await listUnixProcessTreePids(rootPid);
3519
+ for (let index = remaining.length - 1; index >= 0; index -= 1) {
3520
+ killPid(remaining[index], 'SIGKILL');
3521
+ }
3522
+ }
3523
+ async function killWindowsProcessTree(rootPid) {
3524
+ try {
3525
+ await execFileAsync('taskkill', ['/PID', String(rootPid), '/T', '/F'], {
3526
+ windowsHide: true,
3527
+ });
3528
+ }
3529
+ catch (error) {
3530
+ const stderr = error && typeof error === 'object' && 'stderr' in error
3531
+ ? String(error.stderr ?? '')
3532
+ : '';
3533
+ const message = error instanceof Error ? error.message : String(error);
3534
+ const combined = `${message}\n${stderr}`;
3535
+ if (/not found|no running instance|ne existe pas|introuvable/i.test(combined)) {
3536
+ return;
3537
+ }
3538
+ // taskkill can fail to terminate some descendants while the root exits (SEA/agent trees).
3539
+ if (!isProcessAlive(rootPid)) {
3540
+ return;
3541
+ }
3542
+ throw error;
3543
+ }
3544
+ }
3545
+ /**
3546
+ * Kill a process and its descendants.
3547
+ * `graceMs` default 0 → immediate SIGKILL / taskkill /T /F.
3548
+ * When `graceMs > 0` (Unix), SIGTERM first, then SIGKILL after the grace window.
3549
+ */
3550
+ async function killProcessTree(input) {
3551
+ const { pid, graceMs = 0 } = input;
3552
+ if (!Number.isInteger(pid) || pid <= 0) {
3553
+ return failure(`Invalid pid: ${pid}`);
3554
+ }
3555
+ try {
3556
+ if (process.platform === 'win32') {
3557
+ await killWindowsProcessTree(pid);
3558
+ }
3559
+ else if (graceMs > 0) {
3560
+ await killUnixProcessTreeWithGrace(pid, graceMs);
3561
+ }
3562
+ else {
3563
+ await killUnixProcessTreeImmediate(pid);
3564
+ }
3565
+ return success(undefined);
3566
+ }
3567
+ catch (error) {
3568
+ const message = error instanceof Error ? error.message : String(error);
3569
+ return failure(`Could not kill process tree for pid ${pid}: ${message}`);
3570
+ }
3571
+ }
3572
+
3427
3573
  const execAsyncBase = node_util.promisify(node_child_process.exec);
3428
3574
  async function execAsync(command, options) {
3429
3575
  const { stdout, stderr, hasErrored } = await execAsyncBase(command, { cwd: options?.cwd })
@@ -3453,38 +3599,86 @@ async function execAsync(command, options) {
3453
3599
  });
3454
3600
  }
3455
3601
 
3456
- function execBinary(binaryPath, args, timeoutMillis = 1000 * 60 * 10, options) {
3602
+ function execBinary(input) {
3603
+ const { binaryPath, args, timeoutMillis = 1000 * 60 * 10, cwd, env, stdio, signal, killGraceMs = 5000, } = input;
3457
3604
  return new Promise((resolve) => {
3458
3605
  let settled = false;
3606
+ let canceling = false;
3607
+ let child;
3608
+ let stdout = '';
3609
+ let stderr = '';
3610
+ let timeout;
3459
3611
  const finish = (result) => {
3460
3612
  if (settled)
3461
3613
  return;
3462
3614
  settled = true;
3463
- clearTimeout(timeout);
3615
+ if (timeout !== undefined)
3616
+ clearTimeout(timeout);
3617
+ if (signal) {
3618
+ signal.removeEventListener('abort', onAbort);
3619
+ }
3464
3620
  resolve(result);
3465
3621
  };
3466
- const timeout = setTimeout(() => {
3622
+ const cancelWith = async (reason, message) => {
3623
+ if (settled || canceling)
3624
+ return;
3625
+ canceling = true;
3626
+ const pid = child?.pid;
3627
+ if (pid != null) {
3628
+ await killProcessTree({ pid, graceMs: killGraceMs });
3629
+ }
3467
3630
  finish(failure({
3468
- message: `Process timed out after ${timeoutMillis} milliseconds`,
3631
+ message,
3469
3632
  binaryPath,
3470
3633
  args,
3634
+ stdout,
3635
+ stderr,
3636
+ reason,
3471
3637
  }));
3472
- }, timeoutMillis);
3638
+ };
3639
+ const onAbort = () => {
3640
+ void cancelWith('aborted', 'Process aborted');
3641
+ };
3642
+ if (signal?.aborted) {
3643
+ void cancelWith('aborted', 'Process aborted');
3644
+ return;
3645
+ }
3646
+ if (signal) {
3647
+ signal.addEventListener('abort', onAbort, { once: true });
3648
+ }
3473
3649
  const { executable: resolvedExecutable, args: resolvedArgs } = resolveSpawnExecutable(binaryPath, args);
3474
- const child = node_child_process.spawn(resolvedExecutable, resolvedArgs, options || {});
3475
- let stdout = '', stderr = '';
3650
+ const spawnOptions = {
3651
+ ...(cwd !== undefined ? { cwd } : {}),
3652
+ ...(env !== undefined ? { env } : {}),
3653
+ ...(stdio !== undefined ? { stdio } : {}),
3654
+ };
3655
+ child = node_child_process.spawn(resolvedExecutable, resolvedArgs, spawnOptions);
3656
+ // Start the timeout only after the OS has spawned the process so
3657
+ // timeoutMillis measures command runtime, not spawn/setup overhead.
3658
+ child.on('spawn', () => {
3659
+ if (settled || canceling)
3660
+ return;
3661
+ timeout = setTimeout(() => {
3662
+ void cancelWith('timeout', `Process timed out after ${timeoutMillis} milliseconds`);
3663
+ }, timeoutMillis);
3664
+ });
3476
3665
  child.stdout?.on('data', (data) => { stdout += data; });
3477
3666
  child.stderr?.on('data', (data) => { stderr += data; });
3478
3667
  child.on('error', (err) => {
3668
+ if (settled || canceling)
3669
+ return;
3479
3670
  finish(failure({
3480
3671
  message: err.message,
3481
3672
  binaryPath,
3482
3673
  args,
3483
3674
  stdout,
3484
3675
  stderr,
3676
+ reason: 'spawn',
3485
3677
  }));
3486
3678
  });
3487
3679
  child.on('close', (code) => {
3680
+ if (settled || canceling)
3681
+ return;
3488
3682
  if (code === 0) {
3489
3683
  finish(success({ stdout, stderr }));
3490
3684
  return;
@@ -3496,6 +3690,7 @@ function execBinary(binaryPath, args, timeoutMillis = 1000 * 60 * 10, options) {
3496
3690
  code: code ?? undefined,
3497
3691
  stdout,
3498
3692
  stderr,
3693
+ reason: 'exit',
3499
3694
  }));
3500
3695
  });
3501
3696
  });
@@ -3571,7 +3766,28 @@ async function collectStepsForContext(params) {
3571
3766
  return collectedSteps;
3572
3767
  }
3573
3768
 
3574
- async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables, contextList, gitAddCommandFn, gitCommitCommandFn, gitPushCommandFn, gitCommitMessageFn, projectRoot, steps, setupFn, setupWorkspaceFn, teardownFn, teardownWorkspaceFn, getKeepHistoryFilePathFn, logger: loggerInput, }) {
3769
+ async function runOptionalGitCommand(input) {
3770
+ const { label, getCommand, cwd, logger } = input;
3771
+ try {
3772
+ const command = await getCommand();
3773
+ if (command == null || command === '') {
3774
+ return 'ok';
3775
+ }
3776
+ const result = await execAsync(command, { cwd });
3777
+ logger.verbose(`${label} ${JSON.stringify(result)}`);
3778
+ if (!result.success) {
3779
+ logger.error(formatExecFailureMessage({ label, failure: result }));
3780
+ return 'failed';
3781
+ }
3782
+ return 'ok';
3783
+ }
3784
+ catch (error) {
3785
+ const message = error instanceof Error ? error.message : String(error);
3786
+ logger.error(`Failed to run ${label}: ${message}`);
3787
+ return 'failed';
3788
+ }
3789
+ }
3790
+ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables, contextList, gitAddCommandFn, gitCommitCommandFn, gitPushCommandFn, gitCommitMessageFn, projectRoot, steps, setupFn, setupWorkspaceFn, teardownFn, teardownWorkspaceFn, getKeepHistoryFilePathFn, logger: loggerInput, signal, }) {
3575
3791
  const logger = loggerInput ?? createConsoleLogger({});
3576
3792
  const contextNames = contextList.map(context => context.name);
3577
3793
  logger.verbose(`contextNames ${JSON.stringify(contextNames)}`);
@@ -3606,180 +3822,232 @@ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables,
3606
3822
  const gitStatusCommand = await execAsync(`git status`, { cwd: workspacePath });
3607
3823
  logger.verbose(`gitStatusCommand ${JSON.stringify(gitStatusCommand.data)}`);
3608
3824
  injectedGitAndWorkspaceFnsInput.workspacePath = workspacePath;
3609
- for (let i = 0; i < contextList.length; i++) {
3610
- const context = contextList[i];
3611
- logger.info(contextList.length > 1
3612
- ? `Running context "${context.name}" (${i + 1}/${contextList.length})`
3613
- : `Running context "${context.name}"`);
3614
- const setupResult = await setupFn({
3615
- contextList,
3616
- lumpVariables,
3617
- currentContextIndex: i,
3618
- });
3619
- const contextRunState = setupResult?.contextRunState || {};
3620
- let stepWalkFailure;
3621
- async function walkAndExecuteSteps(stepsToExec, currStepIndex) {
3622
- for (let stepIndex = 0; stepIndex < stepsToExec.length; stepIndex++) {
3623
- if (stepWalkFailure) {
3624
- return;
3625
- }
3626
- const step = stepsToExec[stepIndex];
3627
- const nextCallHeadIndex = [...currStepIndex, stepIndex];
3628
- const compositeStepIndex = nextCallHeadIndex.length === 1 ? nextCallHeadIndex[0] : nextCallHeadIndex;
3629
- if (typeof step === 'function' || Array.isArray(step)) {
3630
- let subSteps = [];
3631
- if (typeof step === 'function') {
3632
- subSteps = await step({
3825
+ let runFailure;
3826
+ try {
3827
+ for (let i = 0; i < contextList.length; i++) {
3828
+ const context = contextList[i];
3829
+ logger.info(contextList.length > 1
3830
+ ? `Running context "${context.name}" (${i + 1}/${contextList.length})`
3831
+ : `Running context "${context.name}"`);
3832
+ const setupResult = await setupFn({
3833
+ contextList,
3834
+ lumpVariables,
3835
+ currentContextIndex: i,
3836
+ });
3837
+ const contextRunState = setupResult?.contextRunState || {};
3838
+ let stepWalkFailure;
3839
+ async function walkAndExecuteSteps(stepsToExec, currStepIndex) {
3840
+ for (let stepIndex = 0; stepIndex < stepsToExec.length; stepIndex++) {
3841
+ if (stepWalkFailure) {
3842
+ return;
3843
+ }
3844
+ const step = stepsToExec[stepIndex];
3845
+ const nextCallHeadIndex = [...currStepIndex, stepIndex];
3846
+ const compositeStepIndex = nextCallHeadIndex.length === 1 ? nextCallHeadIndex[0] : nextCallHeadIndex;
3847
+ if (typeof step === 'function' || Array.isArray(step)) {
3848
+ let subSteps = [];
3849
+ if (typeof step === 'function') {
3850
+ subSteps = await step({
3851
+ context,
3852
+ stepIndex: compositeStepIndex,
3853
+ contextRunState,
3854
+ lumpVariables,
3855
+ });
3856
+ }
3857
+ else {
3858
+ subSteps = step;
3859
+ }
3860
+ await walkAndExecuteSteps(subSteps, nextCallHeadIndex);
3861
+ continue;
3862
+ }
3863
+ logger.verbose(`step ${JSON.stringify(step)}`);
3864
+ const { commandFn = () => null, stepVariables, promptFn, postCommandExecFn, continueOnError, timeoutMillis = 1000 * 60 * 30, } = step;
3865
+ const prompt = promptFn
3866
+ ? await promptFn({
3633
3867
  context,
3634
3868
  stepIndex: compositeStepIndex,
3635
3869
  contextRunState,
3636
3870
  lumpVariables,
3637
- });
3638
- }
3639
- else {
3640
- subSteps = step;
3641
- }
3642
- await walkAndExecuteSteps(subSteps, nextCallHeadIndex);
3643
- continue;
3644
- }
3645
- logger.verbose(`step ${JSON.stringify(step)}`);
3646
- const { commandFn = () => null, stepVariables, promptFn, postCommandExecFn, continueOnError, timeoutMillis = 1000 * 60 * 30, } = step;
3647
- const prompt = promptFn
3648
- ? await promptFn({
3871
+ stepVariables,
3872
+ })
3873
+ : '';
3874
+ const command = await commandFn({
3649
3875
  context,
3876
+ prompt,
3650
3877
  stepIndex: compositeStepIndex,
3651
3878
  contextRunState,
3652
3879
  lumpVariables,
3653
3880
  stepVariables,
3654
- })
3655
- : '';
3656
- const command = await commandFn({
3657
- context,
3658
- prompt,
3659
- stepIndex: compositeStepIndex,
3660
- contextRunState,
3661
- lumpVariables,
3662
- stepVariables,
3663
- projectRoot,
3664
- workspacePath,
3665
- });
3666
- let commandResult = '';
3667
- let commandSucceeded = true;
3668
- if (command != null) {
3669
- const { executable, args, env } = command;
3670
- logger.verbose(`command for prompt ${executable} ${args.join(' ')}`);
3671
- if (env != null) {
3672
- logger.verbose(`command env overrides ${JSON.stringify(env)}`);
3673
- }
3674
- logger.verbose(`workspacePath ${workspacePath}`);
3675
- const commandExec = await execBinary(executable, args, timeoutMillis, {
3676
- stdio: ['inherit', 'pipe', 'pipe'],
3677
- cwd: workspacePath,
3678
- ...(env != null ? { env: { ...process.env, ...env } } : {}),
3881
+ projectRoot,
3882
+ workspacePath,
3679
3883
  });
3680
- logger.verbose(`commandExec ${JSON.stringify(commandExec)}`);
3681
- if (!commandExec.success) {
3682
- if (!continueOnError) {
3683
- stepWalkFailure = set(commandExec, ['data', 'message'], `Failed to run the command: ${commandExec.data.message}. Command: ${executable} ${args.join(' ')}`);
3684
- return;
3884
+ let commandResult = '';
3885
+ let commandSucceeded = true;
3886
+ if (command != null) {
3887
+ const { executable, args, env } = command;
3888
+ logger.verbose(`command for prompt ${executable} ${args.join(' ')}`);
3889
+ if (env != null) {
3890
+ logger.verbose(`command env overrides ${JSON.stringify(env)}`);
3891
+ }
3892
+ logger.verbose(`workspacePath ${workspacePath}`);
3893
+ const commandExec = await execBinary({
3894
+ binaryPath: executable,
3895
+ args,
3896
+ timeoutMillis,
3897
+ stdio: ['inherit', 'pipe', 'pipe'],
3898
+ cwd: workspacePath,
3899
+ signal,
3900
+ ...(env != null ? { env: { ...process.env, ...env } } : {}),
3901
+ });
3902
+ logger.verbose(`commandExec ${JSON.stringify(commandExec)}`);
3903
+ if (!commandExec.success) {
3904
+ const aborted = commandExec.data.reason === 'aborted';
3905
+ if (aborted || !continueOnError) {
3906
+ stepWalkFailure = {
3907
+ success: false,
3908
+ data: {
3909
+ message: `Failed to run the command: ${commandExec.data.message}. Command: ${executable} ${args.join(' ')}`,
3910
+ reason: 'stepWalkFailed',
3911
+ },
3912
+ };
3913
+ return;
3914
+ }
3915
+ commandSucceeded = false;
3916
+ commandResult = (commandExec.data.stdout
3917
+ || commandExec.data.stderr
3918
+ || commandExec.data.message
3919
+ || '').toString();
3920
+ logger.verbose(`commandResult ${commandResult}`);
3921
+ }
3922
+ else {
3923
+ commandResult = (commandExec.data.stdout || commandExec.data.stderr || '').toString();
3924
+ logger.verbose(`commandResult ${commandResult}`);
3925
+ }
3926
+ if (commandSucceeded) {
3927
+ const gitStatusAfterCommand = await execAsync(`git status`, { cwd: workspacePath });
3928
+ logger.verbose(`gitStatusCommand ${JSON.stringify(gitStatusAfterCommand.data)}`);
3685
3929
  }
3686
- commandSucceeded = false;
3687
- commandResult = (commandExec.data.stdout
3688
- || commandExec.data.stderr
3689
- || commandExec.data.message
3690
- || '').toString();
3691
- logger.verbose(`commandResult ${commandResult}`);
3692
3930
  }
3693
- else {
3694
- commandResult = (commandExec.data.stdout || commandExec.data.stderr || '').toString();
3695
- logger.verbose(`commandResult ${commandResult}`);
3931
+ const postCommandExecFnInput = {
3932
+ commandResult,
3933
+ commandSucceeded,
3934
+ context,
3935
+ prompt,
3936
+ stepIndex: compositeStepIndex,
3937
+ contextRunState,
3938
+ lumpVariables,
3939
+ stepVariables,
3940
+ projectRoot,
3941
+ };
3942
+ logger.verbose(`context is ${JSON.stringify(context)}`);
3943
+ const keepHistoryFilePath = getKeepHistoryFilePathFn(context) || '';
3944
+ logger.verbose(`keepHistoryFilePath ${keepHistoryFilePath}`);
3945
+ if (!!command && keepHistoryFilePath.length > 0) {
3946
+ const appendResult = await appendHistoryEntry({
3947
+ filePath: keepHistoryFilePath,
3948
+ entry: postCommandExecFnInput,
3949
+ });
3950
+ if (!appendResult.success) {
3951
+ // TODO: sanitize history appending to avoid this warning for certain commands outputs
3952
+ logger.warn(`Failed to append history entry to ${keepHistoryFilePath}: ${appendResult.data}`);
3953
+ }
3696
3954
  }
3697
- if (commandSucceeded) {
3698
- const gitStatusAfterCommand = await execAsync(`git status`, { cwd: workspacePath });
3699
- logger.verbose(`gitStatusCommand ${JSON.stringify(gitStatusAfterCommand.data)}`);
3955
+ if (postCommandExecFn) {
3956
+ const returnedSteps = await postCommandExecFn(postCommandExecFnInput);
3957
+ if (returnedSteps != null && returnedSteps.length > 0) {
3958
+ await walkAndExecuteSteps(returnedSteps, nextCallHeadIndex);
3959
+ }
3700
3960
  }
3701
3961
  }
3702
- const postCommandExecFnInput = {
3703
- commandResult,
3704
- commandSucceeded,
3705
- context,
3706
- prompt,
3707
- stepIndex: compositeStepIndex,
3708
- contextRunState,
3709
- lumpVariables,
3710
- stepVariables,
3711
- projectRoot,
3712
- };
3713
- logger.verbose(`context is ${JSON.stringify(context)}`);
3714
- const keepHistoryFilePath = getKeepHistoryFilePathFn(context) || '';
3715
- logger.verbose(`keepHistoryFilePath ${keepHistoryFilePath}`);
3716
- if (!!command && keepHistoryFilePath.length > 0) {
3717
- const appendResult = await appendHistoryEntry({
3718
- filePath: keepHistoryFilePath,
3719
- entry: postCommandExecFnInput,
3962
+ }
3963
+ try {
3964
+ await walkAndExecuteSteps(steps, []);
3965
+ }
3966
+ finally {
3967
+ try {
3968
+ await teardownFn({
3969
+ lumpVariables,
3970
+ contextList,
3971
+ currentContextIndex: i,
3972
+ contextRunState,
3720
3973
  });
3721
- if (!appendResult.success) {
3722
- // TODO: sanitize history appending to avoid this warning for certain commands outputs
3723
- logger.warn(`Failed to append history entry to ${keepHistoryFilePath}: ${appendResult.data}`);
3724
- }
3725
3974
  }
3726
- if (postCommandExecFn) {
3727
- const returnedSteps = await postCommandExecFn(postCommandExecFnInput);
3728
- if (returnedSteps != null && returnedSteps.length > 0) {
3729
- await walkAndExecuteSteps(returnedSteps, nextCallHeadIndex);
3730
- }
3975
+ catch (error) {
3976
+ const message = error instanceof Error ? error.message : String(error);
3977
+ logger.error(`Failed to run teardownFn: ${message}`);
3731
3978
  }
3732
3979
  }
3733
- }
3734
- await walkAndExecuteSteps(steps, []);
3735
- if (stepWalkFailure) {
3736
- return stepWalkFailure;
3737
- }
3738
- await teardownFn({
3739
- lumpVariables,
3740
- contextList,
3741
- currentContextIndex: i,
3742
- contextRunState,
3743
- });
3744
- const perContextInput = {
3745
- ...injectedGitAndWorkspaceFnsInput,
3746
- context,
3747
- };
3748
- const gitAddCommand = await execAsync(gitAddCommandFn(perContextInput), { cwd: workspacePath });
3749
- logger.verbose(`gitAddCommand ${JSON.stringify(gitAddCommand)}`);
3750
- if (!gitAddCommand.success) {
3751
- return set(gitAddCommand, ['data', 'message'], `Failed to add the changes for context ${context.name}: ${gitAddCommand.data.message}`);
3752
- }
3753
- const commitMessage = gitCommitMessageFn({ context, lumpVariables, baseBranch });
3754
- const commitCommand = await execAsync(gitCommitCommandFn({
3755
- ...perContextInput,
3756
- commitMessage,
3757
- }), { cwd: workspacePath });
3758
- logger.verbose(`commitCommand ${JSON.stringify(commitCommand)}`);
3759
- if (!commitCommand.success) {
3760
- logger.error(formatExecFailureMessage({
3980
+ if (stepWalkFailure) {
3981
+ runFailure = stepWalkFailure;
3982
+ break;
3983
+ }
3984
+ const perContextInput = {
3985
+ ...injectedGitAndWorkspaceFnsInput,
3986
+ context,
3987
+ };
3988
+ const addOutcome = await runOptionalGitCommand({
3989
+ label: `git add for context ${context.name}`,
3990
+ getCommand: () => gitAddCommandFn(perContextInput),
3991
+ cwd: workspacePath,
3992
+ logger,
3993
+ });
3994
+ if (addOutcome === 'failed') {
3995
+ runFailure = {
3996
+ success: false,
3997
+ data: {
3998
+ message: `Failed to add the changes for context ${context.name}`,
3999
+ },
4000
+ };
4001
+ break;
4002
+ }
4003
+ const commitMessage = gitCommitMessageFn({ context, lumpVariables, baseBranch });
4004
+ await runOptionalGitCommand({
3761
4005
  label: `git commit for context ${context.name}`,
3762
- failure: commitCommand,
3763
- }));
4006
+ getCommand: () => gitCommitCommandFn({
4007
+ ...perContextInput,
4008
+ commitMessage,
4009
+ }),
4010
+ cwd: workspacePath,
4011
+ logger,
4012
+ });
4013
+ }
4014
+ if (!runFailure) {
4015
+ await runOptionalGitCommand({
4016
+ label: `git push on branch ${branchName}`,
4017
+ getCommand: () => gitPushCommandFn(injectedGitAndWorkspaceFnsInput),
4018
+ cwd: workspacePath,
4019
+ logger,
4020
+ });
3764
4021
  }
3765
4022
  }
3766
- const pushCommand = await execAsync(gitPushCommandFn(injectedGitAndWorkspaceFnsInput), { cwd: workspacePath });
3767
- logger.verbose(`pushCommand ${JSON.stringify(pushCommand)}`);
3768
- if (!pushCommand.success) {
3769
- logger.error(formatExecFailureMessage({
3770
- label: `git push on branch ${branchName}`,
3771
- failure: pushCommand,
3772
- }));
3773
- }
3774
- const teardownWorkspaceCommand = await teardownWorkspaceFn(injectedGitAndWorkspaceFnsInput);
3775
- logger.verbose(`teardownWorkspaceCommand ${teardownWorkspaceCommand}`);
3776
- if (teardownWorkspaceCommand) {
3777
- const teardownWorkspaceCommandExec = await execAsync(teardownWorkspaceCommand, { cwd: workspacePath });
3778
- logger.verbose(`teardownWorkspaceCommandExec ${JSON.stringify(teardownWorkspaceCommandExec)}`);
3779
- if (!teardownWorkspaceCommandExec.success) {
3780
- return set(teardownWorkspaceCommandExec, ['data', 'message'], `Failed to teardown the workspace: ${teardownWorkspaceCommandExec.data.message}`);
4023
+ finally {
4024
+ const teardownWorkspaceCommand = await teardownWorkspaceFn(injectedGitAndWorkspaceFnsInput);
4025
+ logger.verbose(`teardownWorkspaceCommand ${teardownWorkspaceCommand}`);
4026
+ if (teardownWorkspaceCommand) {
4027
+ const teardownWorkspaceCommandExec = await execAsync(teardownWorkspaceCommand, { cwd: workspacePath });
4028
+ logger.verbose(`teardownWorkspaceCommandExec ${JSON.stringify(teardownWorkspaceCommandExec)}`);
4029
+ if (!teardownWorkspaceCommandExec.success) {
4030
+ if (runFailure) {
4031
+ logger.error(formatExecFailureMessage({
4032
+ label: 'teardown workspace',
4033
+ failure: teardownWorkspaceCommandExec,
4034
+ }));
4035
+ }
4036
+ else {
4037
+ runFailure = {
4038
+ success: false,
4039
+ data: {
4040
+ message: `Failed to teardown the workspace: ${teardownWorkspaceCommandExec.data.message}`,
4041
+ reason: 'workspaceTeardownFailed',
4042
+ },
4043
+ };
4044
+ }
4045
+ }
3781
4046
  }
3782
4047
  }
4048
+ if (runFailure) {
4049
+ return runFailure;
4050
+ }
3783
4051
  return success({
3784
4052
  branchName,
3785
4053
  contextNames,
@@ -3865,17 +4133,32 @@ async function scanDirectory({ dirPath, allPaths, gitignoresMap, projectRoot, lo
3865
4133
  }
3866
4134
  }
3867
4135
 
4136
+ /**
4137
+ * Refreshes remote-tracking refs for status / planning (one network fetch).
4138
+ * Uses `--no-write-fetch-head` so concurrent preflight/pull paths are safer.
4139
+ */
4140
+ const refreshRemoteTrackingRefs = async (input) => {
4141
+ const { projectRoot, remoteName = 'origin' } = input;
4142
+ const result = await execAsync(`git fetch --prune --no-write-fetch-head ${remoteName}`, { cwd: projectRoot });
4143
+ if (!result.success) {
4144
+ return failure(result.data.message);
4145
+ }
4146
+ return success(undefined);
4147
+ };
4148
+
3868
4149
  async function getContextStatus(params) {
3869
- const { contextName, gitCommitMessageFn, projectRoot, baseBranch, lumpVariables: lumpVariablesInput, contextVariables = {}, remoteName = "origin", logger, } = params;
4150
+ const { contextName, gitCommitMessageFn, projectRoot, baseBranch, lumpVariables: lumpVariablesInput, contextVariables = {}, remoteName = "origin", logger, skipFetch = false, } = params;
3870
4151
  const lumpVariables = (lumpVariablesInput ?? {});
3871
4152
  const commitMessage = gitCommitMessageFn({
3872
4153
  context: { name: contextName, variables: contextVariables },
3873
4154
  lumpVariables,
3874
4155
  baseBranch,
3875
4156
  });
3876
- const fetchAll = await execAsync(`git fetch --all --prune`, { cwd: projectRoot });
3877
- if (!fetchAll.success)
3878
- return 'toDo';
4157
+ if (!skipFetch) {
4158
+ const fetchResult = await refreshRemoteTrackingRefs({ projectRoot, remoteName });
4159
+ if (!fetchResult.success)
4160
+ return 'toDo';
4161
+ }
3879
4162
  const logResult = await execAsync(`git log --remotes=${remoteName} -F --grep=${shellSingleQuote(commitMessage)} --format=${shellSingleQuote('%H %s')}`, { cwd: projectRoot });
3880
4163
  if (!logResult.success)
3881
4164
  return 'toDo';
@@ -3884,6 +4167,7 @@ async function getContextStatus(params) {
3884
4167
  const matchingHashes = parseGitLogHashSubjectLines(logResultOutput)
3885
4168
  .filter((entry) => entry.subject === commitMessage)
3886
4169
  .map((entry) => entry.hash);
4170
+ logger?.verbose(`contextName ${contextName}`);
3887
4171
  logger?.verbose(`remoteName ${remoteName}`);
3888
4172
  logger?.verbose(`baseBranch ${baseBranch}`);
3889
4173
  logger?.verbose(`commitMessage ${commitMessage}`);
@@ -3938,7 +4222,7 @@ function validateContextListNames(contextList) {
3938
4222
  }
3939
4223
 
3940
4224
  async function getToDoContextList(params) {
3941
- const { getContextListFn, lumpVariables, gitCommitMessageFn, projectRoot, baseBranch, logger } = params;
4225
+ const { getContextListFn, lumpVariables, gitCommitMessageFn, projectRoot, baseBranch, logger, refreshRemoteTrackingRefsFn = refreshRemoteTrackingRefs, } = params;
3942
4226
  const codeBasePathsResult = await getCodeBasePaths({ cwd: projectRoot, logger });
3943
4227
  if (!codeBasePathsResult.success) {
3944
4228
  return failure({
@@ -3956,21 +4240,28 @@ async function getToDoContextList(params) {
3956
4240
  const allCtxNames = contextList.flatMap(context => [context.name, ...(context.options?.dependsOnContexts ?? [])]);
3957
4241
  const allCtxNamesSet = new Set(allCtxNames);
3958
4242
  const allCtxNamesList = Array.from(allCtxNamesSet);
3959
- const contextStatusList = await Promise.all(allCtxNamesList.map((contextName) => {
3960
- return getContextStatus({
3961
- contextName: contextName,
4243
+ const refreshResult = await refreshRemoteTrackingRefsFn({ projectRoot });
4244
+ let contextStatusMap;
4245
+ if (!refreshResult.success) {
4246
+ logger?.warn(`Failed to refresh remote-tracking refs for context status; treating contexts as toDo: ${refreshResult.data}`);
4247
+ contextStatusMap = new Map(allCtxNamesList.map((name) => [name, 'toDo']));
4248
+ }
4249
+ else {
4250
+ const contextStatusList = await Promise.all(allCtxNamesList.map((contextName) => getContextStatus({
4251
+ contextName,
3962
4252
  contextVariables: {}, // TODO: Remove contextVariables from getContextStatus, really not needed
3963
4253
  gitCommitMessageFn,
3964
4254
  lumpVariables,
3965
4255
  projectRoot,
3966
4256
  baseBranch,
3967
4257
  logger,
3968
- });
3969
- }));
3970
- const contextStatusMap = new Map(allCtxNamesList.map((contextName, i) => [contextName, contextStatusList[i]]));
4258
+ skipFetch: true,
4259
+ })));
4260
+ contextStatusMap = new Map(allCtxNamesList.map((contextName, i) => [contextName, contextStatusList[i]]));
4261
+ }
3971
4262
  const contextListToDo = contextList
3972
- .filter((context, contextIndex) => {
3973
- const contextStatus = contextStatusList[contextIndex];
4263
+ .filter((context) => {
4264
+ const contextStatus = contextStatusMap.get(context.name);
3974
4265
  if (contextStatus && contextStatus !== 'toDo')
3975
4266
  return false;
3976
4267
  const deps = context.options?.dependsOnContexts;
@@ -4036,7 +4327,7 @@ const defaultSetupWorkspaceFnWithWorktree = async (input) => {
4036
4327
  };
4037
4328
 
4038
4329
  async function runLump(input) {
4039
- const { baseBranch, branchFn, lumpVariables: lumpVariablesInput, getContextListFn, gitAddCommandFn = defaultGitAddCommandFn, gitCommitCommandFn = defaultGitCommitCommandFn, gitCommitMessageFn = defaultGitCommitMessageFn, gitPushCommandFn = defaultGitPushCommandFn, numberOfContextsPerBranch = 1, projectRoot, steps, setupFn = () => ({ contextRunState: {} }), setupWorkspaceFn = defaultSetupWorkspaceFn, teardownFn = () => undefined, teardownWorkspaceFn = defaultTeardownWorkspaceFn, getKeepHistoryFilePathFn = () => undefined, logger: loggerInput, } = input;
4330
+ const { baseBranch, branchFn, lumpVariables: lumpVariablesInput, getContextListFn, gitAddCommandFn = defaultGitAddCommandFn, gitCommitCommandFn = defaultGitCommitCommandFn, gitCommitMessageFn = defaultGitCommitMessageFn, gitPushCommandFn = defaultGitPushCommandFn, numberOfContextsPerBranch = 1, projectRoot, steps, setupFn = () => ({ contextRunState: {} }), setupWorkspaceFn = defaultSetupWorkspaceFn, teardownFn = () => undefined, teardownWorkspaceFn = defaultTeardownWorkspaceFn, getKeepHistoryFilePathFn = () => undefined, logger: loggerInput, signal, refreshRemoteTrackingRefsFn, } = input;
4040
4331
  const lumpVariables = (lumpVariablesInput ?? {});
4041
4332
  const logger = loggerInput ?? createConsoleLogger({});
4042
4333
  const contextListToDoResult = await getToDoContextList({
@@ -4046,6 +4337,7 @@ async function runLump(input) {
4046
4337
  baseBranch,
4047
4338
  gitCommitMessageFn,
4048
4339
  logger,
4340
+ refreshRemoteTrackingRefsFn,
4049
4341
  });
4050
4342
  if (!contextListToDoResult.success) {
4051
4343
  return set(contextListToDoResult, ['data', 'message'], "Error in runLump: Failed to get to do context list. Original Error: " + contextListToDoResult.data.message);
@@ -4082,6 +4374,7 @@ async function runLump(input) {
4082
4374
  teardownWorkspaceFn,
4083
4375
  logger,
4084
4376
  getKeepHistoryFilePathFn,
4377
+ signal,
4085
4378
  });
4086
4379
  if (!executeStepsResult.success) {
4087
4380
  return set(executeStepsResult, ['data', 'message'], "Error in runLump: Failed to execute steps for context list. Original Error: " + executeStepsResult.data.message);
@@ -4112,9 +4405,13 @@ exports.getCodeBasePaths = getCodeBasePaths;
4112
4405
  exports.getContextStatus = getContextStatus;
4113
4406
  exports.getToDoContextList = getToDoContextList;
4114
4407
  exports.historyFormatFromPath = historyFormatFromPath;
4408
+ exports.isProcessAlive = isProcessAlive;
4409
+ exports.killProcessTree = killProcessTree;
4410
+ exports.nodeErrnoCode = nodeErrnoCode;
4115
4411
  exports.parseGitLogHashSubjectLines = parseGitLogHashSubjectLines;
4116
4412
  exports.pathExists = pathExists;
4117
4413
  exports.readHistoryFile = readHistoryFile;
4414
+ exports.refreshRemoteTrackingRefs = refreshRemoteTrackingRefs;
4118
4415
  exports.resolveSpawnExecutable = resolveSpawnExecutable;
4119
4416
  exports.runLump = runLump;
4120
4417
  exports.set = set;