git-coco 0.41.1 → 0.42.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.
@@ -17,6 +17,8 @@ import prettyMilliseconds from 'pretty-ms';
17
17
  import { ChatAnthropic } from '@langchain/anthropic';
18
18
  import { ChatOllama } from '@langchain/ollama';
19
19
  import { ChatOpenAI } from '@langchain/openai';
20
+ import * as fs$1 from 'node:fs';
21
+ import * as path$1 from 'node:path';
20
22
  import { StructuredOutputParser, BaseOutputParser, StringOutputParser } from '@langchain/core/output_parsers';
21
23
  import { minimatch } from 'minimatch';
22
24
  import { simpleGit, GitError } from 'simple-git';
@@ -38,9 +40,7 @@ import '@langchain/core/utils/async_caller';
38
40
  import { encoding_for_model } from 'tiktoken';
39
41
  import { spawn, exec, execFile } from 'child_process';
40
42
  import { spawnSync } from 'node:child_process';
41
- import * as fs$1 from 'node:fs';
42
43
  import * as os$1 from 'node:os';
43
- import * as path$1 from 'node:path';
44
44
  import * as crypto from 'node:crypto';
45
45
  import * as readline from 'readline';
46
46
  import readline__default from 'readline';
@@ -53,7 +53,7 @@ import { pathToFileURL } from 'url';
53
53
  /**
54
54
  * Current build version from package.json
55
55
  */
56
- const BUILD_VERSION = "0.41.1";
56
+ const BUILD_VERSION = "0.42.0";
57
57
 
58
58
  const isInteractive = (config) => {
59
59
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -6343,6 +6343,7 @@ function resolveDynamicService(config, task) {
6343
6343
  };
6344
6344
  }
6345
6345
 
6346
+ const benchCalls = [];
6346
6347
  const telemetryByCommand = new Map();
6347
6348
  function estimatePromptTokens(tokenizer, renderedPrompt) {
6348
6349
  if (!tokenizer)
@@ -6354,10 +6355,28 @@ function estimatePromptTokens(tokenizer, renderedPrompt) {
6354
6355
  return undefined;
6355
6356
  }
6356
6357
  }
6358
+ function isBenchModeActive() {
6359
+ return Boolean(process.env.COCO_BENCH && process.env.COCO_BENCH !== '0');
6360
+ }
6361
+ function recordBenchCall(metadata) {
6362
+ if (!isBenchModeActive())
6363
+ return;
6364
+ benchCalls.push({
6365
+ task: metadata.task,
6366
+ command: metadata.command,
6367
+ provider: metadata.provider,
6368
+ model: metadata.model,
6369
+ promptTokens: metadata.promptTokens,
6370
+ elapsedMs: metadata.elapsedMs,
6371
+ inputDocuments: metadata.inputDocuments,
6372
+ inputChunks: metadata.inputChunks,
6373
+ });
6374
+ }
6357
6375
  function logLlmCall(logger, metadata) {
6358
6376
  if (!logger)
6359
6377
  return;
6360
6378
  recordLlmTelemetry(metadata);
6379
+ recordBenchCall(metadata);
6361
6380
  const fields = [
6362
6381
  `task=${metadata.task}`,
6363
6382
  metadata.command ? `command=${metadata.command}` : undefined,
@@ -6750,13 +6769,35 @@ async function getCommitLogRangeDetails(from, to, { noMerges, git }) {
6750
6769
  }
6751
6770
 
6752
6771
  /**
6753
- * Retrieves the name of the current branch.
6772
+ * Retrieve the name of the current branch.
6773
+ *
6774
+ * The first-choice path uses `git rev-parse --abbrev-ref HEAD`, which
6775
+ * returns the active branch on a normal repo. On an initial-commit
6776
+ * repo (fresh `git init` with no commits yet) HEAD does not resolve
6777
+ * and rev-parse fails fatally — but `git symbolic-ref --short HEAD`
6778
+ * still reports the configured initial branch name, so we fall
6779
+ * through to that. Final fallback is an empty string for genuinely
6780
+ * detached / corrupt states; every caller treats that as "no branch
6781
+ * context", which is the right semantics for a no-HEAD repo.
6754
6782
  *
6755
- * @param {GetCurrentBranchName} options - The options for retrieving the branch name.
6756
- * @returns {Promise<string>} - A promise that resolves to the name of the current branch.
6783
+ * Without this resilience, every command that depends on the branch
6784
+ * name (e.g. the post-summary step in `coco commit`) would crash
6785
+ * with `fatal: ambiguous argument 'HEAD'` after the entire diff
6786
+ * pipeline already ran (#844).
6757
6787
  */
6758
6788
  async function getCurrentBranchName({ git }) {
6759
- return await git.revparse(['--abbrev-ref', 'HEAD']);
6789
+ try {
6790
+ return await git.revparse(['--abbrev-ref', 'HEAD']);
6791
+ }
6792
+ catch {
6793
+ try {
6794
+ const ref = await git.raw(['symbolic-ref', '--short', 'HEAD']);
6795
+ return ref.trim();
6796
+ }
6797
+ catch {
6798
+ return '';
6799
+ }
6800
+ }
6760
6801
  }
6761
6802
 
6762
6803
  /**
@@ -15347,6 +15388,13 @@ const LOG_INK_KEY_BINDINGS = [
15347
15388
  description: 'Push the dedicated pull-request action panel for the current branch.',
15348
15389
  contexts: ['normal'],
15349
15390
  },
15391
+ {
15392
+ id: 'navigateConflicts',
15393
+ keys: ['gx'],
15394
+ label: 'conflicts',
15395
+ description: 'Push the conflict resolution helper view (available during merge/rebase/cherry-pick/revert).',
15396
+ contexts: ['normal'],
15397
+ },
15350
15398
  {
15351
15399
  id: 'navigateBack',
15352
15400
  keys: ['<', 'esc'],
@@ -15476,6 +15524,7 @@ const GLOBAL_BINDING_IDS = [
15476
15524
  'navigateStash',
15477
15525
  'navigateWorktrees',
15478
15526
  'navigatePullRequest',
15527
+ 'navigateConflicts',
15479
15528
  'navigateBack',
15480
15529
  ];
15481
15530
  const NORMAL_GLOBAL_HINTS = ['g jump', '< back', '? help', ': cmds', 'q quit'];
@@ -15648,6 +15697,12 @@ function getLogInkFooterHints(options) {
15648
15697
  global: NORMAL_GLOBAL_HINTS,
15649
15698
  };
15650
15699
  }
15700
+ if (options.activeView === 'conflicts') {
15701
+ return {
15702
+ contextual: ['↑/↓ files', 'enter diff', 's stage', 'u theirs', 'U ours', 'o edit', 'C continue*', 'esc back'],
15703
+ global: NORMAL_GLOBAL_HINTS,
15704
+ };
15705
+ }
15651
15706
  return {
15652
15707
  // History view default hints. Mutating ops (`c` cherry-pick, `R`
15653
15708
  // revert, `Z` reset, `i` interactive-rebase) all route through a
@@ -16394,6 +16449,7 @@ function createLogInkState(rows, options = {}) {
16394
16449
  selectedTagIndex: 0,
16395
16450
  selectedStashIndex: 0,
16396
16451
  selectedWorktreeListIndex: 0,
16452
+ selectedConflictFileIndex: 0,
16397
16453
  branchSort: DEFAULT_BRANCH_SORT_MODE,
16398
16454
  tagSort: DEFAULT_TAG_SORT_MODE,
16399
16455
  paletteFilter: '',
@@ -16647,6 +16703,12 @@ function applyLogInkAction(state, action) {
16647
16703
  selectedWorktreeListIndex: clampIndex$1(state.selectedWorktreeListIndex + action.delta, action.count),
16648
16704
  pendingKey: undefined,
16649
16705
  };
16706
+ case 'moveConflictFile':
16707
+ return {
16708
+ ...state,
16709
+ selectedConflictFileIndex: clampIndex$1(state.selectedConflictFileIndex + action.delta, action.count),
16710
+ pendingKey: undefined,
16711
+ };
16650
16712
  case 'cycleBranchSort':
16651
16713
  return {
16652
16714
  ...state,
@@ -17315,6 +17377,8 @@ function getLogInkPaletteExecuteEvents(command, state) {
17315
17377
  return [action({ type: 'pushView', value: 'worktrees' })];
17316
17378
  case 'navigatePullRequest':
17317
17379
  return [action({ type: 'pushView', value: 'pull-request' })];
17380
+ case 'navigateConflicts':
17381
+ return [action({ type: 'pushView', value: 'conflicts' })];
17318
17382
  case 'navigateBack':
17319
17383
  return [action({ type: 'popView' })];
17320
17384
  case 'openSelected': {
@@ -17786,6 +17850,12 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
17786
17850
  action({ type: 'setStatus', value: 'jumped to pull request' }),
17787
17851
  ];
17788
17852
  }
17853
+ if (state.pendingKey === 'g' && inputValue === 'x') {
17854
+ return [
17855
+ action({ type: 'pushView', value: 'conflicts' }),
17856
+ action({ type: 'setStatus', value: 'jumped to conflicts' }),
17857
+ ];
17858
+ }
17789
17859
  // `gH` chord: apply the cursored hunk to the index (`git apply
17790
17860
  // --cached`). Sibling of bare `H` which targets the worktree.
17791
17861
  // Discoverable via the footer hint on diff views and the help
@@ -18095,6 +18165,9 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
18095
18165
  if (isWorktreeActionTarget(state) && context.worktreeListCount) {
18096
18166
  return [action({ type: 'moveWorktreeListEntry', delta: -1, count: context.worktreeListCount })];
18097
18167
  }
18168
+ if (state.activeView === 'conflicts' && context.conflictFileCount) {
18169
+ return [action({ type: 'moveConflictFile', delta: -1, count: context.conflictFileCount })];
18170
+ }
18098
18171
  if (state.activeView === 'history' &&
18099
18172
  state.focus === 'commits' &&
18100
18173
  state.selectedIndex === 0 &&
@@ -18173,6 +18246,9 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
18173
18246
  if (isWorktreeActionTarget(state) && context.worktreeListCount) {
18174
18247
  return [action({ type: 'moveWorktreeListEntry', delta: 1, count: context.worktreeListCount })];
18175
18248
  }
18249
+ if (state.activeView === 'conflicts' && context.conflictFileCount) {
18250
+ return [action({ type: 'moveConflictFile', delta: 1, count: context.conflictFileCount })];
18251
+ }
18176
18252
  return [
18177
18253
  action(state.focus === 'sidebar'
18178
18254
  ? { type: 'nextSidebarTab' }
@@ -18358,6 +18434,11 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
18358
18434
  fileIndex: state.selectedWorktreeFileIndex,
18359
18435
  })];
18360
18436
  }
18437
+ // Enter on a conflict file opens the worktree diff for that file so
18438
+ // the user can inspect the conflict markers in context.
18439
+ if (key.return && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
18440
+ return [{ type: 'runWorkflowAction', id: 'resolve-conflict-open-diff', payload: context.conflictSelectedPath }];
18441
+ }
18361
18442
  // Enter on a branch row checks the branch out. Non-destructive workflow
18362
18443
  // action — no confirmation prompt. Fires from either the dedicated
18363
18444
  // branches view or from the sidebar when the branches tab is focused
@@ -18499,6 +18580,32 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
18499
18580
  if (inputValue === 'o' && state.activeView === 'diff' && state.diffSource === 'stash' && context.stashDiffSelectedPath) {
18500
18581
  return [{ type: 'openFileInEditor', path: context.stashDiffSelectedPath }];
18501
18582
  }
18583
+ // --- Conflicts view per-row handlers ---
18584
+ // `o` opens the conflicted file in $EDITOR for manual resolution.
18585
+ if (inputValue === 'o' && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
18586
+ return [{ type: 'openFileInEditor', path: context.conflictSelectedPath }];
18587
+ }
18588
+ // `s` stages the conflicted file (marks it resolved).
18589
+ if (inputValue === 's' && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
18590
+ return [{ type: 'runWorkflowAction', id: 'resolve-conflict-stage', payload: context.conflictSelectedPath }];
18591
+ }
18592
+ // `u` resolves by keeping theirs (incoming changes).
18593
+ if (inputValue === 'u' && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
18594
+ return [{ type: 'runWorkflowAction', id: 'resolve-conflict-theirs', payload: context.conflictSelectedPath }];
18595
+ }
18596
+ // `U` resolves by keeping ours (current branch).
18597
+ if (inputValue === 'U' && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
18598
+ return [{ type: 'runWorkflowAction', id: 'resolve-conflict-ours', payload: context.conflictSelectedPath }];
18599
+ }
18600
+ // `C` continues the in-progress operation (available when no conflicts remain).
18601
+ if (inputValue === 'C' && state.activeView === 'conflicts' && context.conflictFileCount === 0) {
18602
+ return [{ type: 'runWorkflowAction', id: 'continue-operation' }];
18603
+ }
18604
+ // Always intercept `C` on the conflicts view to prevent fallthrough to
18605
+ // the global `C` (Create PR) binding when conflicts remain.
18606
+ if (inputValue === 'C' && state.activeView === 'conflicts') {
18607
+ return [action({ type: 'setStatus', value: 'Resolve all conflicts before continuing' })];
18608
+ }
18502
18609
  // `c` on a stash diff cherry-picks the file under the cursor —
18503
18610
  // materializes that single path from the stash into the working tree
18504
18611
  // (`git checkout <stashRef> -- <path>`). Routed through the y-confirm
@@ -21795,6 +21902,21 @@ function skipOperation(git, operation) {
21795
21902
  }
21796
21903
  return runAction(() => git.raw(command.args), command.successMessage);
21797
21904
  }
21905
+ function resolveConflictOurs(git, path) {
21906
+ return runAction(async () => {
21907
+ await git.raw(['checkout', '--ours', '--', path]);
21908
+ await git.raw(['add', '--', path]);
21909
+ }, `Resolved ${path} (kept ours)`);
21910
+ }
21911
+ function resolveConflictTheirs(git, path) {
21912
+ return runAction(async () => {
21913
+ await git.raw(['checkout', '--theirs', '--', path]);
21914
+ await git.raw(['add', '--', path]);
21915
+ }, `Resolved ${path} (kept theirs)`);
21916
+ }
21917
+ function stageConflictResolved(git, path) {
21918
+ return runAction(() => git.raw(['add', '--', path]), `Staged ${path} (marked resolved)`);
21919
+ }
21798
21920
 
21799
21921
  function openProviderUrl(repository, target, openUrl = defaultOpenUrlRunner) {
21800
21922
  const url = repository ? buildProviderUrl(repository, target) : undefined;
@@ -25177,6 +25299,51 @@ function LogInkApp(deps) {
25177
25299
  }
25178
25300
  return abortOperation(git, operation);
25179
25301
  },
25302
+ 'resolve-conflict-ours': async () => {
25303
+ const path = payload?.trim();
25304
+ if (!path)
25305
+ return { ok: false, message: 'No conflict file selected' };
25306
+ return resolveConflictOurs(git, path);
25307
+ },
25308
+ 'resolve-conflict-theirs': async () => {
25309
+ const path = payload?.trim();
25310
+ if (!path)
25311
+ return { ok: false, message: 'No conflict file selected' };
25312
+ return resolveConflictTheirs(git, path);
25313
+ },
25314
+ 'resolve-conflict-stage': async () => {
25315
+ const path = payload?.trim();
25316
+ if (!path)
25317
+ return { ok: false, message: 'No conflict file selected' };
25318
+ return stageConflictResolved(git, path);
25319
+ },
25320
+ 'resolve-conflict-open-diff': async () => {
25321
+ // Push the diff view for the conflicted file so the user can
25322
+ // inspect conflict markers in context. We find the file's index
25323
+ // in the worktree file list and navigate to its diff.
25324
+ const path = payload?.trim();
25325
+ if (!path)
25326
+ return { ok: false, message: 'No conflict file selected' };
25327
+ const worktreeFiles = context.worktree?.files || [];
25328
+ const fileIndex = worktreeFiles.findIndex((f) => f.path === path);
25329
+ if (fileIndex >= 0) {
25330
+ dispatch({ type: 'navigateOpenDiffForWorktreeFile', fileIndex });
25331
+ return { ok: true, message: `Viewing diff for ${path}` };
25332
+ }
25333
+ // File not in worktree list (e.g. deleted-by-us) — open in
25334
+ // editor as fallback so the user can still inspect it.
25335
+ return { ok: true, message: `${path} not in worktree diff list` };
25336
+ },
25337
+ 'continue-operation': async () => {
25338
+ const operation = context.operation?.operation;
25339
+ if (!operation || operation === 'none') {
25340
+ return { ok: false, message: 'No git operation in progress' };
25341
+ }
25342
+ if ((context.operation?.conflictedFiles.length ?? 0) > 0) {
25343
+ return { ok: false, message: 'Resolve all conflicts before continuing' };
25344
+ }
25345
+ return continueOperation(git, operation);
25346
+ },
25180
25347
  'open-pr': async () => {
25181
25348
  const repo = context.provider?.repository;
25182
25349
  if (!repo || repo.provider !== 'github' || !repo.owner || !repo.name) {
@@ -25700,6 +25867,14 @@ function LogInkApp(deps) {
25700
25867
  ? selected?.hash
25701
25868
  : undefined,
25702
25869
  worktreeDirty,
25870
+ conflictFileCount: context.operation?.conflictedFiles.length,
25871
+ conflictSelectedPath: (() => {
25872
+ const files = context.operation?.conflictedFiles;
25873
+ if (!files || files.length === 0)
25874
+ return undefined;
25875
+ const clamped = Math.min(state.selectedConflictFileIndex, files.length - 1);
25876
+ return files[clamped]?.path;
25877
+ })(),
25703
25878
  // H / gH need the actual diff text (not just hunk offsets) to
25704
25879
  // slice the cursored hunk into a `git apply` patch. Stash uses
25705
25880
  // the full `git stash show -p` output; commit-diff uses the
@@ -26064,6 +26239,9 @@ function renderMainPanel(h, components, state, context, contextStatus, worktreeD
26064
26239
  if (state.activeView === 'pull-request') {
26065
26240
  return renderPullRequestSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
26066
26241
  }
26242
+ if (state.activeView === 'conflicts') {
26243
+ return renderConflictsSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
26244
+ }
26067
26245
  return renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits);
26068
26246
  }
26069
26247
  function renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits) {
@@ -26247,6 +26425,79 @@ function buildStatusSurfaceRows(groups) {
26247
26425
  }
26248
26426
  return rows;
26249
26427
  }
26428
+ function renderConflictsSurface(h, components, state, context, contextStatus, bodyRows, width, theme) {
26429
+ const { Box, Text } = components;
26430
+ const focused = state.focus === 'commits';
26431
+ const loading = isLogInkContextKeyLoading(contextStatus, 'operation');
26432
+ const operation = context.operation;
26433
+ const conflictedFiles = operation?.conflictedFiles || [];
26434
+ const operationType = operation?.operation || 'none';
26435
+ // If no operation is in progress, show a fallback message.
26436
+ if (!loading && operationType === 'none') {
26437
+ return h(Box, {
26438
+ borderColor: focusBorderColor(theme, focused),
26439
+ borderStyle: theme.borderStyle,
26440
+ flexDirection: 'column',
26441
+ flexShrink: 0,
26442
+ paddingX: 1,
26443
+ width,
26444
+ }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Conflicts', focused)), h(Text, { dimColor: true }, 'no operation in progress')), h(Text, { key: 'conflicts-empty', dimColor: true }, 'No merge, rebase, cherry-pick, or revert in progress.'));
26445
+ }
26446
+ // All conflicts resolved — show the "continue" hint.
26447
+ if (!loading && conflictedFiles.length === 0 && operationType !== 'none') {
26448
+ return h(Box, {
26449
+ borderColor: focusBorderColor(theme, focused),
26450
+ borderStyle: theme.borderStyle,
26451
+ flexDirection: 'column',
26452
+ flexShrink: 0,
26453
+ paddingX: 1,
26454
+ width,
26455
+ }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Conflicts', focused)), h(Text, { dimColor: true }, `${operationType} — all conflicts resolved`)), h(Text, { key: 'conflicts-hint', dimColor: true }, `All conflicts resolved. Press C to continue the ${operationType}, or < to go back.`));
26456
+ }
26457
+ const selected = Math.max(0, Math.min(state.selectedConflictFileIndex, Math.max(0, conflictedFiles.length - 1)));
26458
+ const listRows = Math.max(4, bodyRows - 4);
26459
+ const startIndex = Math.max(0, selected - Math.floor(listRows / 2));
26460
+ const visible = conflictedFiles.slice(startIndex, startIndex + listRows);
26461
+ const remaining = conflictedFiles.length;
26462
+ const headerRight = loading
26463
+ ? 'loading conflicts'
26464
+ : `${operationType} — ${remaining} ${remaining === 1 ? 'conflict' : 'conflicts'} remaining`;
26465
+ const statusLabel = (file) => {
26466
+ const code = `${file.indexStatus}${file.worktreeStatus}`;
26467
+ switch (code) {
26468
+ case 'UU': return 'both modified';
26469
+ case 'AA': return 'added by both';
26470
+ case 'DD': return 'both deleted';
26471
+ case 'AU':
26472
+ case 'UA': return 'added by one';
26473
+ case 'DU': return 'deleted by us';
26474
+ case 'UD': return 'deleted by them';
26475
+ default: return code;
26476
+ }
26477
+ };
26478
+ const lines = loading
26479
+ ? [h(Text, { key: 'conflicts-loading', dimColor: true }, formatLogInkLoading({ resource: 'conflicts' }))]
26480
+ : visible.map((file, offset) => {
26481
+ const index = startIndex + offset;
26482
+ const isSelected = index === selected;
26483
+ const cursor = isSelected ? '>' : ' ';
26484
+ const code = `${file.indexStatus}${file.worktreeStatus}`;
26485
+ const label = statusLabel(file);
26486
+ return h(Text, {
26487
+ key: `conflict-${index}`,
26488
+ bold: isSelected,
26489
+ dimColor: !isSelected,
26490
+ }, truncate$1(`${cursor} ${code} ${file.path} (${label})`, width - 4));
26491
+ });
26492
+ return h(Box, {
26493
+ borderColor: focusBorderColor(theme, focused),
26494
+ borderStyle: theme.borderStyle,
26495
+ flexDirection: 'column',
26496
+ flexShrink: 0,
26497
+ paddingX: 1,
26498
+ width,
26499
+ }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Conflicts', focused)), h(Text, { dimColor: true }, headerRight)), ...lines);
26500
+ }
26250
26501
  function renderStatusSurface(h, components, state, context, contextStatus, bodyRows, width, theme) {
26251
26502
  const { Box, Text } = components;
26252
26503
  const focused = state.focus === 'commits';
package/dist/index.js CHANGED
@@ -16,6 +16,8 @@ var prettyMilliseconds = require('pretty-ms');
16
16
  var anthropic = require('@langchain/anthropic');
17
17
  var ollama = require('@langchain/ollama');
18
18
  var openai = require('@langchain/openai');
19
+ var fs$1 = require('node:fs');
20
+ var path$1 = require('node:path');
19
21
  var output_parsers = require('@langchain/core/output_parsers');
20
22
  var minimatch = require('minimatch');
21
23
  var simpleGit = require('simple-git');
@@ -37,9 +39,7 @@ require('@langchain/core/utils/async_caller');
37
39
  var tiktoken = require('tiktoken');
38
40
  var child_process = require('child_process');
39
41
  var node_child_process = require('node:child_process');
40
- var fs$1 = require('node:fs');
41
42
  var os$1 = require('node:os');
42
- var path$1 = require('node:path');
43
43
  var crypto = require('node:crypto');
44
44
  var readline = require('readline');
45
45
  var util$1 = require('util');
@@ -68,8 +68,8 @@ var ini__namespace = /*#__PURE__*/_interopNamespaceDefault(ini);
68
68
  var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
69
69
  var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
70
70
  var fs__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(fs$1);
71
- var os__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(os$1);
72
71
  var path__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(path$1);
72
+ var os__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(os$1);
73
73
  var crypto__namespace = /*#__PURE__*/_interopNamespaceDefault(crypto);
74
74
  var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline);
75
75
 
@@ -78,7 +78,7 @@ var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline);
78
78
  /**
79
79
  * Current build version from package.json
80
80
  */
81
- const BUILD_VERSION = "0.41.1";
81
+ const BUILD_VERSION = "0.42.0";
82
82
 
83
83
  const isInteractive = (config) => {
84
84
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -6368,6 +6368,7 @@ function resolveDynamicService(config, task) {
6368
6368
  };
6369
6369
  }
6370
6370
 
6371
+ const benchCalls = [];
6371
6372
  const telemetryByCommand = new Map();
6372
6373
  function estimatePromptTokens(tokenizer, renderedPrompt) {
6373
6374
  if (!tokenizer)
@@ -6379,10 +6380,28 @@ function estimatePromptTokens(tokenizer, renderedPrompt) {
6379
6380
  return undefined;
6380
6381
  }
6381
6382
  }
6383
+ function isBenchModeActive() {
6384
+ return Boolean(process.env.COCO_BENCH && process.env.COCO_BENCH !== '0');
6385
+ }
6386
+ function recordBenchCall(metadata) {
6387
+ if (!isBenchModeActive())
6388
+ return;
6389
+ benchCalls.push({
6390
+ task: metadata.task,
6391
+ command: metadata.command,
6392
+ provider: metadata.provider,
6393
+ model: metadata.model,
6394
+ promptTokens: metadata.promptTokens,
6395
+ elapsedMs: metadata.elapsedMs,
6396
+ inputDocuments: metadata.inputDocuments,
6397
+ inputChunks: metadata.inputChunks,
6398
+ });
6399
+ }
6382
6400
  function logLlmCall(logger, metadata) {
6383
6401
  if (!logger)
6384
6402
  return;
6385
6403
  recordLlmTelemetry(metadata);
6404
+ recordBenchCall(metadata);
6386
6405
  const fields = [
6387
6406
  `task=${metadata.task}`,
6388
6407
  metadata.command ? `command=${metadata.command}` : undefined,
@@ -6775,13 +6794,35 @@ async function getCommitLogRangeDetails(from, to, { noMerges, git }) {
6775
6794
  }
6776
6795
 
6777
6796
  /**
6778
- * Retrieves the name of the current branch.
6797
+ * Retrieve the name of the current branch.
6779
6798
  *
6780
- * @param {GetCurrentBranchName} options - The options for retrieving the branch name.
6781
- * @returns {Promise<string>} - A promise that resolves to the name of the current branch.
6799
+ * The first-choice path uses `git rev-parse --abbrev-ref HEAD`, which
6800
+ * returns the active branch on a normal repo. On an initial-commit
6801
+ * repo (fresh `git init` with no commits yet) HEAD does not resolve
6802
+ * and rev-parse fails fatally — but `git symbolic-ref --short HEAD`
6803
+ * still reports the configured initial branch name, so we fall
6804
+ * through to that. Final fallback is an empty string for genuinely
6805
+ * detached / corrupt states; every caller treats that as "no branch
6806
+ * context", which is the right semantics for a no-HEAD repo.
6807
+ *
6808
+ * Without this resilience, every command that depends on the branch
6809
+ * name (e.g. the post-summary step in `coco commit`) would crash
6810
+ * with `fatal: ambiguous argument 'HEAD'` after the entire diff
6811
+ * pipeline already ran (#844).
6782
6812
  */
6783
6813
  async function getCurrentBranchName({ git }) {
6784
- return await git.revparse(['--abbrev-ref', 'HEAD']);
6814
+ try {
6815
+ return await git.revparse(['--abbrev-ref', 'HEAD']);
6816
+ }
6817
+ catch {
6818
+ try {
6819
+ const ref = await git.raw(['symbolic-ref', '--short', 'HEAD']);
6820
+ return ref.trim();
6821
+ }
6822
+ catch {
6823
+ return '';
6824
+ }
6825
+ }
6785
6826
  }
6786
6827
 
6787
6828
  /**
@@ -15372,6 +15413,13 @@ const LOG_INK_KEY_BINDINGS = [
15372
15413
  description: 'Push the dedicated pull-request action panel for the current branch.',
15373
15414
  contexts: ['normal'],
15374
15415
  },
15416
+ {
15417
+ id: 'navigateConflicts',
15418
+ keys: ['gx'],
15419
+ label: 'conflicts',
15420
+ description: 'Push the conflict resolution helper view (available during merge/rebase/cherry-pick/revert).',
15421
+ contexts: ['normal'],
15422
+ },
15375
15423
  {
15376
15424
  id: 'navigateBack',
15377
15425
  keys: ['<', 'esc'],
@@ -15501,6 +15549,7 @@ const GLOBAL_BINDING_IDS = [
15501
15549
  'navigateStash',
15502
15550
  'navigateWorktrees',
15503
15551
  'navigatePullRequest',
15552
+ 'navigateConflicts',
15504
15553
  'navigateBack',
15505
15554
  ];
15506
15555
  const NORMAL_GLOBAL_HINTS = ['g jump', '< back', '? help', ': cmds', 'q quit'];
@@ -15673,6 +15722,12 @@ function getLogInkFooterHints(options) {
15673
15722
  global: NORMAL_GLOBAL_HINTS,
15674
15723
  };
15675
15724
  }
15725
+ if (options.activeView === 'conflicts') {
15726
+ return {
15727
+ contextual: ['↑/↓ files', 'enter diff', 's stage', 'u theirs', 'U ours', 'o edit', 'C continue*', 'esc back'],
15728
+ global: NORMAL_GLOBAL_HINTS,
15729
+ };
15730
+ }
15676
15731
  return {
15677
15732
  // History view default hints. Mutating ops (`c` cherry-pick, `R`
15678
15733
  // revert, `Z` reset, `i` interactive-rebase) all route through a
@@ -16419,6 +16474,7 @@ function createLogInkState(rows, options = {}) {
16419
16474
  selectedTagIndex: 0,
16420
16475
  selectedStashIndex: 0,
16421
16476
  selectedWorktreeListIndex: 0,
16477
+ selectedConflictFileIndex: 0,
16422
16478
  branchSort: DEFAULT_BRANCH_SORT_MODE,
16423
16479
  tagSort: DEFAULT_TAG_SORT_MODE,
16424
16480
  paletteFilter: '',
@@ -16672,6 +16728,12 @@ function applyLogInkAction(state, action) {
16672
16728
  selectedWorktreeListIndex: clampIndex$1(state.selectedWorktreeListIndex + action.delta, action.count),
16673
16729
  pendingKey: undefined,
16674
16730
  };
16731
+ case 'moveConflictFile':
16732
+ return {
16733
+ ...state,
16734
+ selectedConflictFileIndex: clampIndex$1(state.selectedConflictFileIndex + action.delta, action.count),
16735
+ pendingKey: undefined,
16736
+ };
16675
16737
  case 'cycleBranchSort':
16676
16738
  return {
16677
16739
  ...state,
@@ -17340,6 +17402,8 @@ function getLogInkPaletteExecuteEvents(command, state) {
17340
17402
  return [action({ type: 'pushView', value: 'worktrees' })];
17341
17403
  case 'navigatePullRequest':
17342
17404
  return [action({ type: 'pushView', value: 'pull-request' })];
17405
+ case 'navigateConflicts':
17406
+ return [action({ type: 'pushView', value: 'conflicts' })];
17343
17407
  case 'navigateBack':
17344
17408
  return [action({ type: 'popView' })];
17345
17409
  case 'openSelected': {
@@ -17811,6 +17875,12 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
17811
17875
  action({ type: 'setStatus', value: 'jumped to pull request' }),
17812
17876
  ];
17813
17877
  }
17878
+ if (state.pendingKey === 'g' && inputValue === 'x') {
17879
+ return [
17880
+ action({ type: 'pushView', value: 'conflicts' }),
17881
+ action({ type: 'setStatus', value: 'jumped to conflicts' }),
17882
+ ];
17883
+ }
17814
17884
  // `gH` chord: apply the cursored hunk to the index (`git apply
17815
17885
  // --cached`). Sibling of bare `H` which targets the worktree.
17816
17886
  // Discoverable via the footer hint on diff views and the help
@@ -18120,6 +18190,9 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
18120
18190
  if (isWorktreeActionTarget(state) && context.worktreeListCount) {
18121
18191
  return [action({ type: 'moveWorktreeListEntry', delta: -1, count: context.worktreeListCount })];
18122
18192
  }
18193
+ if (state.activeView === 'conflicts' && context.conflictFileCount) {
18194
+ return [action({ type: 'moveConflictFile', delta: -1, count: context.conflictFileCount })];
18195
+ }
18123
18196
  if (state.activeView === 'history' &&
18124
18197
  state.focus === 'commits' &&
18125
18198
  state.selectedIndex === 0 &&
@@ -18198,6 +18271,9 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
18198
18271
  if (isWorktreeActionTarget(state) && context.worktreeListCount) {
18199
18272
  return [action({ type: 'moveWorktreeListEntry', delta: 1, count: context.worktreeListCount })];
18200
18273
  }
18274
+ if (state.activeView === 'conflicts' && context.conflictFileCount) {
18275
+ return [action({ type: 'moveConflictFile', delta: 1, count: context.conflictFileCount })];
18276
+ }
18201
18277
  return [
18202
18278
  action(state.focus === 'sidebar'
18203
18279
  ? { type: 'nextSidebarTab' }
@@ -18383,6 +18459,11 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
18383
18459
  fileIndex: state.selectedWorktreeFileIndex,
18384
18460
  })];
18385
18461
  }
18462
+ // Enter on a conflict file opens the worktree diff for that file so
18463
+ // the user can inspect the conflict markers in context.
18464
+ if (key.return && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
18465
+ return [{ type: 'runWorkflowAction', id: 'resolve-conflict-open-diff', payload: context.conflictSelectedPath }];
18466
+ }
18386
18467
  // Enter on a branch row checks the branch out. Non-destructive workflow
18387
18468
  // action — no confirmation prompt. Fires from either the dedicated
18388
18469
  // branches view or from the sidebar when the branches tab is focused
@@ -18524,6 +18605,32 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
18524
18605
  if (inputValue === 'o' && state.activeView === 'diff' && state.diffSource === 'stash' && context.stashDiffSelectedPath) {
18525
18606
  return [{ type: 'openFileInEditor', path: context.stashDiffSelectedPath }];
18526
18607
  }
18608
+ // --- Conflicts view per-row handlers ---
18609
+ // `o` opens the conflicted file in $EDITOR for manual resolution.
18610
+ if (inputValue === 'o' && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
18611
+ return [{ type: 'openFileInEditor', path: context.conflictSelectedPath }];
18612
+ }
18613
+ // `s` stages the conflicted file (marks it resolved).
18614
+ if (inputValue === 's' && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
18615
+ return [{ type: 'runWorkflowAction', id: 'resolve-conflict-stage', payload: context.conflictSelectedPath }];
18616
+ }
18617
+ // `u` resolves by keeping theirs (incoming changes).
18618
+ if (inputValue === 'u' && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
18619
+ return [{ type: 'runWorkflowAction', id: 'resolve-conflict-theirs', payload: context.conflictSelectedPath }];
18620
+ }
18621
+ // `U` resolves by keeping ours (current branch).
18622
+ if (inputValue === 'U' && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
18623
+ return [{ type: 'runWorkflowAction', id: 'resolve-conflict-ours', payload: context.conflictSelectedPath }];
18624
+ }
18625
+ // `C` continues the in-progress operation (available when no conflicts remain).
18626
+ if (inputValue === 'C' && state.activeView === 'conflicts' && context.conflictFileCount === 0) {
18627
+ return [{ type: 'runWorkflowAction', id: 'continue-operation' }];
18628
+ }
18629
+ // Always intercept `C` on the conflicts view to prevent fallthrough to
18630
+ // the global `C` (Create PR) binding when conflicts remain.
18631
+ if (inputValue === 'C' && state.activeView === 'conflicts') {
18632
+ return [action({ type: 'setStatus', value: 'Resolve all conflicts before continuing' })];
18633
+ }
18527
18634
  // `c` on a stash diff cherry-picks the file under the cursor —
18528
18635
  // materializes that single path from the stash into the working tree
18529
18636
  // (`git checkout <stashRef> -- <path>`). Routed through the y-confirm
@@ -21820,6 +21927,21 @@ function skipOperation(git, operation) {
21820
21927
  }
21821
21928
  return runAction(() => git.raw(command.args), command.successMessage);
21822
21929
  }
21930
+ function resolveConflictOurs(git, path) {
21931
+ return runAction(async () => {
21932
+ await git.raw(['checkout', '--ours', '--', path]);
21933
+ await git.raw(['add', '--', path]);
21934
+ }, `Resolved ${path} (kept ours)`);
21935
+ }
21936
+ function resolveConflictTheirs(git, path) {
21937
+ return runAction(async () => {
21938
+ await git.raw(['checkout', '--theirs', '--', path]);
21939
+ await git.raw(['add', '--', path]);
21940
+ }, `Resolved ${path} (kept theirs)`);
21941
+ }
21942
+ function stageConflictResolved(git, path) {
21943
+ return runAction(() => git.raw(['add', '--', path]), `Staged ${path} (marked resolved)`);
21944
+ }
21823
21945
 
21824
21946
  function openProviderUrl(repository, target, openUrl = defaultOpenUrlRunner) {
21825
21947
  const url = repository ? buildProviderUrl(repository, target) : undefined;
@@ -25202,6 +25324,51 @@ function LogInkApp(deps) {
25202
25324
  }
25203
25325
  return abortOperation(git, operation);
25204
25326
  },
25327
+ 'resolve-conflict-ours': async () => {
25328
+ const path = payload?.trim();
25329
+ if (!path)
25330
+ return { ok: false, message: 'No conflict file selected' };
25331
+ return resolveConflictOurs(git, path);
25332
+ },
25333
+ 'resolve-conflict-theirs': async () => {
25334
+ const path = payload?.trim();
25335
+ if (!path)
25336
+ return { ok: false, message: 'No conflict file selected' };
25337
+ return resolveConflictTheirs(git, path);
25338
+ },
25339
+ 'resolve-conflict-stage': async () => {
25340
+ const path = payload?.trim();
25341
+ if (!path)
25342
+ return { ok: false, message: 'No conflict file selected' };
25343
+ return stageConflictResolved(git, path);
25344
+ },
25345
+ 'resolve-conflict-open-diff': async () => {
25346
+ // Push the diff view for the conflicted file so the user can
25347
+ // inspect conflict markers in context. We find the file's index
25348
+ // in the worktree file list and navigate to its diff.
25349
+ const path = payload?.trim();
25350
+ if (!path)
25351
+ return { ok: false, message: 'No conflict file selected' };
25352
+ const worktreeFiles = context.worktree?.files || [];
25353
+ const fileIndex = worktreeFiles.findIndex((f) => f.path === path);
25354
+ if (fileIndex >= 0) {
25355
+ dispatch({ type: 'navigateOpenDiffForWorktreeFile', fileIndex });
25356
+ return { ok: true, message: `Viewing diff for ${path}` };
25357
+ }
25358
+ // File not in worktree list (e.g. deleted-by-us) — open in
25359
+ // editor as fallback so the user can still inspect it.
25360
+ return { ok: true, message: `${path} not in worktree diff list` };
25361
+ },
25362
+ 'continue-operation': async () => {
25363
+ const operation = context.operation?.operation;
25364
+ if (!operation || operation === 'none') {
25365
+ return { ok: false, message: 'No git operation in progress' };
25366
+ }
25367
+ if ((context.operation?.conflictedFiles.length ?? 0) > 0) {
25368
+ return { ok: false, message: 'Resolve all conflicts before continuing' };
25369
+ }
25370
+ return continueOperation(git, operation);
25371
+ },
25205
25372
  'open-pr': async () => {
25206
25373
  const repo = context.provider?.repository;
25207
25374
  if (!repo || repo.provider !== 'github' || !repo.owner || !repo.name) {
@@ -25725,6 +25892,14 @@ function LogInkApp(deps) {
25725
25892
  ? selected?.hash
25726
25893
  : undefined,
25727
25894
  worktreeDirty,
25895
+ conflictFileCount: context.operation?.conflictedFiles.length,
25896
+ conflictSelectedPath: (() => {
25897
+ const files = context.operation?.conflictedFiles;
25898
+ if (!files || files.length === 0)
25899
+ return undefined;
25900
+ const clamped = Math.min(state.selectedConflictFileIndex, files.length - 1);
25901
+ return files[clamped]?.path;
25902
+ })(),
25728
25903
  // H / gH need the actual diff text (not just hunk offsets) to
25729
25904
  // slice the cursored hunk into a `git apply` patch. Stash uses
25730
25905
  // the full `git stash show -p` output; commit-diff uses the
@@ -26089,6 +26264,9 @@ function renderMainPanel(h, components, state, context, contextStatus, worktreeD
26089
26264
  if (state.activeView === 'pull-request') {
26090
26265
  return renderPullRequestSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
26091
26266
  }
26267
+ if (state.activeView === 'conflicts') {
26268
+ return renderConflictsSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
26269
+ }
26092
26270
  return renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits);
26093
26271
  }
26094
26272
  function renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits) {
@@ -26272,6 +26450,79 @@ function buildStatusSurfaceRows(groups) {
26272
26450
  }
26273
26451
  return rows;
26274
26452
  }
26453
+ function renderConflictsSurface(h, components, state, context, contextStatus, bodyRows, width, theme) {
26454
+ const { Box, Text } = components;
26455
+ const focused = state.focus === 'commits';
26456
+ const loading = isLogInkContextKeyLoading(contextStatus, 'operation');
26457
+ const operation = context.operation;
26458
+ const conflictedFiles = operation?.conflictedFiles || [];
26459
+ const operationType = operation?.operation || 'none';
26460
+ // If no operation is in progress, show a fallback message.
26461
+ if (!loading && operationType === 'none') {
26462
+ return h(Box, {
26463
+ borderColor: focusBorderColor(theme, focused),
26464
+ borderStyle: theme.borderStyle,
26465
+ flexDirection: 'column',
26466
+ flexShrink: 0,
26467
+ paddingX: 1,
26468
+ width,
26469
+ }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Conflicts', focused)), h(Text, { dimColor: true }, 'no operation in progress')), h(Text, { key: 'conflicts-empty', dimColor: true }, 'No merge, rebase, cherry-pick, or revert in progress.'));
26470
+ }
26471
+ // All conflicts resolved — show the "continue" hint.
26472
+ if (!loading && conflictedFiles.length === 0 && operationType !== 'none') {
26473
+ return h(Box, {
26474
+ borderColor: focusBorderColor(theme, focused),
26475
+ borderStyle: theme.borderStyle,
26476
+ flexDirection: 'column',
26477
+ flexShrink: 0,
26478
+ paddingX: 1,
26479
+ width,
26480
+ }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Conflicts', focused)), h(Text, { dimColor: true }, `${operationType} — all conflicts resolved`)), h(Text, { key: 'conflicts-hint', dimColor: true }, `All conflicts resolved. Press C to continue the ${operationType}, or < to go back.`));
26481
+ }
26482
+ const selected = Math.max(0, Math.min(state.selectedConflictFileIndex, Math.max(0, conflictedFiles.length - 1)));
26483
+ const listRows = Math.max(4, bodyRows - 4);
26484
+ const startIndex = Math.max(0, selected - Math.floor(listRows / 2));
26485
+ const visible = conflictedFiles.slice(startIndex, startIndex + listRows);
26486
+ const remaining = conflictedFiles.length;
26487
+ const headerRight = loading
26488
+ ? 'loading conflicts'
26489
+ : `${operationType} — ${remaining} ${remaining === 1 ? 'conflict' : 'conflicts'} remaining`;
26490
+ const statusLabel = (file) => {
26491
+ const code = `${file.indexStatus}${file.worktreeStatus}`;
26492
+ switch (code) {
26493
+ case 'UU': return 'both modified';
26494
+ case 'AA': return 'added by both';
26495
+ case 'DD': return 'both deleted';
26496
+ case 'AU':
26497
+ case 'UA': return 'added by one';
26498
+ case 'DU': return 'deleted by us';
26499
+ case 'UD': return 'deleted by them';
26500
+ default: return code;
26501
+ }
26502
+ };
26503
+ const lines = loading
26504
+ ? [h(Text, { key: 'conflicts-loading', dimColor: true }, formatLogInkLoading({ resource: 'conflicts' }))]
26505
+ : visible.map((file, offset) => {
26506
+ const index = startIndex + offset;
26507
+ const isSelected = index === selected;
26508
+ const cursor = isSelected ? '>' : ' ';
26509
+ const code = `${file.indexStatus}${file.worktreeStatus}`;
26510
+ const label = statusLabel(file);
26511
+ return h(Text, {
26512
+ key: `conflict-${index}`,
26513
+ bold: isSelected,
26514
+ dimColor: !isSelected,
26515
+ }, truncate$1(`${cursor} ${code} ${file.path} (${label})`, width - 4));
26516
+ });
26517
+ return h(Box, {
26518
+ borderColor: focusBorderColor(theme, focused),
26519
+ borderStyle: theme.borderStyle,
26520
+ flexDirection: 'column',
26521
+ flexShrink: 0,
26522
+ paddingX: 1,
26523
+ width,
26524
+ }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Conflicts', focused)), h(Text, { dimColor: true }, headerRight)), ...lines);
26525
+ }
26275
26526
  function renderStatusSurface(h, components, state, context, contextStatus, bodyRows, width, theme) {
26276
26527
  const { Box, Text } = components;
26277
26528
  const focused = state.focus === 'commits';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-coco",
3
- "version": "0.41.1",
3
+ "version": "0.42.0",
4
4
  "description": "zero-effort git commits with coco.",
5
5
  "author": "gfargo <ghfargo@gmail.com>",
6
6
  "license": "MIT",
@@ -39,6 +39,7 @@
39
39
  "test": "npm run test:jest && npm run test:publish",
40
40
  "test:publish": "npm run lint && npm run build && npm run test:cli && npm pack --dry-run",
41
41
  "test:cli": "tsx bin/smokeCli.ts",
42
+ "bench": "tsx bin/benchmark.ts",
42
43
  "pretest:jest": "npm run build:info",
43
44
  "test:jest": "jest",
44
45
  "test:jest:watch": "jest --watch",