@lumpcode/core 0.1.0 → 0.2.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 (39) hide show
  1. package/README.md +18 -16
  2. package/dist/helpers/execAsync/main.d.ts +14 -9
  3. package/dist/helpers/execAsync/main.d.ts.map +1 -1
  4. package/dist/helpers/executeStepsForContextList/main.d.ts +4 -9
  5. package/dist/helpers/executeStepsForContextList/main.d.ts.map +1 -1
  6. package/dist/helpers/getContextStatus/main.d.ts.map +1 -1
  7. package/dist/index.cjs +303 -177
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.js +299 -175
  10. package/dist/index.js.map +1 -1
  11. package/dist/testing/processTreeTestHelpers.d.ts +3 -0
  12. package/dist/testing/processTreeTestHelpers.d.ts.map +1 -1
  13. package/dist/types/ExecuteStepsFailureReason.d.ts +1 -1
  14. package/dist/types/ExecuteStepsFailureReason.d.ts.map +1 -1
  15. package/dist/types/GitAddCommitFn.d.ts +16 -0
  16. package/dist/types/GitAddCommitFn.d.ts.map +1 -0
  17. package/dist/types/GitPushFn.d.ts +12 -0
  18. package/dist/types/GitPushFn.d.ts.map +1 -0
  19. package/dist/types/PostCommandExecFn.d.ts +2 -0
  20. package/dist/types/PostCommandExecFn.d.ts.map +1 -1
  21. package/dist/types/index.d.ts +2 -3
  22. package/dist/types/index.d.ts.map +1 -1
  23. package/dist/usages/runLump/defaultInjectedFns.d.ts +3 -4
  24. package/dist/usages/runLump/defaultInjectedFns.d.ts.map +1 -1
  25. package/dist/usages/runLump/index.d.ts +1 -1
  26. package/dist/usages/runLump/index.d.ts.map +1 -1
  27. package/dist/usages/runLump/main.d.ts +3 -4
  28. package/dist/usages/runLump/main.d.ts.map +1 -1
  29. package/dist/utils/commitMessageIncludesMarker/index.d.ts +2 -0
  30. package/dist/utils/commitMessageIncludesMarker/index.d.ts.map +1 -0
  31. package/dist/utils/commitMessageIncludesMarker/main.d.ts +3 -0
  32. package/dist/utils/commitMessageIncludesMarker/main.d.ts.map +1 -0
  33. package/dist/utils/index.d.ts +2 -0
  34. package/dist/utils/index.d.ts.map +1 -1
  35. package/dist/utils/parseGitLogHashBodyRecords/index.d.ts +2 -0
  36. package/dist/utils/parseGitLogHashBodyRecords/index.d.ts.map +1 -0
  37. package/dist/utils/parseGitLogHashBodyRecords/main.d.ts +8 -0
  38. package/dist/utils/parseGitLogHashBodyRecords/main.d.ts.map +1 -0
  39. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3403,6 +3403,40 @@ function parseGitLogHashSubjectLines(stdout) {
3403
3403
  });
3404
3404
  }
3405
3405
 
3406
+ const GIT_LOG_HASH_BODY_FORMAT = '%x1e%H%x00%B';
3407
+ const RECORD_SEPARATOR = '\x1e';
3408
+ const HASH_MESSAGE_SEPARATOR = '\x00';
3409
+ /** Parses `git log --format='%x1e%H%x00%B'` stdout into commit hash and full-message pairs. */
3410
+ function parseGitLogHashBodyRecords(stdout) {
3411
+ const records = [];
3412
+ for (const record of stdout.split(RECORD_SEPARATOR)) {
3413
+ if (!record)
3414
+ continue;
3415
+ const nul = record.indexOf(HASH_MESSAGE_SEPARATOR);
3416
+ if (nul === -1)
3417
+ continue;
3418
+ const hash = record.slice(0, nul).trim();
3419
+ if (!hash)
3420
+ continue;
3421
+ records.push({
3422
+ hash,
3423
+ message: record.slice(nul + 1),
3424
+ });
3425
+ }
3426
+ return records;
3427
+ }
3428
+
3429
+ const MARKER_CONTINUE = '[A-Za-z0-9_-]';
3430
+ function escapeRegExp(value) {
3431
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3432
+ }
3433
+ /** True when `marker` occurs in `message` and is not a prefix of a longer `[A-Za-z0-9_-]*` token. */
3434
+ function commitMessageIncludesMarker(message, marker) {
3435
+ if (marker.length === 0)
3436
+ return false;
3437
+ return new RegExp(`${escapeRegExp(marker)}(?!${MARKER_CONTINUE})`).test(message);
3438
+ }
3439
+
3406
3440
  /** Node-style `code` from a caught unknown error (e.g. `ENOENT`), or undefined. */
3407
3441
  function nodeErrnoCode(error) {
3408
3442
  if (error && typeof error === 'object' && 'code' in error) {
@@ -3550,32 +3584,54 @@ async function killProcessTree(input) {
3550
3584
  }
3551
3585
 
3552
3586
  const execAsyncBase = promisify(exec);
3587
+ /** Same signal Node's `exec` `timeout` option uses by default. */
3588
+ const EXEC_TIMEOUT_KILL_SIGNAL = 'SIGTERM';
3589
+ function isExecTimeout(error, timeoutMillis) {
3590
+ if (timeoutMillis === undefined) {
3591
+ return false;
3592
+ }
3593
+ if (typeof error !== 'object' || error === null) {
3594
+ return false;
3595
+ }
3596
+ const execError = error;
3597
+ if (execError.killed !== true) {
3598
+ return false;
3599
+ }
3600
+ // Windows has no POSIX signals; Node force-kills and `signal` is often null.
3601
+ if (process.platform === 'win32') {
3602
+ return true;
3603
+ }
3604
+ return execError.signal === EXEC_TIMEOUT_KILL_SIGNAL;
3605
+ }
3553
3606
  async function execAsync(command, options) {
3554
- const { stdout, stderr, hasErrored } = await execAsyncBase(command, { cwd: options?.cwd })
3555
- .then(result => ({
3556
- stdout: result.stdout,
3557
- stderr: result.stderr,
3558
- hasErrored: false,
3559
- }))
3560
- .catch(e => ({
3561
- stderr: e,
3562
- stdout: e,
3563
- hasErrored: true,
3564
- }));
3565
- if (hasErrored) {
3607
+ const timeoutMillis = options?.timeoutMillis;
3608
+ try {
3609
+ const result = await execAsyncBase(command, {
3610
+ cwd: options?.cwd,
3611
+ ...(timeoutMillis !== undefined
3612
+ ? { timeout: timeoutMillis, killSignal: EXEC_TIMEOUT_KILL_SIGNAL }
3613
+ : {}),
3614
+ });
3615
+ return success({
3616
+ stdout: String(result.stdout),
3617
+ stderr: String(result.stderr),
3618
+ });
3619
+ }
3620
+ catch (e) {
3621
+ const timedOut = isExecTimeout(e, timeoutMillis);
3622
+ const reason = timedOut ? 'timeout' : 'exit';
3566
3623
  return failure({
3567
- message: `Command ${command} failed with error: ${stderr}`,
3624
+ message: timedOut
3625
+ ? `Command ${command} timed out after ${timeoutMillis}ms`
3626
+ : `Command ${command} failed with error: ${e}`,
3627
+ reason,
3568
3628
  info: {
3569
3629
  command,
3570
- stdout,
3571
- stderr,
3630
+ stdout: e,
3631
+ stderr: e,
3572
3632
  },
3573
3633
  });
3574
3634
  }
3575
- return success({
3576
- stdout,
3577
- stderr,
3578
- });
3579
3635
  }
3580
3636
 
3581
3637
  function execBinary(input) {
@@ -3745,28 +3801,85 @@ async function collectStepsForContext(params) {
3745
3801
  return collectedSteps;
3746
3802
  }
3747
3803
 
3748
- async function runOptionalGitCommand(input) {
3749
- const { label, getCommand, cwd, logger } = input;
3804
+ const ABORT_STEP_WALK_MESSAGE = 'Process aborted';
3805
+ function stepWalkAbortedFailure() {
3806
+ return {
3807
+ success: false,
3808
+ data: {
3809
+ message: ABORT_STEP_WALK_MESSAGE,
3810
+ reason: 'stepWalkFailed',
3811
+ },
3812
+ };
3813
+ }
3814
+ function createStepWalkAbortError() {
3815
+ const error = new Error(ABORT_STEP_WALK_MESSAGE);
3816
+ error.name = 'AbortError';
3817
+ return error;
3818
+ }
3819
+ function isStepWalkAbortError(error) {
3820
+ return (typeof error === 'object'
3821
+ && error !== null
3822
+ && 'name' in error
3823
+ && error.name === 'AbortError');
3824
+ }
3825
+ /**
3826
+ * Await user-hook work, but stop waiting once `signal` aborts so the walk can
3827
+ * unwind (locks/teardown). Does not cancel sync busy-loops on the event loop.
3828
+ */
3829
+ async function awaitUnlessAborted(work, signal) {
3830
+ if (!signal) {
3831
+ return await work;
3832
+ }
3833
+ signal.throwIfAborted();
3834
+ return new Promise((resolve, reject) => {
3835
+ const onAbort = () => {
3836
+ cleanup();
3837
+ reject(createStepWalkAbortError());
3838
+ };
3839
+ const cleanup = () => {
3840
+ signal.removeEventListener('abort', onAbort);
3841
+ };
3842
+ signal.addEventListener('abort', onAbort, { once: true });
3843
+ Promise.resolve(work).then((value) => {
3844
+ cleanup();
3845
+ resolve(value);
3846
+ }, (error) => {
3847
+ cleanup();
3848
+ reject(error);
3849
+ });
3850
+ });
3851
+ }
3852
+ /**
3853
+ * Result-aware runner for `gitAddCommitFn` / `gitPushFn`.
3854
+ * nullish Success → no-op; empty string / Failure / throw / exec fail → failed.
3855
+ */
3856
+ async function runGitResultHook(input) {
3857
+ const { getResult, cwd, logger, execLabel } = input;
3750
3858
  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)}`);
3859
+ const result = await getResult();
3757
3860
  if (!result.success) {
3758
- logger.error(formatExecFailureMessage({ label, failure: result }));
3759
- return 'failed';
3861
+ return { status: 'failed', detail: result.data };
3862
+ }
3863
+ const command = result.data;
3864
+ if (command == null) {
3865
+ return { status: 'ok' };
3866
+ }
3867
+ if (command === '') {
3868
+ return { status: 'failed', detail: 'empty command string' };
3760
3869
  }
3761
- return 'ok';
3870
+ const execResult = await execAsync(command, { cwd });
3871
+ logger.verbose(`${execLabel} ${JSON.stringify(execResult)}`);
3872
+ if (!execResult.success) {
3873
+ return { status: 'failed', detail: execResult.data.message };
3874
+ }
3875
+ return { status: 'ok' };
3762
3876
  }
3763
3877
  catch (error) {
3764
3878
  const message = error instanceof Error ? error.message : String(error);
3765
- logger.error(`Failed to run ${label}: ${message}`);
3766
- return 'failed';
3879
+ return { status: 'failed', detail: message };
3767
3880
  }
3768
3881
  }
3769
- async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables, contextList, gitAddCommandFn, gitCommitCommandFn, gitPushCommandFn, gitCommitMessageFn, projectRoot, steps, setupFn, setupWorkspaceFn, teardownFn, teardownWorkspaceFn, getKeepHistoryFilePathFn, logger: loggerInput, signal, }) {
3882
+ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables, contextList, gitAddCommitFn, gitPushFn, gitCommitMessageFn, projectRoot, steps, setupFn, setupWorkspaceFn, teardownFn, teardownWorkspaceFn, getKeepHistoryFilePathFn, logger: loggerInput, signal, }) {
3770
3883
  const logger = loggerInput ?? createConsoleLogger({});
3771
3884
  const contextNames = contextList.map(context => context.name);
3772
3885
  logger.verbose(`contextNames ${JSON.stringify(contextNames)}`);
@@ -3792,7 +3905,9 @@ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables,
3792
3905
  });
3793
3906
  logger.verbose(`setupWorkspaceCommandExec ${JSON.stringify(setupWorkspaceCommandExec)}`);
3794
3907
  if (!setupWorkspaceCommandExec.success) {
3795
- return set(setupWorkspaceCommandExec, ['data', 'message'], `Failed to setup the workspace: ${setupWorkspaceCommandExec.data.message}`);
3908
+ return failure({
3909
+ message: `Failed to setup the workspace: ${setupWorkspaceCommandExec.data.message}`,
3910
+ });
3796
3911
  }
3797
3912
  if (afterExec) {
3798
3913
  await afterExec({ workspacePath });
@@ -3804,6 +3919,10 @@ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables,
3804
3919
  let runFailure;
3805
3920
  try {
3806
3921
  for (let i = 0; i < contextList.length; i++) {
3922
+ if (signal?.aborted) {
3923
+ runFailure = stepWalkAbortedFailure();
3924
+ break;
3925
+ }
3807
3926
  const context = contextList[i];
3808
3927
  logger.info(contextList.length > 1
3809
3928
  ? `Running context "${context.name}" (${i + 1}/${contextList.length})`
@@ -3816,127 +3935,137 @@ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables,
3816
3935
  const contextRunState = setupResult?.contextRunState || {};
3817
3936
  let stepWalkFailure;
3818
3937
  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({
3938
+ try {
3939
+ for (let stepIndex = 0; stepIndex < stepsToExec.length; stepIndex++) {
3940
+ if (stepWalkFailure) {
3941
+ return;
3942
+ }
3943
+ signal?.throwIfAborted();
3944
+ const step = stepsToExec[stepIndex];
3945
+ const nextCallHeadIndex = [...currStepIndex, stepIndex];
3946
+ const compositeStepIndex = nextCallHeadIndex.length === 1 ? nextCallHeadIndex[0] : nextCallHeadIndex;
3947
+ if (typeof step === 'function' || Array.isArray(step)) {
3948
+ const subSteps = typeof step === 'function'
3949
+ ? await awaitUnlessAborted(step({
3950
+ context,
3951
+ stepIndex: compositeStepIndex,
3952
+ contextRunState,
3953
+ lumpVariables,
3954
+ }), signal)
3955
+ : step;
3956
+ await walkAndExecuteSteps(subSteps, nextCallHeadIndex);
3957
+ continue;
3958
+ }
3959
+ logger.verbose(`step ${JSON.stringify(step)}`);
3960
+ const { commandFn = () => null, stepVariables, promptFn, postCommandExecFn, continueOnError, timeoutMillis = 1000 * 60 * 30, } = step;
3961
+ const prompt = promptFn
3962
+ ? await awaitUnlessAborted(promptFn({
3830
3963
  context,
3831
3964
  stepIndex: compositeStepIndex,
3832
3965
  contextRunState,
3833
3966
  lumpVariables,
3967
+ stepVariables,
3968
+ }), signal)
3969
+ : '';
3970
+ const command = await awaitUnlessAborted(commandFn({
3971
+ context,
3972
+ prompt,
3973
+ stepIndex: compositeStepIndex,
3974
+ contextRunState,
3975
+ lumpVariables,
3976
+ stepVariables,
3977
+ projectRoot,
3978
+ workspacePath,
3979
+ }), signal);
3980
+ let commandResult = '';
3981
+ let commandSucceeded = true;
3982
+ if (command != null) {
3983
+ const { executable, args, env } = command;
3984
+ logger.verbose(`command for prompt ${executable} ${args.join(' ')}`);
3985
+ if (env != null) {
3986
+ logger.verbose(`command env overrides ${JSON.stringify(env)}`);
3987
+ }
3988
+ logger.verbose(`workspacePath ${workspacePath}`);
3989
+ const commandExec = await execBinary({
3990
+ binaryPath: executable,
3991
+ args,
3992
+ timeoutMillis,
3993
+ stdio: ['inherit', 'pipe', 'pipe'],
3994
+ cwd: workspacePath,
3995
+ signal,
3996
+ ...(env != null ? { env: { ...process.env, ...env } } : {}),
3834
3997
  });
3998
+ logger.verbose(`commandExec ${JSON.stringify(commandExec)}`);
3999
+ if (!commandExec.success) {
4000
+ const aborted = commandExec.data.reason === 'aborted';
4001
+ if (aborted || !continueOnError) {
4002
+ stepWalkFailure = {
4003
+ success: false,
4004
+ data: {
4005
+ message: `Failed to run the command: ${commandExec.data.message}. Command: ${executable} ${args.join(' ')}`,
4006
+ reason: 'stepWalkFailed',
4007
+ },
4008
+ };
4009
+ return;
4010
+ }
4011
+ commandSucceeded = false;
4012
+ commandResult = (commandExec.data.stdout
4013
+ || commandExec.data.stderr
4014
+ || commandExec.data.message
4015
+ || '').toString();
4016
+ logger.verbose(`commandResult ${commandResult}`);
4017
+ }
4018
+ else {
4019
+ commandResult = (commandExec.data.stdout || commandExec.data.stderr || '').toString();
4020
+ logger.verbose(`commandResult ${commandResult}`);
4021
+ }
4022
+ if (commandSucceeded) {
4023
+ const gitStatusAfterCommand = await execAsync(`git status`, { cwd: workspacePath });
4024
+ logger.verbose(`gitStatusCommand ${JSON.stringify(gitStatusAfterCommand.data)}`);
4025
+ }
3835
4026
  }
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({
4027
+ const historyEntry = {
4028
+ commandResult,
4029
+ commandSucceeded,
3846
4030
  context,
4031
+ prompt,
3847
4032
  stepIndex: compositeStepIndex,
3848
4033
  contextRunState,
3849
4034
  lumpVariables,
3850
4035
  stepVariables,
3851
- })
3852
- : '';
3853
- const command = await commandFn({
3854
- context,
3855
- prompt,
3856
- stepIndex: compositeStepIndex,
3857
- contextRunState,
3858
- lumpVariables,
3859
- stepVariables,
3860
- projectRoot,
3861
- workspacePath,
3862
- });
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,
4036
+ projectRoot,
4037
+ };
4038
+ const postCommandExecFnInput = {
4039
+ ...historyEntry,
3878
4040
  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;
4041
+ };
4042
+ logger.verbose(`context is ${JSON.stringify(context)}`);
4043
+ const keepHistoryFilePath = getKeepHistoryFilePathFn(context) || '';
4044
+ logger.verbose(`keepHistoryFilePath ${keepHistoryFilePath}`);
4045
+ if (!!command && keepHistoryFilePath.length > 0) {
4046
+ const appendResult = await appendHistoryEntry({
4047
+ filePath: keepHistoryFilePath,
4048
+ entry: historyEntry,
4049
+ });
4050
+ if (!appendResult.success) {
4051
+ // TODO: sanitize history appending to avoid this warning for certain commands outputs
4052
+ logger.warn(`Failed to append history entry to ${keepHistoryFilePath}: ${appendResult.data}`);
3893
4053
  }
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)}`);
3908
4054
  }
3909
- }
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}`);
4055
+ if (postCommandExecFn) {
4056
+ const returnedSteps = await awaitUnlessAborted(postCommandExecFn(postCommandExecFnInput), signal);
4057
+ if (returnedSteps != null && returnedSteps.length > 0) {
4058
+ await walkAndExecuteSteps(returnedSteps, nextCallHeadIndex);
4059
+ }
3932
4060
  }
3933
4061
  }
3934
- if (postCommandExecFn) {
3935
- const returnedSteps = await postCommandExecFn(postCommandExecFnInput);
3936
- if (returnedSteps != null && returnedSteps.length > 0) {
3937
- await walkAndExecuteSteps(returnedSteps, nextCallHeadIndex);
3938
- }
4062
+ }
4063
+ catch (error) {
4064
+ if (isStepWalkAbortError(error)) {
4065
+ stepWalkFailure = stepWalkAbortedFailure();
4066
+ return;
3939
4067
  }
4068
+ throw error;
3940
4069
  }
3941
4070
  }
3942
4071
  try {
@@ -3960,43 +4089,42 @@ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables,
3960
4089
  runFailure = stepWalkFailure;
3961
4090
  break;
3962
4091
  }
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),
4092
+ const commitMessage = gitCommitMessageFn({ context, lumpVariables, baseBranch });
4093
+ const addCommitExecLabel = `git add+commit for context ${context.name}`;
4094
+ const addCommitOutcome = await runGitResultHook({
4095
+ getResult: () => gitAddCommitFn({
4096
+ ...injectedGitAndWorkspaceFnsInput,
4097
+ context,
4098
+ commitMessage,
4099
+ }),
3970
4100
  cwd: workspacePath,
3971
4101
  logger,
4102
+ execLabel: addCommitExecLabel,
3972
4103
  });
3973
- if (addOutcome === 'failed') {
4104
+ if (addCommitOutcome.status === 'failed') {
4105
+ const message = `Failed to add and commit for context ${context.name}: ${addCommitOutcome.detail}`;
4106
+ logger.error(message);
3974
4107
  runFailure = {
3975
4108
  success: false,
3976
4109
  data: {
3977
- message: `Failed to add the changes for context ${context.name}`,
4110
+ message,
4111
+ reason: 'gitAddCommitFailed',
3978
4112
  },
3979
4113
  };
3980
4114
  break;
3981
4115
  }
3982
- const commitMessage = gitCommitMessageFn({ context, lumpVariables, baseBranch });
3983
- await runOptionalGitCommand({
3984
- label: `git commit for context ${context.name}`,
3985
- getCommand: () => gitCommitCommandFn({
3986
- ...perContextInput,
3987
- commitMessage,
3988
- }),
3989
- cwd: workspacePath,
3990
- logger,
3991
- });
3992
4116
  }
3993
4117
  if (!runFailure) {
3994
- await runOptionalGitCommand({
3995
- label: `git push on branch ${branchName}`,
3996
- getCommand: () => gitPushCommandFn(injectedGitAndWorkspaceFnsInput),
4118
+ const pushExecLabel = `git push on branch ${branchName}`;
4119
+ const pushOutcome = await runGitResultHook({
4120
+ getResult: () => gitPushFn(injectedGitAndWorkspaceFnsInput),
3997
4121
  cwd: workspacePath,
3998
4122
  logger,
4123
+ execLabel: pushExecLabel,
3999
4124
  });
4125
+ if (pushOutcome.status === 'failed') {
4126
+ logger.error(`Failed to ${pushExecLabel}: ${pushOutcome.detail}`);
4127
+ }
4000
4128
  }
4001
4129
  }
4002
4130
  finally {
@@ -4138,13 +4266,13 @@ async function getContextStatus(params) {
4138
4266
  if (!fetchResult.success)
4139
4267
  return 'toDo';
4140
4268
  }
4141
- const logResult = await execAsync(`git log --remotes=${remoteName} -F --grep=${shellSingleQuote(commitMessage)} --format=${shellSingleQuote('%H %s')}`, { cwd: projectRoot });
4269
+ const logResult = await execAsync(`git log --remotes=${remoteName} -F --grep=${shellSingleQuote(commitMessage)} --format=${shellSingleQuote(GIT_LOG_HASH_BODY_FORMAT)}`, { cwd: projectRoot });
4142
4270
  if (!logResult.success)
4143
4271
  return 'toDo';
4144
4272
  logger?.verbose(`logResult ${JSON.stringify(logResult.data)}`);
4145
4273
  const logResultOutput = logResult.data.stdout || logResult.data.stderr || '';
4146
- const matchingHashes = parseGitLogHashSubjectLines(logResultOutput)
4147
- .filter((entry) => entry.subject === commitMessage)
4274
+ const matchingHashes = parseGitLogHashBodyRecords(logResultOutput)
4275
+ .filter((entry) => commitMessageIncludesMarker(entry.message, commitMessage))
4148
4276
  .map((entry) => entry.hash);
4149
4277
  logger?.verbose(`contextName ${contextName}`);
4150
4278
  logger?.verbose(`remoteName ${remoteName}`);
@@ -4261,14 +4389,11 @@ const contextStatus = ['toDo', 'branchPushed', 'finished'];
4261
4389
  const defaultGitCommitMessageFn = ({ context }) => {
4262
4390
  return `LUMP:${context.name}`;
4263
4391
  };
4264
- const defaultGitPushCommandFn = (input) => {
4265
- return `git push origin ${shellSingleQuote(input.branchName)}`;
4266
- };
4267
- const defaultGitAddCommandFn = () => {
4268
- return `git add .`;
4392
+ const defaultGitAddCommitFn = (input) => {
4393
+ return success(`git add . && git commit --allow-empty -m ${shellSingleQuote(input.commitMessage)}`);
4269
4394
  };
4270
- const defaultGitCommitCommandFn = (input) => {
4271
- return `git commit --allow-empty -m ${shellSingleQuote(input.commitMessage)}`;
4395
+ const defaultGitPushFn = (input) => {
4396
+ return success(`git push origin ${shellSingleQuote(input.branchName)}`);
4272
4397
  };
4273
4398
  const defaultSetupWorkspaceFn = async (input) => {
4274
4399
  const { baseBranch, branchName } = input;
@@ -4306,7 +4431,7 @@ const defaultSetupWorkspaceFnWithWorktree = async (input) => {
4306
4431
  };
4307
4432
 
4308
4433
  async function runLump(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;
4434
+ const { baseBranch, branchFn, lumpVariables: lumpVariablesInput, getContextListFn, gitAddCommitFn = defaultGitAddCommitFn, gitCommitMessageFn = defaultGitCommitMessageFn, gitPushFn = defaultGitPushFn, numberOfContextsPerBranch = 1, projectRoot, steps, setupFn = () => ({ contextRunState: {} }), setupWorkspaceFn = defaultSetupWorkspaceFn, teardownFn = () => undefined, teardownWorkspaceFn = defaultTeardownWorkspaceFn, getKeepHistoryFilePathFn = () => undefined, logger: loggerInput, signal, refreshRemoteTrackingRefsFn, } = input;
4310
4435
  const lumpVariables = (lumpVariablesInput ?? {});
4311
4436
  const logger = loggerInput ?? createConsoleLogger({});
4312
4437
  const contextListToDoResult = await getToDoContextList({
@@ -4341,10 +4466,9 @@ async function runLump(input) {
4341
4466
  branchFn: branchFn,
4342
4467
  lumpVariables: lumpVariables,
4343
4468
  contextList: nextContextsForBranchList,
4344
- gitAddCommandFn,
4345
- gitCommitCommandFn,
4469
+ gitAddCommitFn,
4346
4470
  gitCommitMessageFn,
4347
- gitPushCommandFn,
4471
+ gitPushFn,
4348
4472
  projectRoot,
4349
4473
  steps,
4350
4474
  setupFn,
@@ -4363,5 +4487,5 @@ async function runLump(input) {
4363
4487
  });
4364
4488
  }
4365
4489
 
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 };
4490
+ export { GIT_LOG_HASH_BODY_FORMAT, appendHistoryEntry, collectStepsForContext, commitMessageIncludesMarker, contextStatus, createConsoleLogger, defaultGitAddCommitFn, defaultGitCommitMessageFn, defaultGitPushFn, defaultSetupWorkspaceFn, defaultSetupWorkspaceFnWithWorktree, defaultTeardownWorkspaceFn, defaultTeardownWorkspaceFnWithWorktree, execAsync, execBinary, executeStepsForContextList, failure, formatExecFailureMessage, getCodeBasePaths, getContextStatus, getToDoContextList, historyFormatFromPath, isProcessAlive, killProcessTree, nodeErrnoCode, parseGitLogHashBodyRecords, parseGitLogHashSubjectLines, pathExists, readHistoryFile, refreshRemoteTrackingRefs, resolveSpawnExecutable, runLump, set, shellSingleQuote, success, validateContextListNames, windowsNpmCmdShimBody, writeHistoryFile };
4367
4491
  //# sourceMappingURL=index.js.map