@lumpcode/core 0.0.14 → 0.0.16

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 (34) 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/index.cjs +410 -159
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.js +409 -161
  9. package/dist/index.js.map +1 -1
  10. package/dist/testing/processTreeChild.d.cts +2 -0
  11. package/dist/testing/processTreeChild.d.cts.map +1 -0
  12. package/dist/testing/processTreeTestHelpers.d.ts +12 -0
  13. package/dist/testing/processTreeTestHelpers.d.ts.map +1 -0
  14. package/dist/testing/sigtermIgnorantTreeChild.d.cts +2 -0
  15. package/dist/testing/sigtermIgnorantTreeChild.d.cts.map +1 -0
  16. package/dist/types/ExecuteStepsFailureData.d.ts +6 -0
  17. package/dist/types/ExecuteStepsFailureData.d.ts.map +1 -0
  18. package/dist/types/ExecuteStepsFailureReason.d.ts +2 -0
  19. package/dist/types/ExecuteStepsFailureReason.d.ts.map +1 -0
  20. package/dist/types/index.d.ts +2 -0
  21. package/dist/types/index.d.ts.map +1 -1
  22. package/dist/usages/runLump/main.d.ts +4 -4
  23. package/dist/usages/runLump/main.d.ts.map +1 -1
  24. package/dist/utils/index.d.ts +3 -0
  25. package/dist/utils/index.d.ts.map +1 -1
  26. package/dist/utils/isProcessAlive/index.d.ts +2 -1
  27. package/dist/utils/isProcessAlive/index.d.ts.map +1 -1
  28. package/dist/utils/killProcessTree/index.d.ts +2 -0
  29. package/dist/utils/killProcessTree/index.d.ts.map +1 -0
  30. package/dist/utils/killProcessTree/main.d.ts +11 -0
  31. package/dist/utils/killProcessTree/main.d.ts.map +1 -0
  32. package/dist/utils/nodeErrnoCode/index.d.ts +1 -1
  33. package/dist/utils/nodeErrnoCode/index.d.ts.map +1 -1
  34. 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,7 @@ 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 executeStepsForContextList({ baseBranch, branchFn, lumpVariables, contextList, gitAddCommandFn, gitCommitCommandFn, gitPushCommandFn, gitCommitMessageFn, projectRoot, steps, setupFn, setupWorkspaceFn, teardownFn, teardownWorkspaceFn, getKeepHistoryFilePathFn, logger: loggerInput, signal, }) {
3575
3770
  const logger = loggerInput ?? createConsoleLogger({});
3576
3771
  const contextNames = contextList.map(context => context.name);
3577
3772
  logger.verbose(`contextNames ${JSON.stringify(contextNames)}`);
@@ -3606,180 +3801,232 @@ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables,
3606
3801
  const gitStatusCommand = await execAsync(`git status`, { cwd: workspacePath });
3607
3802
  logger.verbose(`gitStatusCommand ${JSON.stringify(gitStatusCommand.data)}`);
3608
3803
  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({
3804
+ let runFailure;
3805
+ try {
3806
+ for (let i = 0; i < contextList.length; i++) {
3807
+ const context = contextList[i];
3808
+ logger.info(contextList.length > 1
3809
+ ? `Running context "${context.name}" (${i + 1}/${contextList.length})`
3810
+ : `Running context "${context.name}"`);
3811
+ const setupResult = await setupFn({
3812
+ contextList,
3813
+ lumpVariables,
3814
+ currentContextIndex: i,
3815
+ });
3816
+ const contextRunState = setupResult?.contextRunState || {};
3817
+ let stepWalkFailure;
3818
+ async function walkAndExecuteSteps(stepsToExec, currStepIndex) {
3819
+ for (let stepIndex = 0; stepIndex < stepsToExec.length; stepIndex++) {
3820
+ if (stepWalkFailure) {
3821
+ return;
3822
+ }
3823
+ const step = stepsToExec[stepIndex];
3824
+ const nextCallHeadIndex = [...currStepIndex, stepIndex];
3825
+ const compositeStepIndex = nextCallHeadIndex.length === 1 ? nextCallHeadIndex[0] : nextCallHeadIndex;
3826
+ if (typeof step === 'function' || Array.isArray(step)) {
3827
+ let subSteps = [];
3828
+ if (typeof step === 'function') {
3829
+ subSteps = await step({
3830
+ context,
3831
+ stepIndex: compositeStepIndex,
3832
+ contextRunState,
3833
+ lumpVariables,
3834
+ });
3835
+ }
3836
+ else {
3837
+ subSteps = step;
3838
+ }
3839
+ await walkAndExecuteSteps(subSteps, nextCallHeadIndex);
3840
+ continue;
3841
+ }
3842
+ logger.verbose(`step ${JSON.stringify(step)}`);
3843
+ const { commandFn = () => null, stepVariables, promptFn, postCommandExecFn, continueOnError, timeoutMillis = 1000 * 60 * 30, } = step;
3844
+ const prompt = promptFn
3845
+ ? await promptFn({
3633
3846
  context,
3634
3847
  stepIndex: compositeStepIndex,
3635
3848
  contextRunState,
3636
3849
  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({
3850
+ stepVariables,
3851
+ })
3852
+ : '';
3853
+ const command = await commandFn({
3649
3854
  context,
3855
+ prompt,
3650
3856
  stepIndex: compositeStepIndex,
3651
3857
  contextRunState,
3652
3858
  lumpVariables,
3653
3859
  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 } } : {}),
3860
+ projectRoot,
3861
+ workspacePath,
3679
3862
  });
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;
3863
+ let commandResult = '';
3864
+ let commandSucceeded = true;
3865
+ if (command != null) {
3866
+ const { executable, args, env } = command;
3867
+ logger.verbose(`command for prompt ${executable} ${args.join(' ')}`);
3868
+ if (env != null) {
3869
+ logger.verbose(`command env overrides ${JSON.stringify(env)}`);
3870
+ }
3871
+ logger.verbose(`workspacePath ${workspacePath}`);
3872
+ const commandExec = await execBinary({
3873
+ binaryPath: executable,
3874
+ args,
3875
+ timeoutMillis,
3876
+ stdio: ['inherit', 'pipe', 'pipe'],
3877
+ cwd: workspacePath,
3878
+ signal,
3879
+ ...(env != null ? { env: { ...process.env, ...env } } : {}),
3880
+ });
3881
+ logger.verbose(`commandExec ${JSON.stringify(commandExec)}`);
3882
+ if (!commandExec.success) {
3883
+ const aborted = commandExec.data.reason === 'aborted';
3884
+ if (aborted || !continueOnError) {
3885
+ stepWalkFailure = {
3886
+ success: false,
3887
+ data: {
3888
+ message: `Failed to run the command: ${commandExec.data.message}. Command: ${executable} ${args.join(' ')}`,
3889
+ reason: 'stepWalkFailed',
3890
+ },
3891
+ };
3892
+ return;
3893
+ }
3894
+ commandSucceeded = false;
3895
+ commandResult = (commandExec.data.stdout
3896
+ || commandExec.data.stderr
3897
+ || commandExec.data.message
3898
+ || '').toString();
3899
+ logger.verbose(`commandResult ${commandResult}`);
3900
+ }
3901
+ else {
3902
+ commandResult = (commandExec.data.stdout || commandExec.data.stderr || '').toString();
3903
+ logger.verbose(`commandResult ${commandResult}`);
3904
+ }
3905
+ if (commandSucceeded) {
3906
+ const gitStatusAfterCommand = await execAsync(`git status`, { cwd: workspacePath });
3907
+ logger.verbose(`gitStatusCommand ${JSON.stringify(gitStatusAfterCommand.data)}`);
3685
3908
  }
3686
- commandSucceeded = false;
3687
- commandResult = (commandExec.data.stdout
3688
- || commandExec.data.stderr
3689
- || commandExec.data.message
3690
- || '').toString();
3691
- logger.verbose(`commandResult ${commandResult}`);
3692
3909
  }
3693
- else {
3694
- commandResult = (commandExec.data.stdout || commandExec.data.stderr || '').toString();
3695
- logger.verbose(`commandResult ${commandResult}`);
3910
+ const postCommandExecFnInput = {
3911
+ commandResult,
3912
+ commandSucceeded,
3913
+ context,
3914
+ prompt,
3915
+ stepIndex: compositeStepIndex,
3916
+ contextRunState,
3917
+ lumpVariables,
3918
+ stepVariables,
3919
+ projectRoot,
3920
+ };
3921
+ logger.verbose(`context is ${JSON.stringify(context)}`);
3922
+ const keepHistoryFilePath = getKeepHistoryFilePathFn(context) || '';
3923
+ logger.verbose(`keepHistoryFilePath ${keepHistoryFilePath}`);
3924
+ if (!!command && keepHistoryFilePath.length > 0) {
3925
+ const appendResult = await appendHistoryEntry({
3926
+ filePath: keepHistoryFilePath,
3927
+ entry: postCommandExecFnInput,
3928
+ });
3929
+ if (!appendResult.success) {
3930
+ // TODO: sanitize history appending to avoid this warning for certain commands outputs
3931
+ logger.warn(`Failed to append history entry to ${keepHistoryFilePath}: ${appendResult.data}`);
3932
+ }
3696
3933
  }
3697
- if (commandSucceeded) {
3698
- const gitStatusAfterCommand = await execAsync(`git status`, { cwd: workspacePath });
3699
- logger.verbose(`gitStatusCommand ${JSON.stringify(gitStatusAfterCommand.data)}`);
3934
+ if (postCommandExecFn) {
3935
+ const returnedSteps = await postCommandExecFn(postCommandExecFnInput);
3936
+ if (returnedSteps != null && returnedSteps.length > 0) {
3937
+ await walkAndExecuteSteps(returnedSteps, nextCallHeadIndex);
3938
+ }
3700
3939
  }
3701
3940
  }
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,
3941
+ }
3942
+ try {
3943
+ await walkAndExecuteSteps(steps, []);
3944
+ }
3945
+ finally {
3946
+ try {
3947
+ await teardownFn({
3948
+ lumpVariables,
3949
+ contextList,
3950
+ currentContextIndex: i,
3951
+ contextRunState,
3720
3952
  });
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
3953
  }
3726
- if (postCommandExecFn) {
3727
- const returnedSteps = await postCommandExecFn(postCommandExecFnInput);
3728
- if (returnedSteps != null && returnedSteps.length > 0) {
3729
- await walkAndExecuteSteps(returnedSteps, nextCallHeadIndex);
3730
- }
3954
+ catch (error) {
3955
+ const message = error instanceof Error ? error.message : String(error);
3956
+ logger.error(`Failed to run teardownFn: ${message}`);
3731
3957
  }
3732
3958
  }
3959
+ if (stepWalkFailure) {
3960
+ runFailure = stepWalkFailure;
3961
+ break;
3962
+ }
3963
+ const perContextInput = {
3964
+ ...injectedGitAndWorkspaceFnsInput,
3965
+ context,
3966
+ };
3967
+ const gitAddCommand = await execAsync(gitAddCommandFn(perContextInput), { cwd: workspacePath });
3968
+ logger.verbose(`gitAddCommand ${JSON.stringify(gitAddCommand)}`);
3969
+ if (!gitAddCommand.success) {
3970
+ runFailure = {
3971
+ success: false,
3972
+ data: {
3973
+ message: `Failed to add the changes for context ${context.name}: ${gitAddCommand.data.message}`,
3974
+ },
3975
+ };
3976
+ break;
3977
+ }
3978
+ const commitMessage = gitCommitMessageFn({ context, lumpVariables, baseBranch });
3979
+ const commitCommand = await execAsync(gitCommitCommandFn({
3980
+ ...perContextInput,
3981
+ commitMessage,
3982
+ }), { cwd: workspacePath });
3983
+ logger.verbose(`commitCommand ${JSON.stringify(commitCommand)}`);
3984
+ if (!commitCommand.success) {
3985
+ logger.error(formatExecFailureMessage({
3986
+ label: `git commit for context ${context.name}`,
3987
+ failure: commitCommand,
3988
+ }));
3989
+ }
3733
3990
  }
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({
3761
- label: `git commit for context ${context.name}`,
3762
- failure: commitCommand,
3763
- }));
3991
+ if (!runFailure) {
3992
+ const pushCommand = await execAsync(gitPushCommandFn(injectedGitAndWorkspaceFnsInput), { cwd: workspacePath });
3993
+ logger.verbose(`pushCommand ${JSON.stringify(pushCommand)}`);
3994
+ if (!pushCommand.success) {
3995
+ logger.error(formatExecFailureMessage({
3996
+ label: `git push on branch ${branchName}`,
3997
+ failure: pushCommand,
3998
+ }));
3999
+ }
3764
4000
  }
3765
4001
  }
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}`);
4002
+ finally {
4003
+ const teardownWorkspaceCommand = await teardownWorkspaceFn(injectedGitAndWorkspaceFnsInput);
4004
+ logger.verbose(`teardownWorkspaceCommand ${teardownWorkspaceCommand}`);
4005
+ if (teardownWorkspaceCommand) {
4006
+ const teardownWorkspaceCommandExec = await execAsync(teardownWorkspaceCommand, { cwd: workspacePath });
4007
+ logger.verbose(`teardownWorkspaceCommandExec ${JSON.stringify(teardownWorkspaceCommandExec)}`);
4008
+ if (!teardownWorkspaceCommandExec.success) {
4009
+ if (runFailure) {
4010
+ logger.error(formatExecFailureMessage({
4011
+ label: 'teardown workspace',
4012
+ failure: teardownWorkspaceCommandExec,
4013
+ }));
4014
+ }
4015
+ else {
4016
+ runFailure = {
4017
+ success: false,
4018
+ data: {
4019
+ message: `Failed to teardown the workspace: ${teardownWorkspaceCommandExec.data.message}`,
4020
+ reason: 'workspaceTeardownFailed',
4021
+ },
4022
+ };
4023
+ }
4024
+ }
3781
4025
  }
3782
4026
  }
4027
+ if (runFailure) {
4028
+ return runFailure;
4029
+ }
3783
4030
  return success({
3784
4031
  branchName,
3785
4032
  contextNames,
@@ -4036,7 +4283,7 @@ const defaultSetupWorkspaceFnWithWorktree = async (input) => {
4036
4283
  };
4037
4284
 
4038
4285
  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;
4286
+ 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, } = input;
4040
4287
  const lumpVariables = (lumpVariablesInput ?? {});
4041
4288
  const logger = loggerInput ?? createConsoleLogger({});
4042
4289
  const contextListToDoResult = await getToDoContextList({
@@ -4082,6 +4329,7 @@ async function runLump(input) {
4082
4329
  teardownWorkspaceFn,
4083
4330
  logger,
4084
4331
  getKeepHistoryFilePathFn,
4332
+ signal,
4085
4333
  });
4086
4334
  if (!executeStepsResult.success) {
4087
4335
  return set(executeStepsResult, ['data', 'message'], "Error in runLump: Failed to execute steps for context list. Original Error: " + executeStepsResult.data.message);
@@ -4112,6 +4360,9 @@ exports.getCodeBasePaths = getCodeBasePaths;
4112
4360
  exports.getContextStatus = getContextStatus;
4113
4361
  exports.getToDoContextList = getToDoContextList;
4114
4362
  exports.historyFormatFromPath = historyFormatFromPath;
4363
+ exports.isProcessAlive = isProcessAlive;
4364
+ exports.killProcessTree = killProcessTree;
4365
+ exports.nodeErrnoCode = nodeErrnoCode;
4115
4366
  exports.parseGitLogHashSubjectLines = parseGitLogHashSubjectLines;
4116
4367
  exports.pathExists = pathExists;
4117
4368
  exports.readHistoryFile = readHistoryFile;