@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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { exec, spawn } from 'node:child_process';
1
+ import { execFile, exec, spawn } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
3
  import * as fs from 'node:fs';
4
4
  import * as path from 'node:path';
@@ -3403,6 +3403,152 @@ function parseGitLogHashSubjectLines(stdout) {
3403
3403
  });
3404
3404
  }
3405
3405
 
3406
+ /** Node-style `code` from a caught unknown error (e.g. `ENOENT`), or undefined. */
3407
+ function nodeErrnoCode(error) {
3408
+ if (error && typeof error === 'object' && 'code' in error) {
3409
+ const code = error.code;
3410
+ return typeof code === 'string' ? code : undefined;
3411
+ }
3412
+ return undefined;
3413
+ }
3414
+
3415
+ /** Returns whether a process id is still running (signal 0 probe). */
3416
+ function isProcessAlive(pid, options) {
3417
+ const onProbeError = options?.onProbeError ?? 'throw';
3418
+ try {
3419
+ process.kill(pid, 0);
3420
+ return true;
3421
+ }
3422
+ catch (error) {
3423
+ if (nodeErrnoCode(error) === 'ESRCH')
3424
+ return false;
3425
+ if (onProbeError === 'throw')
3426
+ throw error;
3427
+ return onProbeError === 'alive';
3428
+ }
3429
+ }
3430
+
3431
+ const execFileAsync = promisify(execFile);
3432
+ function sleep(ms) {
3433
+ return new Promise((resolve) => setTimeout(resolve, ms));
3434
+ }
3435
+ async function listUnixProcessTreePids(rootPid) {
3436
+ const { stdout } = await execFileAsync('ps', ['-eo', 'pid=,ppid=']);
3437
+ const childrenByParent = new Map();
3438
+ for (const line of stdout.split('\n')) {
3439
+ const trimmed = line.trim();
3440
+ if (!trimmed)
3441
+ continue;
3442
+ const parts = trimmed.split(/\s+/);
3443
+ if (parts.length < 2)
3444
+ continue;
3445
+ const pid = Number.parseInt(parts[0] ?? '', 10);
3446
+ const ppid = Number.parseInt(parts[1] ?? '', 10);
3447
+ if (Number.isNaN(pid) || Number.isNaN(ppid))
3448
+ continue;
3449
+ const siblings = childrenByParent.get(ppid) ?? [];
3450
+ siblings.push(pid);
3451
+ childrenByParent.set(ppid, siblings);
3452
+ }
3453
+ const ordered = [];
3454
+ const queue = [rootPid];
3455
+ while (queue.length > 0) {
3456
+ const pid = queue.shift();
3457
+ if (pid === undefined)
3458
+ break;
3459
+ ordered.push(pid);
3460
+ const children = childrenByParent.get(pid) ?? [];
3461
+ for (const childPid of children) {
3462
+ queue.push(childPid);
3463
+ }
3464
+ }
3465
+ return ordered;
3466
+ }
3467
+ function killPid(pid, signal) {
3468
+ try {
3469
+ process.kill(pid, signal);
3470
+ }
3471
+ catch (error) {
3472
+ const code = nodeErrnoCode(error);
3473
+ if (code !== 'ESRCH') {
3474
+ throw error;
3475
+ }
3476
+ }
3477
+ }
3478
+ async function killUnixProcessTreeImmediate(rootPid) {
3479
+ const pids = await listUnixProcessTreePids(rootPid);
3480
+ for (let index = pids.length - 1; index >= 0; index -= 1) {
3481
+ killPid(pids[index], 'SIGKILL');
3482
+ }
3483
+ }
3484
+ async function killUnixProcessTreeWithGrace(rootPid, graceMs) {
3485
+ const pids = await listUnixProcessTreePids(rootPid);
3486
+ for (let index = pids.length - 1; index >= 0; index -= 1) {
3487
+ killPid(pids[index], 'SIGTERM');
3488
+ }
3489
+ const deadline = Date.now() + graceMs;
3490
+ while (Date.now() < deadline) {
3491
+ const anyAlive = pids.some((pid) => isProcessAlive(pid, { onProbeError: 'dead' }));
3492
+ if (!anyAlive) {
3493
+ return;
3494
+ }
3495
+ await sleep(25);
3496
+ }
3497
+ const remaining = await listUnixProcessTreePids(rootPid);
3498
+ for (let index = remaining.length - 1; index >= 0; index -= 1) {
3499
+ killPid(remaining[index], 'SIGKILL');
3500
+ }
3501
+ }
3502
+ async function killWindowsProcessTree(rootPid) {
3503
+ try {
3504
+ await execFileAsync('taskkill', ['/PID', String(rootPid), '/T', '/F'], {
3505
+ windowsHide: true,
3506
+ });
3507
+ }
3508
+ catch (error) {
3509
+ const stderr = error && typeof error === 'object' && 'stderr' in error
3510
+ ? String(error.stderr ?? '')
3511
+ : '';
3512
+ const message = error instanceof Error ? error.message : String(error);
3513
+ const combined = `${message}\n${stderr}`;
3514
+ if (/not found|no running instance|ne existe pas|introuvable/i.test(combined)) {
3515
+ return;
3516
+ }
3517
+ // taskkill can fail to terminate some descendants while the root exits (SEA/agent trees).
3518
+ if (!isProcessAlive(rootPid)) {
3519
+ return;
3520
+ }
3521
+ throw error;
3522
+ }
3523
+ }
3524
+ /**
3525
+ * Kill a process and its descendants.
3526
+ * `graceMs` default 0 → immediate SIGKILL / taskkill /T /F.
3527
+ * When `graceMs > 0` (Unix), SIGTERM first, then SIGKILL after the grace window.
3528
+ */
3529
+ async function killProcessTree(input) {
3530
+ const { pid, graceMs = 0 } = input;
3531
+ if (!Number.isInteger(pid) || pid <= 0) {
3532
+ return failure(`Invalid pid: ${pid}`);
3533
+ }
3534
+ try {
3535
+ if (process.platform === 'win32') {
3536
+ await killWindowsProcessTree(pid);
3537
+ }
3538
+ else if (graceMs > 0) {
3539
+ await killUnixProcessTreeWithGrace(pid, graceMs);
3540
+ }
3541
+ else {
3542
+ await killUnixProcessTreeImmediate(pid);
3543
+ }
3544
+ return success(undefined);
3545
+ }
3546
+ catch (error) {
3547
+ const message = error instanceof Error ? error.message : String(error);
3548
+ return failure(`Could not kill process tree for pid ${pid}: ${message}`);
3549
+ }
3550
+ }
3551
+
3406
3552
  const execAsyncBase = promisify(exec);
3407
3553
  async function execAsync(command, options) {
3408
3554
  const { stdout, stderr, hasErrored } = await execAsyncBase(command, { cwd: options?.cwd })
@@ -3432,38 +3578,86 @@ async function execAsync(command, options) {
3432
3578
  });
3433
3579
  }
3434
3580
 
3435
- function execBinary(binaryPath, args, timeoutMillis = 1000 * 60 * 10, options) {
3581
+ function execBinary(input) {
3582
+ const { binaryPath, args, timeoutMillis = 1000 * 60 * 10, cwd, env, stdio, signal, killGraceMs = 5000, } = input;
3436
3583
  return new Promise((resolve) => {
3437
3584
  let settled = false;
3585
+ let canceling = false;
3586
+ let child;
3587
+ let stdout = '';
3588
+ let stderr = '';
3589
+ let timeout;
3438
3590
  const finish = (result) => {
3439
3591
  if (settled)
3440
3592
  return;
3441
3593
  settled = true;
3442
- clearTimeout(timeout);
3594
+ if (timeout !== undefined)
3595
+ clearTimeout(timeout);
3596
+ if (signal) {
3597
+ signal.removeEventListener('abort', onAbort);
3598
+ }
3443
3599
  resolve(result);
3444
3600
  };
3445
- const timeout = setTimeout(() => {
3601
+ const cancelWith = async (reason, message) => {
3602
+ if (settled || canceling)
3603
+ return;
3604
+ canceling = true;
3605
+ const pid = child?.pid;
3606
+ if (pid != null) {
3607
+ await killProcessTree({ pid, graceMs: killGraceMs });
3608
+ }
3446
3609
  finish(failure({
3447
- message: `Process timed out after ${timeoutMillis} milliseconds`,
3610
+ message,
3448
3611
  binaryPath,
3449
3612
  args,
3613
+ stdout,
3614
+ stderr,
3615
+ reason,
3450
3616
  }));
3451
- }, timeoutMillis);
3617
+ };
3618
+ const onAbort = () => {
3619
+ void cancelWith('aborted', 'Process aborted');
3620
+ };
3621
+ if (signal?.aborted) {
3622
+ void cancelWith('aborted', 'Process aborted');
3623
+ return;
3624
+ }
3625
+ if (signal) {
3626
+ signal.addEventListener('abort', onAbort, { once: true });
3627
+ }
3452
3628
  const { executable: resolvedExecutable, args: resolvedArgs } = resolveSpawnExecutable(binaryPath, args);
3453
- const child = spawn(resolvedExecutable, resolvedArgs, options || {});
3454
- let stdout = '', stderr = '';
3629
+ const spawnOptions = {
3630
+ ...(cwd !== undefined ? { cwd } : {}),
3631
+ ...(env !== undefined ? { env } : {}),
3632
+ ...(stdio !== undefined ? { stdio } : {}),
3633
+ };
3634
+ child = spawn(resolvedExecutable, resolvedArgs, spawnOptions);
3635
+ // Start the timeout only after the OS has spawned the process so
3636
+ // timeoutMillis measures command runtime, not spawn/setup overhead.
3637
+ child.on('spawn', () => {
3638
+ if (settled || canceling)
3639
+ return;
3640
+ timeout = setTimeout(() => {
3641
+ void cancelWith('timeout', `Process timed out after ${timeoutMillis} milliseconds`);
3642
+ }, timeoutMillis);
3643
+ });
3455
3644
  child.stdout?.on('data', (data) => { stdout += data; });
3456
3645
  child.stderr?.on('data', (data) => { stderr += data; });
3457
3646
  child.on('error', (err) => {
3647
+ if (settled || canceling)
3648
+ return;
3458
3649
  finish(failure({
3459
3650
  message: err.message,
3460
3651
  binaryPath,
3461
3652
  args,
3462
3653
  stdout,
3463
3654
  stderr,
3655
+ reason: 'spawn',
3464
3656
  }));
3465
3657
  });
3466
3658
  child.on('close', (code) => {
3659
+ if (settled || canceling)
3660
+ return;
3467
3661
  if (code === 0) {
3468
3662
  finish(success({ stdout, stderr }));
3469
3663
  return;
@@ -3475,6 +3669,7 @@ function execBinary(binaryPath, args, timeoutMillis = 1000 * 60 * 10, options) {
3475
3669
  code: code ?? undefined,
3476
3670
  stdout,
3477
3671
  stderr,
3672
+ reason: 'exit',
3478
3673
  }));
3479
3674
  });
3480
3675
  });
@@ -3550,7 +3745,7 @@ async function collectStepsForContext(params) {
3550
3745
  return collectedSteps;
3551
3746
  }
3552
3747
 
3553
- async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables, contextList, gitAddCommandFn, gitCommitCommandFn, gitPushCommandFn, gitCommitMessageFn, projectRoot, steps, setupFn, setupWorkspaceFn, teardownFn, teardownWorkspaceFn, getKeepHistoryFilePathFn, logger: loggerInput, }) {
3748
+ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables, contextList, gitAddCommandFn, gitCommitCommandFn, gitPushCommandFn, gitCommitMessageFn, projectRoot, steps, setupFn, setupWorkspaceFn, teardownFn, teardownWorkspaceFn, getKeepHistoryFilePathFn, logger: loggerInput, signal, }) {
3554
3749
  const logger = loggerInput ?? createConsoleLogger({});
3555
3750
  const contextNames = contextList.map(context => context.name);
3556
3751
  logger.verbose(`contextNames ${JSON.stringify(contextNames)}`);
@@ -3585,180 +3780,232 @@ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables,
3585
3780
  const gitStatusCommand = await execAsync(`git status`, { cwd: workspacePath });
3586
3781
  logger.verbose(`gitStatusCommand ${JSON.stringify(gitStatusCommand.data)}`);
3587
3782
  injectedGitAndWorkspaceFnsInput.workspacePath = workspacePath;
3588
- for (let i = 0; i < contextList.length; i++) {
3589
- const context = contextList[i];
3590
- logger.info(contextList.length > 1
3591
- ? `Running context "${context.name}" (${i + 1}/${contextList.length})`
3592
- : `Running context "${context.name}"`);
3593
- const setupResult = await setupFn({
3594
- contextList,
3595
- lumpVariables,
3596
- currentContextIndex: i,
3597
- });
3598
- const contextRunState = setupResult?.contextRunState || {};
3599
- let stepWalkFailure;
3600
- async function walkAndExecuteSteps(stepsToExec, currStepIndex) {
3601
- for (let stepIndex = 0; stepIndex < stepsToExec.length; stepIndex++) {
3602
- if (stepWalkFailure) {
3603
- return;
3604
- }
3605
- const step = stepsToExec[stepIndex];
3606
- const nextCallHeadIndex = [...currStepIndex, stepIndex];
3607
- const compositeStepIndex = nextCallHeadIndex.length === 1 ? nextCallHeadIndex[0] : nextCallHeadIndex;
3608
- if (typeof step === 'function' || Array.isArray(step)) {
3609
- let subSteps = [];
3610
- if (typeof step === 'function') {
3611
- subSteps = await step({
3783
+ let runFailure;
3784
+ try {
3785
+ for (let i = 0; i < contextList.length; i++) {
3786
+ const context = contextList[i];
3787
+ logger.info(contextList.length > 1
3788
+ ? `Running context "${context.name}" (${i + 1}/${contextList.length})`
3789
+ : `Running context "${context.name}"`);
3790
+ const setupResult = await setupFn({
3791
+ contextList,
3792
+ lumpVariables,
3793
+ currentContextIndex: i,
3794
+ });
3795
+ const contextRunState = setupResult?.contextRunState || {};
3796
+ let stepWalkFailure;
3797
+ async function walkAndExecuteSteps(stepsToExec, currStepIndex) {
3798
+ for (let stepIndex = 0; stepIndex < stepsToExec.length; stepIndex++) {
3799
+ if (stepWalkFailure) {
3800
+ return;
3801
+ }
3802
+ const step = stepsToExec[stepIndex];
3803
+ const nextCallHeadIndex = [...currStepIndex, stepIndex];
3804
+ const compositeStepIndex = nextCallHeadIndex.length === 1 ? nextCallHeadIndex[0] : nextCallHeadIndex;
3805
+ if (typeof step === 'function' || Array.isArray(step)) {
3806
+ let subSteps = [];
3807
+ if (typeof step === 'function') {
3808
+ subSteps = await step({
3809
+ context,
3810
+ stepIndex: compositeStepIndex,
3811
+ contextRunState,
3812
+ lumpVariables,
3813
+ });
3814
+ }
3815
+ else {
3816
+ subSteps = step;
3817
+ }
3818
+ await walkAndExecuteSteps(subSteps, nextCallHeadIndex);
3819
+ continue;
3820
+ }
3821
+ logger.verbose(`step ${JSON.stringify(step)}`);
3822
+ const { commandFn = () => null, stepVariables, promptFn, postCommandExecFn, continueOnError, timeoutMillis = 1000 * 60 * 30, } = step;
3823
+ const prompt = promptFn
3824
+ ? await promptFn({
3612
3825
  context,
3613
3826
  stepIndex: compositeStepIndex,
3614
3827
  contextRunState,
3615
3828
  lumpVariables,
3616
- });
3617
- }
3618
- else {
3619
- subSteps = step;
3620
- }
3621
- await walkAndExecuteSteps(subSteps, nextCallHeadIndex);
3622
- continue;
3623
- }
3624
- logger.verbose(`step ${JSON.stringify(step)}`);
3625
- const { commandFn = () => null, stepVariables, promptFn, postCommandExecFn, continueOnError, timeoutMillis = 1000 * 60 * 30, } = step;
3626
- const prompt = promptFn
3627
- ? await promptFn({
3829
+ stepVariables,
3830
+ })
3831
+ : '';
3832
+ const command = await commandFn({
3628
3833
  context,
3834
+ prompt,
3629
3835
  stepIndex: compositeStepIndex,
3630
3836
  contextRunState,
3631
3837
  lumpVariables,
3632
3838
  stepVariables,
3633
- })
3634
- : '';
3635
- const command = await commandFn({
3636
- context,
3637
- prompt,
3638
- stepIndex: compositeStepIndex,
3639
- contextRunState,
3640
- lumpVariables,
3641
- stepVariables,
3642
- projectRoot,
3643
- workspacePath,
3644
- });
3645
- let commandResult = '';
3646
- let commandSucceeded = true;
3647
- if (command != null) {
3648
- const { executable, args, env } = command;
3649
- logger.verbose(`command for prompt ${executable} ${args.join(' ')}`);
3650
- if (env != null) {
3651
- logger.verbose(`command env overrides ${JSON.stringify(env)}`);
3652
- }
3653
- logger.verbose(`workspacePath ${workspacePath}`);
3654
- const commandExec = await execBinary(executable, args, timeoutMillis, {
3655
- stdio: ['inherit', 'pipe', 'pipe'],
3656
- cwd: workspacePath,
3657
- ...(env != null ? { env: { ...process.env, ...env } } : {}),
3839
+ projectRoot,
3840
+ workspacePath,
3658
3841
  });
3659
- logger.verbose(`commandExec ${JSON.stringify(commandExec)}`);
3660
- if (!commandExec.success) {
3661
- if (!continueOnError) {
3662
- stepWalkFailure = set(commandExec, ['data', 'message'], `Failed to run the command: ${commandExec.data.message}. Command: ${executable} ${args.join(' ')}`);
3663
- return;
3842
+ let commandResult = '';
3843
+ let commandSucceeded = true;
3844
+ if (command != null) {
3845
+ const { executable, args, env } = command;
3846
+ logger.verbose(`command for prompt ${executable} ${args.join(' ')}`);
3847
+ if (env != null) {
3848
+ logger.verbose(`command env overrides ${JSON.stringify(env)}`);
3849
+ }
3850
+ logger.verbose(`workspacePath ${workspacePath}`);
3851
+ const commandExec = await execBinary({
3852
+ binaryPath: executable,
3853
+ args,
3854
+ timeoutMillis,
3855
+ stdio: ['inherit', 'pipe', 'pipe'],
3856
+ cwd: workspacePath,
3857
+ signal,
3858
+ ...(env != null ? { env: { ...process.env, ...env } } : {}),
3859
+ });
3860
+ logger.verbose(`commandExec ${JSON.stringify(commandExec)}`);
3861
+ if (!commandExec.success) {
3862
+ const aborted = commandExec.data.reason === 'aborted';
3863
+ if (aborted || !continueOnError) {
3864
+ stepWalkFailure = {
3865
+ success: false,
3866
+ data: {
3867
+ message: `Failed to run the command: ${commandExec.data.message}. Command: ${executable} ${args.join(' ')}`,
3868
+ reason: 'stepWalkFailed',
3869
+ },
3870
+ };
3871
+ return;
3872
+ }
3873
+ commandSucceeded = false;
3874
+ commandResult = (commandExec.data.stdout
3875
+ || commandExec.data.stderr
3876
+ || commandExec.data.message
3877
+ || '').toString();
3878
+ logger.verbose(`commandResult ${commandResult}`);
3879
+ }
3880
+ else {
3881
+ commandResult = (commandExec.data.stdout || commandExec.data.stderr || '').toString();
3882
+ logger.verbose(`commandResult ${commandResult}`);
3883
+ }
3884
+ if (commandSucceeded) {
3885
+ const gitStatusAfterCommand = await execAsync(`git status`, { cwd: workspacePath });
3886
+ logger.verbose(`gitStatusCommand ${JSON.stringify(gitStatusAfterCommand.data)}`);
3664
3887
  }
3665
- commandSucceeded = false;
3666
- commandResult = (commandExec.data.stdout
3667
- || commandExec.data.stderr
3668
- || commandExec.data.message
3669
- || '').toString();
3670
- logger.verbose(`commandResult ${commandResult}`);
3671
3888
  }
3672
- else {
3673
- commandResult = (commandExec.data.stdout || commandExec.data.stderr || '').toString();
3674
- logger.verbose(`commandResult ${commandResult}`);
3889
+ const postCommandExecFnInput = {
3890
+ commandResult,
3891
+ commandSucceeded,
3892
+ context,
3893
+ prompt,
3894
+ stepIndex: compositeStepIndex,
3895
+ contextRunState,
3896
+ lumpVariables,
3897
+ stepVariables,
3898
+ projectRoot,
3899
+ };
3900
+ logger.verbose(`context is ${JSON.stringify(context)}`);
3901
+ const keepHistoryFilePath = getKeepHistoryFilePathFn(context) || '';
3902
+ logger.verbose(`keepHistoryFilePath ${keepHistoryFilePath}`);
3903
+ if (!!command && keepHistoryFilePath.length > 0) {
3904
+ const appendResult = await appendHistoryEntry({
3905
+ filePath: keepHistoryFilePath,
3906
+ entry: postCommandExecFnInput,
3907
+ });
3908
+ if (!appendResult.success) {
3909
+ // TODO: sanitize history appending to avoid this warning for certain commands outputs
3910
+ logger.warn(`Failed to append history entry to ${keepHistoryFilePath}: ${appendResult.data}`);
3911
+ }
3675
3912
  }
3676
- if (commandSucceeded) {
3677
- const gitStatusAfterCommand = await execAsync(`git status`, { cwd: workspacePath });
3678
- logger.verbose(`gitStatusCommand ${JSON.stringify(gitStatusAfterCommand.data)}`);
3913
+ if (postCommandExecFn) {
3914
+ const returnedSteps = await postCommandExecFn(postCommandExecFnInput);
3915
+ if (returnedSteps != null && returnedSteps.length > 0) {
3916
+ await walkAndExecuteSteps(returnedSteps, nextCallHeadIndex);
3917
+ }
3679
3918
  }
3680
3919
  }
3681
- const postCommandExecFnInput = {
3682
- commandResult,
3683
- commandSucceeded,
3684
- context,
3685
- prompt,
3686
- stepIndex: compositeStepIndex,
3687
- contextRunState,
3688
- lumpVariables,
3689
- stepVariables,
3690
- projectRoot,
3691
- };
3692
- logger.verbose(`context is ${JSON.stringify(context)}`);
3693
- const keepHistoryFilePath = getKeepHistoryFilePathFn(context) || '';
3694
- logger.verbose(`keepHistoryFilePath ${keepHistoryFilePath}`);
3695
- if (!!command && keepHistoryFilePath.length > 0) {
3696
- const appendResult = await appendHistoryEntry({
3697
- filePath: keepHistoryFilePath,
3698
- entry: postCommandExecFnInput,
3920
+ }
3921
+ try {
3922
+ await walkAndExecuteSteps(steps, []);
3923
+ }
3924
+ finally {
3925
+ try {
3926
+ await teardownFn({
3927
+ lumpVariables,
3928
+ contextList,
3929
+ currentContextIndex: i,
3930
+ contextRunState,
3699
3931
  });
3700
- if (!appendResult.success) {
3701
- // TODO: sanitize history appending to avoid this warning for certain commands outputs
3702
- logger.warn(`Failed to append history entry to ${keepHistoryFilePath}: ${appendResult.data}`);
3703
- }
3704
3932
  }
3705
- if (postCommandExecFn) {
3706
- const returnedSteps = await postCommandExecFn(postCommandExecFnInput);
3707
- if (returnedSteps != null && returnedSteps.length > 0) {
3708
- await walkAndExecuteSteps(returnedSteps, nextCallHeadIndex);
3709
- }
3933
+ catch (error) {
3934
+ const message = error instanceof Error ? error.message : String(error);
3935
+ logger.error(`Failed to run teardownFn: ${message}`);
3710
3936
  }
3711
3937
  }
3938
+ if (stepWalkFailure) {
3939
+ runFailure = stepWalkFailure;
3940
+ break;
3941
+ }
3942
+ const perContextInput = {
3943
+ ...injectedGitAndWorkspaceFnsInput,
3944
+ context,
3945
+ };
3946
+ const gitAddCommand = await execAsync(gitAddCommandFn(perContextInput), { cwd: workspacePath });
3947
+ logger.verbose(`gitAddCommand ${JSON.stringify(gitAddCommand)}`);
3948
+ if (!gitAddCommand.success) {
3949
+ runFailure = {
3950
+ success: false,
3951
+ data: {
3952
+ message: `Failed to add the changes for context ${context.name}: ${gitAddCommand.data.message}`,
3953
+ },
3954
+ };
3955
+ break;
3956
+ }
3957
+ const commitMessage = gitCommitMessageFn({ context, lumpVariables, baseBranch });
3958
+ const commitCommand = await execAsync(gitCommitCommandFn({
3959
+ ...perContextInput,
3960
+ commitMessage,
3961
+ }), { cwd: workspacePath });
3962
+ logger.verbose(`commitCommand ${JSON.stringify(commitCommand)}`);
3963
+ if (!commitCommand.success) {
3964
+ logger.error(formatExecFailureMessage({
3965
+ label: `git commit for context ${context.name}`,
3966
+ failure: commitCommand,
3967
+ }));
3968
+ }
3712
3969
  }
3713
- await walkAndExecuteSteps(steps, []);
3714
- if (stepWalkFailure) {
3715
- return stepWalkFailure;
3716
- }
3717
- await teardownFn({
3718
- lumpVariables,
3719
- contextList,
3720
- currentContextIndex: i,
3721
- contextRunState,
3722
- });
3723
- const perContextInput = {
3724
- ...injectedGitAndWorkspaceFnsInput,
3725
- context,
3726
- };
3727
- const gitAddCommand = await execAsync(gitAddCommandFn(perContextInput), { cwd: workspacePath });
3728
- logger.verbose(`gitAddCommand ${JSON.stringify(gitAddCommand)}`);
3729
- if (!gitAddCommand.success) {
3730
- return set(gitAddCommand, ['data', 'message'], `Failed to add the changes for context ${context.name}: ${gitAddCommand.data.message}`);
3731
- }
3732
- const commitMessage = gitCommitMessageFn({ context, lumpVariables, baseBranch });
3733
- const commitCommand = await execAsync(gitCommitCommandFn({
3734
- ...perContextInput,
3735
- commitMessage,
3736
- }), { cwd: workspacePath });
3737
- logger.verbose(`commitCommand ${JSON.stringify(commitCommand)}`);
3738
- if (!commitCommand.success) {
3739
- logger.error(formatExecFailureMessage({
3740
- label: `git commit for context ${context.name}`,
3741
- failure: commitCommand,
3742
- }));
3970
+ if (!runFailure) {
3971
+ const pushCommand = await execAsync(gitPushCommandFn(injectedGitAndWorkspaceFnsInput), { cwd: workspacePath });
3972
+ logger.verbose(`pushCommand ${JSON.stringify(pushCommand)}`);
3973
+ if (!pushCommand.success) {
3974
+ logger.error(formatExecFailureMessage({
3975
+ label: `git push on branch ${branchName}`,
3976
+ failure: pushCommand,
3977
+ }));
3978
+ }
3743
3979
  }
3744
3980
  }
3745
- const pushCommand = await execAsync(gitPushCommandFn(injectedGitAndWorkspaceFnsInput), { cwd: workspacePath });
3746
- logger.verbose(`pushCommand ${JSON.stringify(pushCommand)}`);
3747
- if (!pushCommand.success) {
3748
- logger.error(formatExecFailureMessage({
3749
- label: `git push on branch ${branchName}`,
3750
- failure: pushCommand,
3751
- }));
3752
- }
3753
- const teardownWorkspaceCommand = await teardownWorkspaceFn(injectedGitAndWorkspaceFnsInput);
3754
- logger.verbose(`teardownWorkspaceCommand ${teardownWorkspaceCommand}`);
3755
- if (teardownWorkspaceCommand) {
3756
- const teardownWorkspaceCommandExec = await execAsync(teardownWorkspaceCommand, { cwd: workspacePath });
3757
- logger.verbose(`teardownWorkspaceCommandExec ${JSON.stringify(teardownWorkspaceCommandExec)}`);
3758
- if (!teardownWorkspaceCommandExec.success) {
3759
- return set(teardownWorkspaceCommandExec, ['data', 'message'], `Failed to teardown the workspace: ${teardownWorkspaceCommandExec.data.message}`);
3981
+ finally {
3982
+ const teardownWorkspaceCommand = await teardownWorkspaceFn(injectedGitAndWorkspaceFnsInput);
3983
+ logger.verbose(`teardownWorkspaceCommand ${teardownWorkspaceCommand}`);
3984
+ if (teardownWorkspaceCommand) {
3985
+ const teardownWorkspaceCommandExec = await execAsync(teardownWorkspaceCommand, { cwd: workspacePath });
3986
+ logger.verbose(`teardownWorkspaceCommandExec ${JSON.stringify(teardownWorkspaceCommandExec)}`);
3987
+ if (!teardownWorkspaceCommandExec.success) {
3988
+ if (runFailure) {
3989
+ logger.error(formatExecFailureMessage({
3990
+ label: 'teardown workspace',
3991
+ failure: teardownWorkspaceCommandExec,
3992
+ }));
3993
+ }
3994
+ else {
3995
+ runFailure = {
3996
+ success: false,
3997
+ data: {
3998
+ message: `Failed to teardown the workspace: ${teardownWorkspaceCommandExec.data.message}`,
3999
+ reason: 'workspaceTeardownFailed',
4000
+ },
4001
+ };
4002
+ }
4003
+ }
3760
4004
  }
3761
4005
  }
4006
+ if (runFailure) {
4007
+ return runFailure;
4008
+ }
3762
4009
  return success({
3763
4010
  branchName,
3764
4011
  contextNames,
@@ -4015,7 +4262,7 @@ const defaultSetupWorkspaceFnWithWorktree = async (input) => {
4015
4262
  };
4016
4263
 
4017
4264
  async function runLump(input) {
4018
- 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;
4265
+ 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;
4019
4266
  const lumpVariables = (lumpVariablesInput ?? {});
4020
4267
  const logger = loggerInput ?? createConsoleLogger({});
4021
4268
  const contextListToDoResult = await getToDoContextList({
@@ -4061,6 +4308,7 @@ async function runLump(input) {
4061
4308
  teardownWorkspaceFn,
4062
4309
  logger,
4063
4310
  getKeepHistoryFilePathFn,
4311
+ signal,
4064
4312
  });
4065
4313
  if (!executeStepsResult.success) {
4066
4314
  return set(executeStepsResult, ['data', 'message'], "Error in runLump: Failed to execute steps for context list. Original Error: " + executeStepsResult.data.message);
@@ -4070,5 +4318,5 @@ async function runLump(input) {
4070
4318
  });
4071
4319
  }
4072
4320
 
4073
- export { appendHistoryEntry, collectStepsForContext, contextStatus, createConsoleLogger, defaultGitAddCommandFn, defaultGitCommitCommandFn, defaultGitCommitMessageFn, defaultGitPushCommandFn, defaultSetupWorkspaceFn, defaultSetupWorkspaceFnWithWorktree, defaultTeardownWorkspaceFn, defaultTeardownWorkspaceFnWithWorktree, execAsync, execBinary, executeStepsForContextList, failure, formatExecFailureMessage, getCodeBasePaths, getContextStatus, getToDoContextList, historyFormatFromPath, parseGitLogHashSubjectLines, pathExists, readHistoryFile, resolveSpawnExecutable, runLump, set, shellSingleQuote, success, validateContextListNames, windowsNpmCmdShimBody, writeHistoryFile };
4321
+ export { appendHistoryEntry, collectStepsForContext, contextStatus, createConsoleLogger, defaultGitAddCommandFn, defaultGitCommitCommandFn, defaultGitCommitMessageFn, defaultGitPushCommandFn, defaultSetupWorkspaceFn, defaultSetupWorkspaceFnWithWorktree, defaultTeardownWorkspaceFn, defaultTeardownWorkspaceFnWithWorktree, execAsync, execBinary, executeStepsForContextList, failure, formatExecFailureMessage, getCodeBasePaths, getContextStatus, getToDoContextList, historyFormatFromPath, isProcessAlive, killProcessTree, nodeErrnoCode, parseGitLogHashSubjectLines, pathExists, readHistoryFile, resolveSpawnExecutable, runLump, set, shellSingleQuote, success, validateContextListNames, windowsNpmCmdShimBody, writeHistoryFile };
4074
4322
  //# sourceMappingURL=index.js.map