@lumpcode/core 0.1.0 → 0.1.1

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.
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) {
@@ -3745,6 +3779,54 @@ async function collectStepsForContext(params) {
3745
3779
  return collectedSteps;
3746
3780
  }
3747
3781
 
3782
+ const ABORT_STEP_WALK_MESSAGE = 'Process aborted';
3783
+ function stepWalkAbortedFailure() {
3784
+ return {
3785
+ success: false,
3786
+ data: {
3787
+ message: ABORT_STEP_WALK_MESSAGE,
3788
+ reason: 'stepWalkFailed',
3789
+ },
3790
+ };
3791
+ }
3792
+ function createStepWalkAbortError() {
3793
+ const error = new Error(ABORT_STEP_WALK_MESSAGE);
3794
+ error.name = 'AbortError';
3795
+ return error;
3796
+ }
3797
+ function isStepWalkAbortError(error) {
3798
+ return (typeof error === 'object'
3799
+ && error !== null
3800
+ && 'name' in error
3801
+ && error.name === 'AbortError');
3802
+ }
3803
+ /**
3804
+ * Await user-hook work, but stop waiting once `signal` aborts so the walk can
3805
+ * unwind (locks/teardown). Does not cancel sync busy-loops on the event loop.
3806
+ */
3807
+ async function awaitUnlessAborted(work, signal) {
3808
+ if (!signal) {
3809
+ return await work;
3810
+ }
3811
+ signal.throwIfAborted();
3812
+ return new Promise((resolve, reject) => {
3813
+ const onAbort = () => {
3814
+ cleanup();
3815
+ reject(createStepWalkAbortError());
3816
+ };
3817
+ const cleanup = () => {
3818
+ signal.removeEventListener('abort', onAbort);
3819
+ };
3820
+ signal.addEventListener('abort', onAbort, { once: true });
3821
+ Promise.resolve(work).then((value) => {
3822
+ cleanup();
3823
+ resolve(value);
3824
+ }, (error) => {
3825
+ cleanup();
3826
+ reject(error);
3827
+ });
3828
+ });
3829
+ }
3748
3830
  async function runOptionalGitCommand(input) {
3749
3831
  const { label, getCommand, cwd, logger } = input;
3750
3832
  try {
@@ -3804,6 +3886,10 @@ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables,
3804
3886
  let runFailure;
3805
3887
  try {
3806
3888
  for (let i = 0; i < contextList.length; i++) {
3889
+ if (signal?.aborted) {
3890
+ runFailure = stepWalkAbortedFailure();
3891
+ break;
3892
+ }
3807
3893
  const context = contextList[i];
3808
3894
  logger.info(contextList.length > 1
3809
3895
  ? `Running context "${context.name}" (${i + 1}/${contextList.length})`
@@ -3816,127 +3902,137 @@ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables,
3816
3902
  const contextRunState = setupResult?.contextRunState || {};
3817
3903
  let stepWalkFailure;
3818
3904
  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({
3905
+ try {
3906
+ for (let stepIndex = 0; stepIndex < stepsToExec.length; stepIndex++) {
3907
+ if (stepWalkFailure) {
3908
+ return;
3909
+ }
3910
+ signal?.throwIfAborted();
3911
+ const step = stepsToExec[stepIndex];
3912
+ const nextCallHeadIndex = [...currStepIndex, stepIndex];
3913
+ const compositeStepIndex = nextCallHeadIndex.length === 1 ? nextCallHeadIndex[0] : nextCallHeadIndex;
3914
+ if (typeof step === 'function' || Array.isArray(step)) {
3915
+ const subSteps = typeof step === 'function'
3916
+ ? await awaitUnlessAborted(step({
3917
+ context,
3918
+ stepIndex: compositeStepIndex,
3919
+ contextRunState,
3920
+ lumpVariables,
3921
+ }), signal)
3922
+ : step;
3923
+ await walkAndExecuteSteps(subSteps, nextCallHeadIndex);
3924
+ continue;
3925
+ }
3926
+ logger.verbose(`step ${JSON.stringify(step)}`);
3927
+ const { commandFn = () => null, stepVariables, promptFn, postCommandExecFn, continueOnError, timeoutMillis = 1000 * 60 * 30, } = step;
3928
+ const prompt = promptFn
3929
+ ? await awaitUnlessAborted(promptFn({
3830
3930
  context,
3831
3931
  stepIndex: compositeStepIndex,
3832
3932
  contextRunState,
3833
3933
  lumpVariables,
3934
+ stepVariables,
3935
+ }), signal)
3936
+ : '';
3937
+ const command = await awaitUnlessAborted(commandFn({
3938
+ context,
3939
+ prompt,
3940
+ stepIndex: compositeStepIndex,
3941
+ contextRunState,
3942
+ lumpVariables,
3943
+ stepVariables,
3944
+ projectRoot,
3945
+ workspacePath,
3946
+ }), signal);
3947
+ let commandResult = '';
3948
+ let commandSucceeded = true;
3949
+ if (command != null) {
3950
+ const { executable, args, env } = command;
3951
+ logger.verbose(`command for prompt ${executable} ${args.join(' ')}`);
3952
+ if (env != null) {
3953
+ logger.verbose(`command env overrides ${JSON.stringify(env)}`);
3954
+ }
3955
+ logger.verbose(`workspacePath ${workspacePath}`);
3956
+ const commandExec = await execBinary({
3957
+ binaryPath: executable,
3958
+ args,
3959
+ timeoutMillis,
3960
+ stdio: ['inherit', 'pipe', 'pipe'],
3961
+ cwd: workspacePath,
3962
+ signal,
3963
+ ...(env != null ? { env: { ...process.env, ...env } } : {}),
3834
3964
  });
3965
+ logger.verbose(`commandExec ${JSON.stringify(commandExec)}`);
3966
+ if (!commandExec.success) {
3967
+ const aborted = commandExec.data.reason === 'aborted';
3968
+ if (aborted || !continueOnError) {
3969
+ stepWalkFailure = {
3970
+ success: false,
3971
+ data: {
3972
+ message: `Failed to run the command: ${commandExec.data.message}. Command: ${executable} ${args.join(' ')}`,
3973
+ reason: 'stepWalkFailed',
3974
+ },
3975
+ };
3976
+ return;
3977
+ }
3978
+ commandSucceeded = false;
3979
+ commandResult = (commandExec.data.stdout
3980
+ || commandExec.data.stderr
3981
+ || commandExec.data.message
3982
+ || '').toString();
3983
+ logger.verbose(`commandResult ${commandResult}`);
3984
+ }
3985
+ else {
3986
+ commandResult = (commandExec.data.stdout || commandExec.data.stderr || '').toString();
3987
+ logger.verbose(`commandResult ${commandResult}`);
3988
+ }
3989
+ if (commandSucceeded) {
3990
+ const gitStatusAfterCommand = await execAsync(`git status`, { cwd: workspacePath });
3991
+ logger.verbose(`gitStatusCommand ${JSON.stringify(gitStatusAfterCommand.data)}`);
3992
+ }
3835
3993
  }
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({
3994
+ const historyEntry = {
3995
+ commandResult,
3996
+ commandSucceeded,
3846
3997
  context,
3998
+ prompt,
3847
3999
  stepIndex: compositeStepIndex,
3848
4000
  contextRunState,
3849
4001
  lumpVariables,
3850
4002
  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,
4003
+ projectRoot,
4004
+ };
4005
+ const postCommandExecFnInput = {
4006
+ ...historyEntry,
3878
4007
  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;
4008
+ };
4009
+ logger.verbose(`context is ${JSON.stringify(context)}`);
4010
+ const keepHistoryFilePath = getKeepHistoryFilePathFn(context) || '';
4011
+ logger.verbose(`keepHistoryFilePath ${keepHistoryFilePath}`);
4012
+ if (!!command && keepHistoryFilePath.length > 0) {
4013
+ const appendResult = await appendHistoryEntry({
4014
+ filePath: keepHistoryFilePath,
4015
+ entry: historyEntry,
4016
+ });
4017
+ if (!appendResult.success) {
4018
+ // TODO: sanitize history appending to avoid this warning for certain commands outputs
4019
+ logger.warn(`Failed to append history entry to ${keepHistoryFilePath}: ${appendResult.data}`);
3893
4020
  }
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
4021
  }
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}`);
4022
+ if (postCommandExecFn) {
4023
+ const returnedSteps = await awaitUnlessAborted(postCommandExecFn(postCommandExecFnInput), signal);
4024
+ if (returnedSteps != null && returnedSteps.length > 0) {
4025
+ await walkAndExecuteSteps(returnedSteps, nextCallHeadIndex);
4026
+ }
3932
4027
  }
3933
4028
  }
3934
- if (postCommandExecFn) {
3935
- const returnedSteps = await postCommandExecFn(postCommandExecFnInput);
3936
- if (returnedSteps != null && returnedSteps.length > 0) {
3937
- await walkAndExecuteSteps(returnedSteps, nextCallHeadIndex);
3938
- }
4029
+ }
4030
+ catch (error) {
4031
+ if (isStepWalkAbortError(error)) {
4032
+ stepWalkFailure = stepWalkAbortedFailure();
4033
+ return;
3939
4034
  }
4035
+ throw error;
3940
4036
  }
3941
4037
  }
3942
4038
  try {
@@ -4138,13 +4234,13 @@ async function getContextStatus(params) {
4138
4234
  if (!fetchResult.success)
4139
4235
  return 'toDo';
4140
4236
  }
4141
- const logResult = await execAsync(`git log --remotes=${remoteName} -F --grep=${shellSingleQuote(commitMessage)} --format=${shellSingleQuote('%H %s')}`, { cwd: projectRoot });
4237
+ const logResult = await execAsync(`git log --remotes=${remoteName} -F --grep=${shellSingleQuote(commitMessage)} --format=${shellSingleQuote(GIT_LOG_HASH_BODY_FORMAT)}`, { cwd: projectRoot });
4142
4238
  if (!logResult.success)
4143
4239
  return 'toDo';
4144
4240
  logger?.verbose(`logResult ${JSON.stringify(logResult.data)}`);
4145
4241
  const logResultOutput = logResult.data.stdout || logResult.data.stderr || '';
4146
- const matchingHashes = parseGitLogHashSubjectLines(logResultOutput)
4147
- .filter((entry) => entry.subject === commitMessage)
4242
+ const matchingHashes = parseGitLogHashBodyRecords(logResultOutput)
4243
+ .filter((entry) => commitMessageIncludesMarker(entry.message, commitMessage))
4148
4244
  .map((entry) => entry.hash);
4149
4245
  logger?.verbose(`contextName ${contextName}`);
4150
4246
  logger?.verbose(`remoteName ${remoteName}`);
@@ -4363,5 +4459,5 @@ async function runLump(input) {
4363
4459
  });
4364
4460
  }
4365
4461
 
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 };
4462
+ export { GIT_LOG_HASH_BODY_FORMAT, appendHistoryEntry, collectStepsForContext, commitMessageIncludesMarker, contextStatus, createConsoleLogger, defaultGitAddCommandFn, defaultGitCommitCommandFn, defaultGitCommitMessageFn, defaultGitPushCommandFn, 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
4463
  //# sourceMappingURL=index.js.map