@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.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,28 @@ 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 runOptionalGitCommand(input) {
3749
+ const { label, getCommand, cwd, logger } = input;
3750
+ try {
3751
+ const command = await getCommand();
3752
+ if (command == null || command === '') {
3753
+ return 'ok';
3754
+ }
3755
+ const result = await execAsync(command, { cwd });
3756
+ logger.verbose(`${label} ${JSON.stringify(result)}`);
3757
+ if (!result.success) {
3758
+ logger.error(formatExecFailureMessage({ label, failure: result }));
3759
+ return 'failed';
3760
+ }
3761
+ return 'ok';
3762
+ }
3763
+ catch (error) {
3764
+ const message = error instanceof Error ? error.message : String(error);
3765
+ logger.error(`Failed to run ${label}: ${message}`);
3766
+ return 'failed';
3767
+ }
3768
+ }
3769
+ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables, contextList, gitAddCommandFn, gitCommitCommandFn, gitPushCommandFn, gitCommitMessageFn, projectRoot, steps, setupFn, setupWorkspaceFn, teardownFn, teardownWorkspaceFn, getKeepHistoryFilePathFn, logger: loggerInput, signal, }) {
3554
3770
  const logger = loggerInput ?? createConsoleLogger({});
3555
3771
  const contextNames = contextList.map(context => context.name);
3556
3772
  logger.verbose(`contextNames ${JSON.stringify(contextNames)}`);
@@ -3585,180 +3801,232 @@ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables,
3585
3801
  const gitStatusCommand = await execAsync(`git status`, { cwd: workspacePath });
3586
3802
  logger.verbose(`gitStatusCommand ${JSON.stringify(gitStatusCommand.data)}`);
3587
3803
  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({
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({
3612
3846
  context,
3613
3847
  stepIndex: compositeStepIndex,
3614
3848
  contextRunState,
3615
3849
  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({
3850
+ stepVariables,
3851
+ })
3852
+ : '';
3853
+ const command = await commandFn({
3628
3854
  context,
3855
+ prompt,
3629
3856
  stepIndex: compositeStepIndex,
3630
3857
  contextRunState,
3631
3858
  lumpVariables,
3632
3859
  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 } } : {}),
3860
+ projectRoot,
3861
+ workspacePath,
3658
3862
  });
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;
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)}`);
3664
3908
  }
3665
- commandSucceeded = false;
3666
- commandResult = (commandExec.data.stdout
3667
- || commandExec.data.stderr
3668
- || commandExec.data.message
3669
- || '').toString();
3670
- logger.verbose(`commandResult ${commandResult}`);
3671
3909
  }
3672
- else {
3673
- commandResult = (commandExec.data.stdout || commandExec.data.stderr || '').toString();
3674
- 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
+ }
3675
3933
  }
3676
- if (commandSucceeded) {
3677
- const gitStatusAfterCommand = await execAsync(`git status`, { cwd: workspacePath });
3678
- 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
+ }
3679
3939
  }
3680
3940
  }
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,
3941
+ }
3942
+ try {
3943
+ await walkAndExecuteSteps(steps, []);
3944
+ }
3945
+ finally {
3946
+ try {
3947
+ await teardownFn({
3948
+ lumpVariables,
3949
+ contextList,
3950
+ currentContextIndex: i,
3951
+ contextRunState,
3699
3952
  });
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
3953
  }
3705
- if (postCommandExecFn) {
3706
- const returnedSteps = await postCommandExecFn(postCommandExecFnInput);
3707
- if (returnedSteps != null && returnedSteps.length > 0) {
3708
- await walkAndExecuteSteps(returnedSteps, nextCallHeadIndex);
3709
- }
3954
+ catch (error) {
3955
+ const message = error instanceof Error ? error.message : String(error);
3956
+ logger.error(`Failed to run teardownFn: ${message}`);
3710
3957
  }
3711
3958
  }
3712
- }
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({
3959
+ if (stepWalkFailure) {
3960
+ runFailure = stepWalkFailure;
3961
+ break;
3962
+ }
3963
+ const perContextInput = {
3964
+ ...injectedGitAndWorkspaceFnsInput,
3965
+ context,
3966
+ };
3967
+ const addOutcome = await runOptionalGitCommand({
3968
+ label: `git add for context ${context.name}`,
3969
+ getCommand: () => gitAddCommandFn(perContextInput),
3970
+ cwd: workspacePath,
3971
+ logger,
3972
+ });
3973
+ if (addOutcome === 'failed') {
3974
+ runFailure = {
3975
+ success: false,
3976
+ data: {
3977
+ message: `Failed to add the changes for context ${context.name}`,
3978
+ },
3979
+ };
3980
+ break;
3981
+ }
3982
+ const commitMessage = gitCommitMessageFn({ context, lumpVariables, baseBranch });
3983
+ await runOptionalGitCommand({
3740
3984
  label: `git commit for context ${context.name}`,
3741
- failure: commitCommand,
3742
- }));
3985
+ getCommand: () => gitCommitCommandFn({
3986
+ ...perContextInput,
3987
+ commitMessage,
3988
+ }),
3989
+ cwd: workspacePath,
3990
+ logger,
3991
+ });
3992
+ }
3993
+ if (!runFailure) {
3994
+ await runOptionalGitCommand({
3995
+ label: `git push on branch ${branchName}`,
3996
+ getCommand: () => gitPushCommandFn(injectedGitAndWorkspaceFnsInput),
3997
+ cwd: workspacePath,
3998
+ logger,
3999
+ });
3743
4000
  }
3744
4001
  }
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}`);
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
+ }
3760
4025
  }
3761
4026
  }
4027
+ if (runFailure) {
4028
+ return runFailure;
4029
+ }
3762
4030
  return success({
3763
4031
  branchName,
3764
4032
  contextNames,
@@ -3844,17 +4112,32 @@ async function scanDirectory({ dirPath, allPaths, gitignoresMap, projectRoot, lo
3844
4112
  }
3845
4113
  }
3846
4114
 
4115
+ /**
4116
+ * Refreshes remote-tracking refs for status / planning (one network fetch).
4117
+ * Uses `--no-write-fetch-head` so concurrent preflight/pull paths are safer.
4118
+ */
4119
+ const refreshRemoteTrackingRefs = async (input) => {
4120
+ const { projectRoot, remoteName = 'origin' } = input;
4121
+ const result = await execAsync(`git fetch --prune --no-write-fetch-head ${remoteName}`, { cwd: projectRoot });
4122
+ if (!result.success) {
4123
+ return failure(result.data.message);
4124
+ }
4125
+ return success(undefined);
4126
+ };
4127
+
3847
4128
  async function getContextStatus(params) {
3848
- const { contextName, gitCommitMessageFn, projectRoot, baseBranch, lumpVariables: lumpVariablesInput, contextVariables = {}, remoteName = "origin", logger, } = params;
4129
+ const { contextName, gitCommitMessageFn, projectRoot, baseBranch, lumpVariables: lumpVariablesInput, contextVariables = {}, remoteName = "origin", logger, skipFetch = false, } = params;
3849
4130
  const lumpVariables = (lumpVariablesInput ?? {});
3850
4131
  const commitMessage = gitCommitMessageFn({
3851
4132
  context: { name: contextName, variables: contextVariables },
3852
4133
  lumpVariables,
3853
4134
  baseBranch,
3854
4135
  });
3855
- const fetchAll = await execAsync(`git fetch --all --prune`, { cwd: projectRoot });
3856
- if (!fetchAll.success)
3857
- return 'toDo';
4136
+ if (!skipFetch) {
4137
+ const fetchResult = await refreshRemoteTrackingRefs({ projectRoot, remoteName });
4138
+ if (!fetchResult.success)
4139
+ return 'toDo';
4140
+ }
3858
4141
  const logResult = await execAsync(`git log --remotes=${remoteName} -F --grep=${shellSingleQuote(commitMessage)} --format=${shellSingleQuote('%H %s')}`, { cwd: projectRoot });
3859
4142
  if (!logResult.success)
3860
4143
  return 'toDo';
@@ -3863,6 +4146,7 @@ async function getContextStatus(params) {
3863
4146
  const matchingHashes = parseGitLogHashSubjectLines(logResultOutput)
3864
4147
  .filter((entry) => entry.subject === commitMessage)
3865
4148
  .map((entry) => entry.hash);
4149
+ logger?.verbose(`contextName ${contextName}`);
3866
4150
  logger?.verbose(`remoteName ${remoteName}`);
3867
4151
  logger?.verbose(`baseBranch ${baseBranch}`);
3868
4152
  logger?.verbose(`commitMessage ${commitMessage}`);
@@ -3917,7 +4201,7 @@ function validateContextListNames(contextList) {
3917
4201
  }
3918
4202
 
3919
4203
  async function getToDoContextList(params) {
3920
- const { getContextListFn, lumpVariables, gitCommitMessageFn, projectRoot, baseBranch, logger } = params;
4204
+ const { getContextListFn, lumpVariables, gitCommitMessageFn, projectRoot, baseBranch, logger, refreshRemoteTrackingRefsFn = refreshRemoteTrackingRefs, } = params;
3921
4205
  const codeBasePathsResult = await getCodeBasePaths({ cwd: projectRoot, logger });
3922
4206
  if (!codeBasePathsResult.success) {
3923
4207
  return failure({
@@ -3935,21 +4219,28 @@ async function getToDoContextList(params) {
3935
4219
  const allCtxNames = contextList.flatMap(context => [context.name, ...(context.options?.dependsOnContexts ?? [])]);
3936
4220
  const allCtxNamesSet = new Set(allCtxNames);
3937
4221
  const allCtxNamesList = Array.from(allCtxNamesSet);
3938
- const contextStatusList = await Promise.all(allCtxNamesList.map((contextName) => {
3939
- return getContextStatus({
3940
- contextName: contextName,
4222
+ const refreshResult = await refreshRemoteTrackingRefsFn({ projectRoot });
4223
+ let contextStatusMap;
4224
+ if (!refreshResult.success) {
4225
+ logger?.warn(`Failed to refresh remote-tracking refs for context status; treating contexts as toDo: ${refreshResult.data}`);
4226
+ contextStatusMap = new Map(allCtxNamesList.map((name) => [name, 'toDo']));
4227
+ }
4228
+ else {
4229
+ const contextStatusList = await Promise.all(allCtxNamesList.map((contextName) => getContextStatus({
4230
+ contextName,
3941
4231
  contextVariables: {}, // TODO: Remove contextVariables from getContextStatus, really not needed
3942
4232
  gitCommitMessageFn,
3943
4233
  lumpVariables,
3944
4234
  projectRoot,
3945
4235
  baseBranch,
3946
4236
  logger,
3947
- });
3948
- }));
3949
- const contextStatusMap = new Map(allCtxNamesList.map((contextName, i) => [contextName, contextStatusList[i]]));
4237
+ skipFetch: true,
4238
+ })));
4239
+ contextStatusMap = new Map(allCtxNamesList.map((contextName, i) => [contextName, contextStatusList[i]]));
4240
+ }
3950
4241
  const contextListToDo = contextList
3951
- .filter((context, contextIndex) => {
3952
- const contextStatus = contextStatusList[contextIndex];
4242
+ .filter((context) => {
4243
+ const contextStatus = contextStatusMap.get(context.name);
3953
4244
  if (contextStatus && contextStatus !== 'toDo')
3954
4245
  return false;
3955
4246
  const deps = context.options?.dependsOnContexts;
@@ -4015,7 +4306,7 @@ const defaultSetupWorkspaceFnWithWorktree = async (input) => {
4015
4306
  };
4016
4307
 
4017
4308
  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;
4309
+ 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;
4019
4310
  const lumpVariables = (lumpVariablesInput ?? {});
4020
4311
  const logger = loggerInput ?? createConsoleLogger({});
4021
4312
  const contextListToDoResult = await getToDoContextList({
@@ -4025,6 +4316,7 @@ async function runLump(input) {
4025
4316
  baseBranch,
4026
4317
  gitCommitMessageFn,
4027
4318
  logger,
4319
+ refreshRemoteTrackingRefsFn,
4028
4320
  });
4029
4321
  if (!contextListToDoResult.success) {
4030
4322
  return set(contextListToDoResult, ['data', 'message'], "Error in runLump: Failed to get to do context list. Original Error: " + contextListToDoResult.data.message);
@@ -4061,6 +4353,7 @@ async function runLump(input) {
4061
4353
  teardownWorkspaceFn,
4062
4354
  logger,
4063
4355
  getKeepHistoryFilePathFn,
4356
+ signal,
4064
4357
  });
4065
4358
  if (!executeStepsResult.success) {
4066
4359
  return set(executeStepsResult, ['data', 'message'], "Error in runLump: Failed to execute steps for context list. Original Error: " + executeStepsResult.data.message);
@@ -4070,5 +4363,5 @@ async function runLump(input) {
4070
4363
  });
4071
4364
  }
4072
4365
 
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 };
4366
+ 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, refreshRemoteTrackingRefs, resolveSpawnExecutable, runLump, set, shellSingleQuote, success, validateContextListNames, windowsNpmCmdShimBody, writeHistoryFile };
4074
4367
  //# sourceMappingURL=index.js.map