@oh-my-pi/pi-coding-agent 15.7.6 → 15.8.2

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 (117) hide show
  1. package/CHANGELOG.md +172 -198
  2. package/dist/types/async/job-manager.d.ts +3 -3
  3. package/dist/types/cli/args.d.ts +1 -0
  4. package/dist/types/cli/claude-trace-cli.d.ts +54 -0
  5. package/dist/types/cli/session-picker.d.ts +10 -3
  6. package/dist/types/cli/update-cli.d.ts +17 -0
  7. package/dist/types/commands/launch.d.ts +3 -0
  8. package/dist/types/config/keybindings.d.ts +10 -1
  9. package/dist/types/config/settings-schema.d.ts +2 -2
  10. package/dist/types/config/settings.d.ts +13 -0
  11. package/dist/types/edit/index.d.ts +6 -0
  12. package/dist/types/edit/streaming.d.ts +8 -0
  13. package/dist/types/eval/concurrency-bridge.d.ts +26 -0
  14. package/dist/types/eval/js/tool-bridge.d.ts +2 -1
  15. package/dist/types/export/ttsr.d.ts +9 -0
  16. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +11 -0
  17. package/dist/types/main.d.ts +5 -0
  18. package/dist/types/mcp/transports/stdio.d.ts +19 -0
  19. package/dist/types/modes/components/custom-editor.d.ts +3 -1
  20. package/dist/types/modes/components/hook-selector.d.ts +3 -0
  21. package/dist/types/modes/components/session-selector.d.ts +32 -5
  22. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  23. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -2
  24. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  25. package/dist/types/modes/interactive-mode.d.ts +9 -2
  26. package/dist/types/modes/types.d.ts +4 -2
  27. package/dist/types/registry/agent-registry.d.ts +1 -1
  28. package/dist/types/sdk.d.ts +2 -2
  29. package/dist/types/session/agent-session.d.ts +4 -2
  30. package/dist/types/session/history-storage.d.ts +16 -1
  31. package/dist/types/session/session-manager.d.ts +4 -0
  32. package/dist/types/task/output-manager.d.ts +6 -15
  33. package/dist/types/tools/find.d.ts +0 -9
  34. package/dist/types/tools/index.d.ts +1 -1
  35. package/dist/types/tools/path-utils.d.ts +16 -0
  36. package/dist/types/tools/sqlite-reader.d.ts +25 -8
  37. package/dist/types/tools/write.d.ts +2 -0
  38. package/dist/types/utils/clipboard.d.ts +4 -0
  39. package/dist/types/utils/jj.d.ts +49 -0
  40. package/dist/types/web/kagi.d.ts +76 -0
  41. package/dist/types/web/search/providers/exa.d.ts +7 -1
  42. package/dist/types/web/search/providers/kagi.d.ts +1 -0
  43. package/package.json +9 -9
  44. package/src/async/job-manager.ts +3 -3
  45. package/src/cli/args.ts +6 -2
  46. package/src/cli/claude-trace-cli.ts +783 -0
  47. package/src/cli/session-picker.ts +36 -10
  48. package/src/cli/update-cli.ts +35 -2
  49. package/src/commands/launch.ts +3 -0
  50. package/src/config/keybindings.ts +14 -1
  51. package/src/config/model-registry.ts +18 -7
  52. package/src/config/settings-schema.ts +2 -2
  53. package/src/config/settings.ts +23 -0
  54. package/src/discovery/builtin-rules/index.ts +2 -0
  55. package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +44 -0
  56. package/src/discovery/claude-plugins.ts +7 -9
  57. package/src/edit/index.ts +10 -0
  58. package/src/edit/streaming.ts +65 -0
  59. package/src/eval/__tests__/agent-bridge.test.ts +58 -4
  60. package/src/eval/concurrency-bridge.ts +34 -0
  61. package/src/eval/js/shared/prelude.txt +20 -17
  62. package/src/eval/js/tool-bridge.ts +5 -0
  63. package/src/eval/py/prelude.py +23 -15
  64. package/src/export/ttsr.ts +18 -1
  65. package/src/extensibility/custom-commands/bundled/review/index.ts +74 -45
  66. package/src/extensibility/plugins/legacy-pi-compat.ts +115 -131
  67. package/src/extensibility/skills.ts +0 -1
  68. package/src/internal-urls/docs-index.generated.ts +12 -11
  69. package/src/main.ts +92 -24
  70. package/src/mcp/transports/stdio.ts +55 -22
  71. package/src/modes/acp/acp-event-mapper.ts +54 -4
  72. package/src/modes/components/custom-editor.ts +10 -0
  73. package/src/modes/components/hook-selector.ts +89 -31
  74. package/src/modes/components/oauth-selector.ts +12 -6
  75. package/src/modes/components/session-selector.ts +179 -24
  76. package/src/modes/components/tool-execution.ts +16 -3
  77. package/src/modes/controllers/command-controller.ts +2 -11
  78. package/src/modes/controllers/extension-ui-controller.ts +3 -2
  79. package/src/modes/controllers/input-controller.ts +19 -1
  80. package/src/modes/controllers/selector-controller.ts +61 -21
  81. package/src/modes/interactive-mode.ts +125 -15
  82. package/src/modes/types.ts +5 -2
  83. package/src/prompts/agents/reviewer.md +2 -2
  84. package/src/prompts/review-request.md +1 -1
  85. package/src/prompts/system/empty-stop-retry.md +6 -0
  86. package/src/prompts/system/orchestrate-notice.md +5 -3
  87. package/src/prompts/system/workflow-notice.md +2 -2
  88. package/src/prompts/tools/eval.md +5 -5
  89. package/src/prompts/tools/find.md +1 -1
  90. package/src/prompts/tools/irc.md +6 -6
  91. package/src/prompts/tools/search-tool-bm25.md +9 -2
  92. package/src/prompts/tools/search.md +1 -1
  93. package/src/prompts/tools/task.md +1 -1
  94. package/src/registry/agent-registry.ts +1 -1
  95. package/src/sdk.ts +85 -31
  96. package/src/session/agent-session.ts +209 -54
  97. package/src/session/history-storage.ts +56 -12
  98. package/src/session/session-manager.ts +34 -0
  99. package/src/task/output-manager.ts +40 -48
  100. package/src/task/render.ts +3 -8
  101. package/src/tools/browser/tab-worker.ts +8 -5
  102. package/src/tools/find.ts +5 -29
  103. package/src/tools/index.ts +1 -1
  104. package/src/tools/path-utils.ts +144 -1
  105. package/src/tools/read.ts +47 -0
  106. package/src/tools/search-tool-bm25.ts +7 -1
  107. package/src/tools/search.ts +2 -27
  108. package/src/tools/sqlite-reader.ts +92 -9
  109. package/src/tools/write.ts +6 -0
  110. package/src/utils/clipboard.ts +38 -1
  111. package/src/utils/git.ts +19 -23
  112. package/src/utils/jj.ts +225 -0
  113. package/src/utils/open.ts +37 -2
  114. package/src/web/kagi.ts +168 -49
  115. package/src/web/search/providers/anthropic.ts +1 -1
  116. package/src/web/search/providers/exa.ts +20 -86
  117. package/src/web/search/providers/kagi.ts +4 -0
package/src/tools/read.ts CHANGED
@@ -69,6 +69,7 @@ import {
69
69
  type LineRange,
70
70
  parseLineRanges,
71
71
  resolveReadPath,
72
+ splitDelimitedPathEntry,
72
73
  splitInternalUrlSel,
73
74
  splitPathAndSel,
74
75
  } from "./path-utils";
@@ -693,6 +694,50 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
693
694
  });
694
695
  }
695
696
 
697
+ async #tryReadDelimitedPaths(
698
+ readPath: string,
699
+ signal?: AbortSignal,
700
+ ): Promise<AgentToolResult<ReadToolDetails> | null> {
701
+ const parts = await splitDelimitedPathEntry(readPath, this.session.cwd);
702
+ if (!parts) return null;
703
+
704
+ const notice = `Note: interpreted as ${parts.length} paths: ${parts.join(", ")}`;
705
+ const notes = [notice];
706
+ const content: Array<TextContent | ImageContent> = [];
707
+ let pendingText = notice;
708
+ const flushText = () => {
709
+ if (pendingText.length === 0) return;
710
+ content.push({ type: "text", text: pendingText });
711
+ pendingText = "";
712
+ };
713
+ const appendText = (text: string) => {
714
+ pendingText = pendingText.length > 0 ? `${pendingText}\n\n${text}` : text;
715
+ };
716
+
717
+ for (const part of parts) {
718
+ try {
719
+ const result = await this.execute("read-delimited-part", { path: part }, signal);
720
+ for (const block of result.content) {
721
+ if (block.type === "text") {
722
+ appendText(block.text);
723
+ continue;
724
+ }
725
+ flushText();
726
+ content.push(block);
727
+ }
728
+ } catch (error) {
729
+ if (error instanceof ToolAbortError || signal?.aborted) throw error;
730
+ const message = error instanceof Error ? error.message : String(error);
731
+ const errorNote = `Could not read ${part}: ${message}`;
732
+ notes.push(errorNote);
733
+ appendText(`[${errorNote}]`);
734
+ }
735
+ }
736
+ flushText();
737
+
738
+ return toolResult<ReadToolDetails>({ notes }).content(content).done();
739
+ }
740
+
696
741
  async #resolveArchiveReadPath(readPath: string, signal?: AbortSignal): Promise<ResolvedArchiveReadPath | null> {
697
742
  const candidates = parseArchivePathCandidates(readPath);
698
743
  for (const candidate of candidates) {
@@ -1596,6 +1641,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1596
1641
  }
1597
1642
 
1598
1643
  if (!suffixResolution) {
1644
+ const delimitedResult = await this.#tryReadDelimitedPaths(readPath, signal);
1645
+ if (delimitedResult) return delimitedResult;
1599
1646
  throw new ToolError(`Path '${localReadPath}' not found`);
1600
1647
  }
1601
1648
  } else {
@@ -10,6 +10,7 @@ import {
10
10
  buildDiscoverableToolSearchIndex,
11
11
  type DiscoverableTool,
12
12
  type DiscoverableToolSearchIndex,
13
+ filterBySource,
13
14
  formatDiscoverableToolServerSummary,
14
15
  searchDiscoverableTools,
15
16
  summarizeDiscoverableTools,
@@ -141,10 +142,15 @@ function isDiscoveryEnabled(session: ToolSession): boolean {
141
142
 
142
143
  export function renderSearchToolBm25Description(discoverableTools: DiscoverableTool[] = []): string {
143
144
  const summary = summarizeDiscoverableTools(discoverableTools);
145
+ const builtinToolNames = filterBySource(discoverableTools, "builtin")
146
+ .map(t => t.name)
147
+ .sort();
144
148
  return prompt.render(searchToolBm25Description, {
145
- discoverableMCPToolCount: summary.toolCount,
149
+ discoverableToolCount: summary.toolCount,
146
150
  discoverableMCPServerSummaries: summary.servers.map(formatDiscoverableToolServerSummary),
147
151
  hasDiscoverableMCPServers: summary.servers.length > 0,
152
+ discoverableBuiltinToolNames: builtinToolNames,
153
+ hasDiscoverableBuiltinTools: builtinToolNames.length > 0,
148
154
  });
149
155
  }
150
156
 
@@ -30,6 +30,7 @@ import { formatGroupedFiles } from "./grouped-file-output";
30
30
  import { formatMatchLine } from "./match-line-format";
31
31
  import { formatFullOutputReference, type OutputMeta } from "./output-meta";
32
32
  import {
33
+ expandDelimitedPathEntries,
33
34
  hasGlobPathChars,
34
35
  isLineInRanges,
35
36
  type LineRange,
@@ -93,29 +94,6 @@ export const SINGLE_FILE_MATCHES = 200;
93
94
  * pagination headroom so the caller can see total file count. */
94
95
  const INTERNAL_TOTAL_CAP = 2000;
95
96
 
96
- /**
97
- * Detect a `,` that is not inside a `{…}` brace expansion. Used to catch
98
- * `paths: ["a,b"]` mistakes where the caller flattened multiple entries
99
- * into a single string instead of passing a JSON array of strings.
100
- */
101
- function containsTopLevelComma(entry: string): boolean {
102
- let depth = 0;
103
- for (let i = 0; i < entry.length; i++) {
104
- const ch = entry[i];
105
- if (ch === "\\" && i + 1 < entry.length) {
106
- i++;
107
- continue;
108
- }
109
- if (ch === "{") depth++;
110
- else if (ch === "}") {
111
- if (depth > 0) depth--;
112
- } else if (ch === "," && depth === 0) {
113
- return true;
114
- }
115
- }
116
- return false;
117
- }
118
-
119
97
  /**
120
98
  * Parsed `paths` entry — a path (possibly archive-shaped) plus an optional
121
99
  * line-range selector peeled off the trailing `:N-M` (or `:N+K`, `:N,M`, …)
@@ -146,9 +124,6 @@ function parsePathSpecs(rawEntries: readonly string[]): SearchPathSpec[] {
146
124
  clean = split.path;
147
125
  ranges = parsed;
148
126
  }
149
- if (containsTopLevelComma(clean)) {
150
- throw new ToolError('paths is an array — pass ["a", "b"] not ["a,b"]');
151
- }
152
127
  specs.push({ original: entry, clean, ranges });
153
128
  }
154
129
  return specs;
@@ -663,7 +638,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
663
638
  if (normalizedSkip < 0 || !Number.isFinite(normalizedSkip)) {
664
639
  throw new ToolError("Skip must be a non-negative number");
665
640
  }
666
- const rawEntries = toPathList(rawPaths);
641
+ const rawEntries = await expandDelimitedPathEntries(toPathList(rawPaths), this.session.cwd);
667
642
  const pathSpecs = parsePathSpecs(rawEntries);
668
643
  const paths = pathSpecs.map(spec => spec.clean);
669
644
  const {
@@ -12,6 +12,15 @@ const MAX_QUERY_LIMIT = 500;
12
12
  const MAX_RENDER_WIDTH = 120;
13
13
  const MAX_COLUMN_WIDTH = 40;
14
14
  const MIN_COLUMN_WIDTH = 1;
15
+ /**
16
+ * Upper bound on rows scanned when counting a table for the listing. SQLite has
17
+ * no stored row count, so `COUNT(*)` is a full b-tree scan — multi-second on a
18
+ * multi-GB database, and `bun:sqlite` runs it synchronously on the JS thread
19
+ * that also drives the TUI, freezing rendering and input. The listing instead
20
+ * trusts the planner's `sqlite_stat1` estimate for large tables and only counts
21
+ * exactly when a table is provably small, reading at most this many rows.
22
+ */
23
+ const ROW_COUNT_PROBE_CAP = 50_000;
15
24
 
16
25
  type SqliteBinding = Exclude<SQLQueryBindings, Record<string, unknown>>;
17
26
 
@@ -26,6 +35,11 @@ interface SqliteCountRow {
26
35
  count: number;
27
36
  }
28
37
 
38
+ interface SqliteStat1Row {
39
+ tbl: string;
40
+ stat: string | null;
41
+ }
42
+
29
43
  interface SqliteTableInfoRow {
30
44
  cid: number;
31
45
  name: string;
@@ -50,6 +64,23 @@ export type SqliteSelector =
50
64
 
51
65
  export type SqliteRowLookup = { kind: "pk"; column: string; type: string } | { kind: "rowid" };
52
66
 
67
+ /**
68
+ * Row count for a table in the listing.
69
+ * - `exact`: counted in full (the table is small enough to count cheaply).
70
+ * - `estimate`: the planner's `sqlite_stat1` figure; the table is too large to
71
+ * scan, so this may be stale.
72
+ * - `atLeast`: a lower bound; counting was capped before reaching the end.
73
+ */
74
+ export type TableRowCount =
75
+ | { kind: "exact"; rows: number }
76
+ | { kind: "estimate"; rows: number }
77
+ | { kind: "atLeast"; rows: number };
78
+
79
+ export interface SqliteTableSummary {
80
+ name: string;
81
+ count: TableRowCount;
82
+ }
83
+
53
84
  function splitSqliteRemainder(remainder: string): { subPath: string; queryString: string } {
54
85
  const queryIndex = remainder.indexOf("?");
55
86
  if (queryIndex === -1) {
@@ -495,20 +526,61 @@ export function parseSqliteSelector(subPath: string, queryString: string): Sqlit
495
526
  return { kind: "schema", table, sampleLimit: DEFAULT_SCHEMA_SAMPLE_LIMIT };
496
527
  }
497
528
 
498
- export function listTables(db: Database): { name: string; rowCount: number }[] {
529
+ /**
530
+ * Reads the planner's per-table row estimate from `sqlite_stat1` (populated by
531
+ * `ANALYZE`). The first integer of each `stat` string is the number of rows in
532
+ * that index; for a full (non-partial) index it equals the table's row count,
533
+ * so the max across a table's entries is the table estimate. Returns an empty
534
+ * map when the database was never analyzed. One small indexed read — no scan.
535
+ */
536
+ function loadRowEstimates(db: Database): Map<string, number> {
537
+ const estimates = new Map<string, number>();
538
+ const hasStat1 = db
539
+ .prepare<Pick<SqliteMasterRow, "name">, []>(
540
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'sqlite_stat1'",
541
+ )
542
+ .get();
543
+ if (!hasStat1) return estimates;
544
+
545
+ for (const { tbl, stat } of db.prepare<SqliteStat1Row, []>("SELECT tbl, stat FROM sqlite_stat1").all()) {
546
+ if (!stat) continue;
547
+ const rows = Number.parseInt(stat, 10);
548
+ if (!Number.isFinite(rows)) continue;
549
+ const prev = estimates.get(tbl);
550
+ if (prev === undefined || rows > prev) estimates.set(tbl, rows);
551
+ }
552
+ return estimates;
553
+ }
554
+
555
+ /**
556
+ * Counts a table while reading at most `cap + 1` rows. Returns an exact count
557
+ * when the table holds `cap` rows or fewer, otherwise a lower bound of `cap`.
558
+ * Bounds the worst-case scan so a stale or missing estimate can never trigger a
559
+ * full-table scan on the JS thread.
560
+ */
561
+ function probeRowCount(db: Database, table: string, cap: number): TableRowCount {
562
+ const sql = `SELECT COUNT(*) AS count FROM (SELECT 1 FROM ${quoteSqliteIdentifier(table)} LIMIT ${cap + 1})`;
563
+ const counted = db.prepare<SqliteCountRow, []>(sql).get()?.count ?? 0;
564
+ return counted > cap ? { kind: "atLeast", rows: cap } : { kind: "exact", rows: counted };
565
+ }
566
+
567
+ export function listTables(db: Database, options: { probeCap?: number } = {}): SqliteTableSummary[] {
568
+ const cap = options.probeCap ?? ROW_COUNT_PROBE_CAP;
499
569
  const names = db
500
570
  .prepare<Pick<SqliteMasterRow, "name">, []>(
501
571
  "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name COLLATE NOCASE",
502
572
  )
503
573
  .all();
574
+ const estimates = loadRowEstimates(db);
504
575
 
505
576
  return names.map(({ name }) => {
506
- const countRow =
507
- db.prepare<SqliteCountRow, []>(`SELECT COUNT(*) AS count FROM ${quoteSqliteIdentifier(name)}`).get() ?? null;
508
- return {
509
- name,
510
- rowCount: countRow?.count ?? 0,
511
- };
577
+ const estimate = estimates.get(name);
578
+ // Trust the planner only when it says the table is too large to count
579
+ // cheaply; otherwise count exactly (bounded), which also corrects a
580
+ // stale-low estimate without ever scanning more than `cap` rows.
581
+ const count: TableRowCount =
582
+ estimate !== undefined && estimate > cap ? { kind: "estimate", rows: estimate } : probeRowCount(db, name, cap);
583
+ return { name, count };
512
584
  });
513
585
  }
514
586
 
@@ -679,13 +751,24 @@ export function deleteRowByRowId(db: Database, table: string, key: string): numb
679
751
  return statement.run(binding).changes;
680
752
  }
681
753
 
682
- export function renderTableList(tables: { name: string; rowCount: number }[]): string {
754
+ function formatRowCount(count: TableRowCount): string {
755
+ switch (count.kind) {
756
+ case "exact":
757
+ return `${count.rows} rows`;
758
+ case "estimate":
759
+ return `~${count.rows} rows`;
760
+ case "atLeast":
761
+ return `${count.rows}+ rows`;
762
+ }
763
+ }
764
+
765
+ export function renderTableList(tables: SqliteTableSummary[]): string {
683
766
  if (tables.length === 0) {
684
767
  return "(no tables)";
685
768
  }
686
769
 
687
770
  return tables
688
- .map(table => truncateToWidth(replaceTabs(`${table.name} (${table.rowCount} rows)`), MAX_RENDER_WIDTH))
771
+ .map(table => truncateToWidth(replaceTabs(`${table.name} (${formatRowCount(table.count)})`), MAX_RENDER_WIDTH))
689
772
  .join("\n");
690
773
  }
691
774
 
@@ -273,6 +273,12 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
273
273
  readonly loadMode = "discoverable";
274
274
  readonly summary = "Write content to a file (creates or overwrites)";
275
275
 
276
+ /** Stream matchers should see the real file content, not its JSON-escaped argument encoding. */
277
+ matcherDigest(args: unknown): string | undefined {
278
+ const content = (args as Partial<WriteParams>).content;
279
+ return typeof content === "string" ? content : undefined;
280
+ }
281
+
276
282
  readonly #writethrough: WritethroughCallback;
277
283
 
278
284
  constructor(private readonly session: ToolSession) {
@@ -119,7 +119,7 @@ async function readImageViaPowerShell(): Promise<ClipboardImage | null> {
119
119
  if (!b64) return null;
120
120
  const bytes = Buffer.from(b64, "base64");
121
121
  if (bytes.byteLength === 0) return null;
122
- return { data: new Uint8Array(bytes), mimeType: "image/png" };
122
+ return { data: bytes, mimeType: "image/png" };
123
123
  } catch {
124
124
  return null;
125
125
  }
@@ -154,3 +154,40 @@ export async function readImageFromClipboard(): Promise<ClipboardImage | null> {
154
154
 
155
155
  return (await native.readImageFromClipboard()) ?? null;
156
156
  }
157
+
158
+ /**
159
+ * Read plain text from the system clipboard.
160
+ */
161
+ export async function readTextFromClipboard(): Promise<string> {
162
+ try {
163
+ const p = process.platform;
164
+ if (p === "darwin") {
165
+ return execSync("pbpaste", { encoding: "utf8", timeout: 2000 }).toString();
166
+ }
167
+ if (p === "win32") {
168
+ return execSync('powershell.exe -NoProfile -Command "Get-Clipboard"', {
169
+ encoding: "utf8",
170
+ timeout: 2000,
171
+ }).toString();
172
+ }
173
+ if (process.env.TERMUX_VERSION) {
174
+ return execSync("termux-clipboard-get", { encoding: "utf8", timeout: 2000 }).toString();
175
+ }
176
+ const hasWaylandDisplay = Boolean(process.env.WAYLAND_DISPLAY);
177
+ const hasX11Display = Boolean(process.env.DISPLAY);
178
+ if (hasWaylandDisplay) {
179
+ try {
180
+ return execSync("wl-paste --type text/plain --no-newline", { encoding: "utf8", timeout: 2000 }).toString();
181
+ } catch {
182
+ if (hasX11Display) {
183
+ return execSync("xclip -selection clipboard -o", { encoding: "utf8", timeout: 2000 }).toString();
184
+ }
185
+ }
186
+ } else if (hasX11Display) {
187
+ return execSync("xclip -selection clipboard -o", { encoding: "utf8", timeout: 2000 }).toString();
188
+ }
189
+ } catch (error) {
190
+ logger.warn("clipboard: failed to read clipboard text", { error: String(error) });
191
+ }
192
+ return "";
193
+ }
package/src/utils/git.ts CHANGED
@@ -189,11 +189,7 @@ function formatCommandFailure(
189
189
  return `git ${args.join(" ")} failed with exit code ${result.exitCode}`;
190
190
  }
191
191
 
192
- async function runCommand(
193
- cwd: string,
194
- args: readonly string[],
195
- options: CommandOptions = {},
196
- ): Promise<GitCommandResult> {
192
+ async function git(cwd: string, args: readonly string[], options: CommandOptions = {}): Promise<GitCommandResult> {
197
193
  const commandArgs = withShortLivedGitConfig(options.readOnly ? withNoOptionalLocks(args) : [...args]);
198
194
  const child = Bun.spawn(["git", ...commandArgs], {
199
195
  cwd,
@@ -248,7 +244,7 @@ async function runChecked(
248
244
  options: CommandOptions = {},
249
245
  ): Promise<GitCommandResult> {
250
246
  ensureAvailable();
251
- const result = await runCommand(cwd, args, options);
247
+ const result = await git(cwd, args, options);
252
248
  if (result.exitCode !== 0) {
253
249
  throw new GitCommandError(args, result);
254
250
  }
@@ -269,7 +265,7 @@ async function tryText(
269
265
  options: CommandOptions = {},
270
266
  ): Promise<string | undefined> {
271
267
  ensureAvailable();
272
- const result = await runCommand(cwd, args, options);
268
+ const result = await git(cwd, args, options);
273
269
  if (result.exitCode !== 0) return undefined;
274
270
  return result.stdout;
275
271
  }
@@ -709,7 +705,7 @@ export const diff = Object.assign(
709
705
  async function diff(cwd: string, options: DiffOptions = {}): Promise<string> {
710
706
  const args = buildDiffArgs(options);
711
707
  if (options.allowFailure) {
712
- return (await runCommand(cwd, args, { env: options.env, readOnly: true, signal: options.signal })).stdout;
708
+ return (await git(cwd, args, { env: options.env, readOnly: true, signal: options.signal })).stdout;
713
709
  }
714
710
  return runText(cwd, args, { env: options.env, readOnly: true, signal: options.signal });
715
711
  },
@@ -741,7 +737,7 @@ export const diff = Object.assign(
741
737
  if (options.cached) args.push("--cached");
742
738
  args.push("--quiet");
743
739
  if (options.files?.length) args.push("--", ...options.files);
744
- const result = await runCommand(cwd, args, { readOnly: true, signal: options.signal });
740
+ const result = await git(cwd, args, { readOnly: true, signal: options.signal });
745
741
  if (result.exitCode === 0) return false;
746
742
  if (result.exitCode === 1) return true;
747
743
  throw new GitCommandError(args, result);
@@ -757,7 +753,7 @@ export const diff = Object.assign(
757
753
  if (options.binary) args.push("--binary");
758
754
  args.push(base, headRef);
759
755
  if (options.allowFailure) {
760
- return (await runCommand(cwd, args, { readOnly: true, signal: options.signal })).stdout;
756
+ return (await git(cwd, args, { readOnly: true, signal: options.signal })).stdout;
761
757
  }
762
758
  return runText(cwd, args, { readOnly: true, signal: options.signal });
763
759
  },
@@ -789,7 +785,7 @@ export const status = Object.assign(
789
785
  {
790
786
  /** Parsed status counts (staged, unstaged, untracked). */
791
787
  async summary(cwd: string, signal?: AbortSignal): Promise<GitStatusSummary | null> {
792
- const result = await runCommand(cwd, ["status", "--porcelain"], { readOnly: true, signal });
788
+ const result = await git(cwd, ["status", "--porcelain"], { readOnly: true, signal });
793
789
  if (result.exitCode !== 0) return null;
794
790
  return parseStatusPorcelain(result.stdout);
795
791
  },
@@ -950,7 +946,7 @@ export const branch = {
950
946
  async current(cwd: string, signal?: AbortSignal): Promise<string | null> {
951
947
  const headState = await resolveHead(cwd);
952
948
  if (headState?.kind === "ref") return headState.branchName ?? headState.ref;
953
- const result = await runCommand(cwd, ["symbolic-ref", "--short", "HEAD"], { readOnly: true, signal });
949
+ const result = await git(cwd, ["symbolic-ref", "--short", "HEAD"], { readOnly: true, signal });
954
950
  if (result.exitCode !== 0) return null;
955
951
  return result.stdout.trim() || null;
956
952
  },
@@ -966,7 +962,7 @@ export const branch = {
966
962
  }
967
963
  }
968
964
  for (const remoteRef of ["origin/HEAD", "upstream/HEAD"]) {
969
- const result = await runCommand(cwd, ["rev-parse", "--abbrev-ref", remoteRef], { readOnly: true, signal });
965
+ const result = await git(cwd, ["rev-parse", "--abbrev-ref", remoteRef], { readOnly: true, signal });
970
966
  if (result.exitCode !== 0) continue;
971
967
  const branchName = stripRemotePrefix(result.stdout.trim());
972
968
  if (branchName) return branchName;
@@ -995,7 +991,7 @@ export const branch = {
995
991
  name: string,
996
992
  options: { force?: boolean; signal?: AbortSignal } = {},
997
993
  ): Promise<boolean> {
998
- const result = await runCommand(cwd, ["branch", options.force === false ? "-d" : "-D", name], {
994
+ const result = await git(cwd, ["branch", options.force === false ? "-d" : "-D", name], {
999
995
  signal: options.signal,
1000
996
  });
1001
997
  return result.exitCode === 0;
@@ -1038,7 +1034,7 @@ export const remote = {
1038
1034
  * needs to resolve, not paper over.
1039
1035
  */
1040
1036
  async add(cwd: string, name: string, url: string, signal?: AbortSignal): Promise<void> {
1041
- const result = await runCommand(cwd, ["remote", "add", name, url], { signal });
1037
+ const result = await git(cwd, ["remote", "add", name, url], { signal });
1042
1038
  if (result.exitCode === 0) return;
1043
1039
  if (REMOTE_ALREADY_EXISTS.test(result.stderr)) {
1044
1040
  const existing = await remote.url(cwd, name, signal);
@@ -1059,7 +1055,7 @@ export const ref = {
1059
1055
  if (refName === "HEAD") return (await head.sha(cwd, signal)) !== null;
1060
1056
  const repository = await resolveRepository(cwd);
1061
1057
  if (repository && refName.startsWith("refs/")) return (await readRef(repository, refName)) !== null;
1062
- const result = await runCommand(cwd, ["show-ref", "--verify", "--quiet", refName], { readOnly: true, signal });
1058
+ const result = await git(cwd, ["show-ref", "--verify", "--quiet", refName], { readOnly: true, signal });
1063
1059
  return result.exitCode === 0;
1064
1060
  },
1065
1061
 
@@ -1068,7 +1064,7 @@ export const ref = {
1068
1064
  if (refName === "HEAD") return head.sha(cwd, signal);
1069
1065
  const repository = await resolveRepository(cwd);
1070
1066
  if (repository && refName.startsWith("refs/")) return readRef(repository, refName);
1071
- const result = await runCommand(cwd, ["rev-parse", refName], { readOnly: true, signal });
1067
+ const result = await git(cwd, ["rev-parse", refName], { readOnly: true, signal });
1072
1068
  if (result.exitCode !== 0) return null;
1073
1069
  return result.stdout.trim() || null;
1074
1070
  },
@@ -1150,7 +1146,7 @@ export const worktree = {
1150
1146
  const args = ["worktree", "remove"];
1151
1147
  if (options.force ?? true) args.push("-f");
1152
1148
  args.push(worktreePath);
1153
- const result = await runCommand(cwd, args, { signal: options.signal });
1149
+ const result = await git(cwd, args, { signal: options.signal });
1154
1150
  return result.exitCode === 0;
1155
1151
  },
1156
1152
 
@@ -1186,7 +1182,7 @@ export const patch = {
1186
1182
 
1187
1183
  /** Check if a patch file can be applied cleanly. */
1188
1184
  async canApply(cwd: string, patchPath: string, options: Omit<PatchOptions, "check"> = {}): Promise<boolean> {
1189
- const result = await runCommand(cwd, buildApplyArgs(patchPath, { ...options, check: true }), {
1185
+ const result = await git(cwd, buildApplyArgs(patchPath, { ...options, check: true }), {
1190
1186
  env: options.env,
1191
1187
  readOnly: true,
1192
1188
  signal: options.signal,
@@ -1346,7 +1342,7 @@ export const ls = {
1346
1342
 
1347
1343
  /** List submodule paths (recursive). */
1348
1344
  async submodules(cwd: string, signal?: AbortSignal): Promise<string[]> {
1349
- const output = await runCommand(cwd, ["submodule", "--quiet", "foreach", "--recursive", "echo $sm_path"], {
1345
+ const output = await git(cwd, ["submodule", "--quiet", "foreach", "--recursive", "echo $sm_path"], {
1350
1346
  readOnly: true,
1351
1347
  signal,
1352
1348
  });
@@ -1381,14 +1377,14 @@ export const head = {
1381
1377
  async sha(cwd: string, signal?: AbortSignal): Promise<string | null> {
1382
1378
  const headState = await head.resolve(cwd);
1383
1379
  if (headState?.commit) return headState.commit;
1384
- const result = await runCommand(cwd, ["rev-parse", "HEAD"], { readOnly: true, signal });
1380
+ const result = await git(cwd, ["rev-parse", "HEAD"], { readOnly: true, signal });
1385
1381
  if (result.exitCode !== 0) return null;
1386
1382
  return result.stdout.trim() || null;
1387
1383
  },
1388
1384
 
1389
1385
  /** Abbreviated HEAD commit SHA. */
1390
1386
  async short(cwd: string, length = 7, signal?: AbortSignal): Promise<string | null> {
1391
- const result = await runCommand(cwd, ["rev-parse", `--short=${length}`, "HEAD"], { readOnly: true, signal });
1387
+ const result = await git(cwd, ["rev-parse", `--short=${length}`, "HEAD"], { readOnly: true, signal });
1392
1388
  if (result.exitCode !== 0) return null;
1393
1389
  return result.stdout.trim() || null;
1394
1390
  },
@@ -1403,7 +1399,7 @@ export const repo = {
1403
1399
  async root(cwd: string, signal?: AbortSignal): Promise<string | null> {
1404
1400
  const repository = await resolveRepository(cwd);
1405
1401
  if (repository) return repository.repoRoot;
1406
- const result = await runCommand(cwd, ["rev-parse", "--show-toplevel"], { readOnly: true, signal });
1402
+ const result = await git(cwd, ["rev-parse", "--show-toplevel"], { readOnly: true, signal });
1407
1403
  if (result.exitCode !== 0) return null;
1408
1404
  return result.stdout.trim() || null;
1409
1405
  },