@oh-my-pi/pi-coding-agent 3.15.1 → 3.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/docs/extensions.md +1055 -0
  3. package/docs/rpc.md +69 -13
  4. package/docs/session-tree-plan.md +1 -1
  5. package/examples/extensions/README.md +141 -0
  6. package/examples/extensions/api-demo.ts +87 -0
  7. package/examples/extensions/chalk-logger.ts +26 -0
  8. package/examples/extensions/hello.ts +33 -0
  9. package/examples/extensions/pirate.ts +44 -0
  10. package/examples/extensions/plan-mode.ts +551 -0
  11. package/examples/extensions/subagent/agents/reviewer.md +35 -0
  12. package/examples/extensions/todo.ts +299 -0
  13. package/examples/extensions/tools.ts +145 -0
  14. package/examples/extensions/with-deps/index.ts +36 -0
  15. package/examples/extensions/with-deps/package-lock.json +31 -0
  16. package/examples/extensions/with-deps/package.json +16 -0
  17. package/examples/sdk/02-custom-model.ts +3 -3
  18. package/examples/sdk/05-tools.ts +7 -3
  19. package/examples/sdk/06-extensions.ts +81 -0
  20. package/examples/sdk/06-hooks.ts +14 -13
  21. package/examples/sdk/08-prompt-templates.ts +42 -0
  22. package/examples/sdk/08-slash-commands.ts +17 -12
  23. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  24. package/examples/sdk/12-full-control.ts +6 -6
  25. package/package.json +11 -7
  26. package/src/capability/extension-module.ts +34 -0
  27. package/src/cli/args.ts +22 -7
  28. package/src/cli/file-processor.ts +38 -67
  29. package/src/cli/list-models.ts +1 -1
  30. package/src/config.ts +25 -14
  31. package/src/core/agent-session.ts +505 -242
  32. package/src/core/auth-storage.ts +33 -21
  33. package/src/core/compaction/branch-summarization.ts +4 -4
  34. package/src/core/compaction/compaction.ts +3 -3
  35. package/src/core/custom-commands/bundled/wt/index.ts +430 -0
  36. package/src/core/custom-commands/loader.ts +9 -0
  37. package/src/core/custom-tools/wrapper.ts +5 -0
  38. package/src/core/event-bus.ts +59 -0
  39. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  40. package/src/core/export-html/vendor/marked.min.js +6 -0
  41. package/src/core/extensions/index.ts +100 -0
  42. package/src/core/extensions/loader.ts +501 -0
  43. package/src/core/extensions/runner.ts +477 -0
  44. package/src/core/extensions/types.ts +712 -0
  45. package/src/core/extensions/wrapper.ts +147 -0
  46. package/src/core/hooks/types.ts +2 -2
  47. package/src/core/index.ts +10 -21
  48. package/src/core/keybindings.ts +199 -0
  49. package/src/core/messages.ts +26 -7
  50. package/src/core/model-registry.ts +123 -46
  51. package/src/core/model-resolver.ts +7 -5
  52. package/src/core/prompt-templates.ts +242 -0
  53. package/src/core/sdk.ts +378 -295
  54. package/src/core/session-manager.ts +72 -58
  55. package/src/core/settings-manager.ts +118 -22
  56. package/src/core/system-prompt.ts +24 -1
  57. package/src/core/terminal-notify.ts +37 -0
  58. package/src/core/tools/context.ts +4 -4
  59. package/src/core/tools/exa/mcp-client.ts +5 -4
  60. package/src/core/tools/exa/render.ts +176 -131
  61. package/src/core/tools/gemini-image.ts +361 -0
  62. package/src/core/tools/git.ts +216 -0
  63. package/src/core/tools/index.ts +28 -15
  64. package/src/core/tools/lsp/config.ts +5 -4
  65. package/src/core/tools/lsp/index.ts +17 -12
  66. package/src/core/tools/lsp/render.ts +39 -47
  67. package/src/core/tools/read.ts +66 -29
  68. package/src/core/tools/render-utils.ts +268 -0
  69. package/src/core/tools/renderers.ts +243 -225
  70. package/src/core/tools/task/discovery.ts +2 -2
  71. package/src/core/tools/task/executor.ts +66 -58
  72. package/src/core/tools/task/index.ts +29 -10
  73. package/src/core/tools/task/model-resolver.ts +8 -13
  74. package/src/core/tools/task/omp-command.ts +24 -0
  75. package/src/core/tools/task/render.ts +35 -60
  76. package/src/core/tools/task/types.ts +3 -0
  77. package/src/core/tools/web-fetch.ts +29 -28
  78. package/src/core/tools/web-search/index.ts +6 -5
  79. package/src/core/tools/web-search/providers/exa.ts +6 -5
  80. package/src/core/tools/web-search/render.ts +66 -111
  81. package/src/core/voice-controller.ts +135 -0
  82. package/src/core/voice-supervisor.ts +1003 -0
  83. package/src/core/voice.ts +308 -0
  84. package/src/discovery/builtin.ts +75 -1
  85. package/src/discovery/claude.ts +47 -1
  86. package/src/discovery/codex.ts +54 -2
  87. package/src/discovery/gemini.ts +55 -2
  88. package/src/discovery/helpers.ts +100 -1
  89. package/src/discovery/index.ts +2 -0
  90. package/src/index.ts +14 -9
  91. package/src/lib/worktree/collapse.ts +179 -0
  92. package/src/lib/worktree/constants.ts +14 -0
  93. package/src/lib/worktree/errors.ts +23 -0
  94. package/src/lib/worktree/git.ts +110 -0
  95. package/src/lib/worktree/index.ts +23 -0
  96. package/src/lib/worktree/operations.ts +216 -0
  97. package/src/lib/worktree/session.ts +114 -0
  98. package/src/lib/worktree/stats.ts +67 -0
  99. package/src/main.ts +61 -37
  100. package/src/migrations.ts +37 -7
  101. package/src/modes/interactive/components/bash-execution.ts +6 -4
  102. package/src/modes/interactive/components/custom-editor.ts +55 -0
  103. package/src/modes/interactive/components/custom-message.ts +95 -0
  104. package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
  105. package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
  106. package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
  107. package/src/modes/interactive/components/extensions/types.ts +1 -0
  108. package/src/modes/interactive/components/footer.ts +324 -0
  109. package/src/modes/interactive/components/hook-selector.ts +3 -3
  110. package/src/modes/interactive/components/model-selector.ts +7 -6
  111. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  112. package/src/modes/interactive/components/settings-defs.ts +55 -6
  113. package/src/modes/interactive/components/status-line.ts +45 -37
  114. package/src/modes/interactive/components/tool-execution.ts +95 -23
  115. package/src/modes/interactive/interactive-mode.ts +643 -113
  116. package/src/modes/interactive/theme/defaults/index.ts +16 -16
  117. package/src/modes/print-mode.ts +14 -72
  118. package/src/modes/rpc/rpc-client.ts +23 -9
  119. package/src/modes/rpc/rpc-mode.ts +137 -125
  120. package/src/modes/rpc/rpc-types.ts +46 -24
  121. package/src/prompts/task.md +1 -0
  122. package/src/prompts/tools/gemini-image.md +4 -0
  123. package/src/prompts/tools/git.md +9 -0
  124. package/src/prompts/voice-summary.md +12 -0
  125. package/src/utils/image-convert.ts +26 -0
  126. package/src/utils/image-resize.ts +215 -0
  127. package/src/utils/shell-snapshot.ts +22 -20
@@ -2,7 +2,7 @@
2
2
  * Shared helpers for discovery providers.
3
3
  */
4
4
 
5
- import { join, resolve } from "node:path";
5
+ import { join, resolve } from "path";
6
6
  import { parse as parseYAML } from "yaml";
7
7
  import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
8
8
 
@@ -247,3 +247,102 @@ export function parseJSON<T>(content: string): T | null {
247
247
  export function calculateDepth(cwd: string, targetDir: string, separator: string): number {
248
248
  return cwd.split(separator).length - targetDir.split(separator).length;
249
249
  }
250
+
251
+ interface ExtensionModuleManifest {
252
+ extensions?: string[];
253
+ }
254
+
255
+ function readExtensionModuleManifest(ctx: LoadContext, packageJsonPath: string): ExtensionModuleManifest | null {
256
+ const content = ctx.fs.readFile(packageJsonPath);
257
+ if (!content) return null;
258
+
259
+ const pkg = parseJSON<{ omp?: ExtensionModuleManifest; pi?: ExtensionModuleManifest }>(content);
260
+ const manifest = pkg?.omp ?? pkg?.pi;
261
+ if (manifest && typeof manifest === "object") {
262
+ return manifest;
263
+ }
264
+ return null;
265
+ }
266
+
267
+ function isExtensionModuleFile(name: string): boolean {
268
+ return name.endsWith(".ts") || name.endsWith(".js");
269
+ }
270
+
271
+ /**
272
+ * Discover extension module entry points in a directory.
273
+ *
274
+ * Discovery rules:
275
+ * 1. Direct files: `extensions/*.ts` or `*.js` → load
276
+ * 2. Subdirectory with index: `extensions/<ext>/index.ts` or `index.js` → load
277
+ * 3. Subdirectory with package.json: `extensions/<ext>/package.json` with "omp"/"pi" field → load declared paths
278
+ *
279
+ * No recursion beyond one level. Complex packages must use package.json manifest.
280
+ */
281
+ export function discoverExtensionModulePaths(ctx: LoadContext, dir: string): string[] {
282
+ if (!ctx.fs.isDir(dir)) {
283
+ return [];
284
+ }
285
+
286
+ const discovered: string[] = [];
287
+
288
+ for (const name of ctx.fs.readDir(dir)) {
289
+ if (name.startsWith(".")) continue;
290
+
291
+ const entryPath = join(dir, name);
292
+
293
+ // 1. Direct files: *.ts or *.js
294
+ if (ctx.fs.isFile(entryPath) && isExtensionModuleFile(name)) {
295
+ discovered.push(entryPath);
296
+ continue;
297
+ }
298
+
299
+ // 2 & 3. Subdirectories
300
+ if (ctx.fs.isDir(entryPath)) {
301
+ // Check for package.json with "omp"/"pi" field first
302
+ const packageJsonPath = join(entryPath, "package.json");
303
+ if (ctx.fs.isFile(packageJsonPath)) {
304
+ const manifest = readExtensionModuleManifest(ctx, packageJsonPath);
305
+ if (manifest?.extensions && Array.isArray(manifest.extensions)) {
306
+ for (const extPath of manifest.extensions) {
307
+ const resolvedExtPath = resolve(entryPath, extPath);
308
+ if (ctx.fs.isFile(resolvedExtPath)) {
309
+ discovered.push(resolvedExtPath);
310
+ }
311
+ }
312
+ continue;
313
+ }
314
+ }
315
+
316
+ // Check for index.ts or index.js
317
+ const indexTs = join(entryPath, "index.ts");
318
+ const indexJs = join(entryPath, "index.js");
319
+ if (ctx.fs.isFile(indexTs)) {
320
+ discovered.push(indexTs);
321
+ } else if (ctx.fs.isFile(indexJs)) {
322
+ discovered.push(indexJs);
323
+ }
324
+ }
325
+ }
326
+
327
+ return discovered;
328
+ }
329
+
330
+ /**
331
+ * Derive a stable extension name from a path.
332
+ */
333
+ export function getExtensionNameFromPath(extensionPath: string): string {
334
+ const base = extensionPath.replace(/\\/g, "/").split("/").pop() ?? extensionPath;
335
+
336
+ if (base === "index.ts" || base === "index.js") {
337
+ const parts = extensionPath.replace(/\\/g, "/").split("/");
338
+ const parent = parts[parts.length - 2];
339
+ return parent ?? base;
340
+ }
341
+
342
+ const dot = base.lastIndexOf(".");
343
+ if (dot > 0) {
344
+ return base.slice(0, dot);
345
+ }
346
+
347
+ return base;
348
+ }
@@ -8,6 +8,7 @@
8
8
  // Import capability definitions (ensures capabilities are defined before providers register)
9
9
  import "../capability/context-file";
10
10
  import "../capability/extension";
11
+ import "../capability/extension-module";
11
12
  import "../capability/hook";
12
13
  import "../capability/instruction";
13
14
  import "../capability/mcp";
@@ -34,6 +35,7 @@ import "./mcp-json";
34
35
 
35
36
  export type { ContextFile } from "../capability/context-file";
36
37
  export type { Extension, ExtensionManifest } from "../capability/extension";
38
+ export type { ExtensionModule } from "../capability/extension-module";
37
39
  export type { Hook } from "../capability/hook";
38
40
  // Re-export the main API from capability registry
39
41
  export {
package/src/index.ts CHANGED
@@ -60,6 +60,8 @@ export type {
60
60
  RenderResultOptions,
61
61
  } from "./core/custom-tools/index";
62
62
  export { discoverAndLoadCustomTools, loadCustomTools } from "./core/custom-tools/index";
63
+ // Extension types
64
+ export type { ExtensionAPI, ExtensionContext, ExtensionFactory } from "./core/extensions/types";
63
65
  export type * from "./core/hooks/index";
64
66
  // Hook system types and type guards
65
67
  export {
@@ -75,6 +77,8 @@ export {
75
77
  export { type Logger, logger } from "./core/logger";
76
78
  export { convertToLlm } from "./core/messages";
77
79
  export { ModelRegistry } from "./core/model-registry";
80
+ // Prompt templates
81
+ export type { PromptTemplate } from "./core/prompt-templates";
78
82
  // SDK for programmatic usage
79
83
  export {
80
84
  type BuildSystemPromptOptions,
@@ -96,16 +100,12 @@ export {
96
100
  // Discovery
97
101
  discoverAuthStorage,
98
102
  discoverContextFiles,
99
- discoverCustomTools,
100
- discoverHooks,
103
+ discoverCustomTSCommands,
104
+ discoverExtensions,
105
+ discoverMCPServers,
101
106
  discoverModels,
107
+ discoverPromptTemplates,
102
108
  discoverSkills,
103
- discoverSlashCommands,
104
- type FileSlashCommand,
105
- // Hook types
106
- type HookAPI,
107
- type HookContext,
108
- type HookFactory,
109
109
  loadSettings,
110
110
  // Pre-built tools (use process.cwd())
111
111
  readOnlyTools,
@@ -134,6 +134,7 @@ export {
134
134
  } from "./core/session-manager";
135
135
  export {
136
136
  type CompactionSettings,
137
+ type ImageSettings,
137
138
  type LspSettings,
138
139
  type RetrySettings,
139
140
  type Settings,
@@ -151,6 +152,8 @@ export {
151
152
  type SkillFrontmatter,
152
153
  type SkillWarning,
153
154
  } from "./core/skills";
155
+ // Slash commands
156
+ export { type FileSlashCommand, loadSlashCommands as discoverSlashCommands } from "./core/slash-commands";
154
157
  // Tools
155
158
  export {
156
159
  type BashToolDetails,
@@ -160,7 +163,9 @@ export {
160
163
  editTool,
161
164
  type FindToolDetails,
162
165
  findTool,
166
+ type GitToolDetails,
163
167
  type GrepToolDetails,
168
+ gitTool,
164
169
  grepTool,
165
170
  type LsToolDetails,
166
171
  lsTool,
@@ -177,7 +182,7 @@ export { main } from "./main";
177
182
  // UI components for hooks and custom tools
178
183
  export { BorderedLoader } from "./modes/interactive/components/bordered-loader";
179
184
  // Theme utilities for custom tools
180
- export { getMarkdownTheme } from "./modes/interactive/theme/theme";
185
+ export { getMarkdownTheme, getSettingsListTheme, type Theme } from "./modes/interactive/theme/theme";
181
186
 
182
187
  // TypeBox helper for string enums (convenience for custom tools)
183
188
  import { type TSchema, Type } from "@sinclair/typebox";
@@ -0,0 +1,179 @@
1
+ import { WorktreeError, WorktreeErrorCode } from "./errors";
2
+ import { git, gitWithStdin } from "./git";
3
+ import { find, remove, type Worktree } from "./operations";
4
+
5
+ export type CollapseStrategy = "simple" | "merge-base" | "rebase";
6
+
7
+ export interface CollapseOptions {
8
+ strategy?: CollapseStrategy;
9
+ keepSource?: boolean;
10
+ }
11
+
12
+ export interface CollapseResult {
13
+ filesChanged: number;
14
+ insertions: number;
15
+ deletions: number;
16
+ }
17
+
18
+ function diffStats(diff: string): CollapseResult {
19
+ let filesChanged = 0;
20
+ let insertions = 0;
21
+ let deletions = 0;
22
+
23
+ for (const line of diff.split("\n")) {
24
+ if (line.startsWith("diff --git ")) {
25
+ filesChanged += 1;
26
+ continue;
27
+ }
28
+ if (line.startsWith("+++") || line.startsWith("---")) continue;
29
+ if (line.startsWith("+")) {
30
+ insertions += 1;
31
+ continue;
32
+ }
33
+ if (line.startsWith("-")) {
34
+ deletions += 1;
35
+ }
36
+ }
37
+
38
+ return { filesChanged, insertions, deletions };
39
+ }
40
+
41
+ async function requireGitSuccess(result: { code: number; stderr: string }, message: string): Promise<void> {
42
+ if (result.code !== 0) {
43
+ throw new WorktreeError(
44
+ message + (result.stderr ? `\n${result.stderr.trim()}` : ""),
45
+ WorktreeErrorCode.COLLAPSE_FAILED,
46
+ );
47
+ }
48
+ }
49
+
50
+ async function ensureHasChanges(result: { stdout: string }): Promise<string> {
51
+ const diff = result.stdout;
52
+ if (!diff.trim()) {
53
+ throw new WorktreeError("No changes to collapse", WorktreeErrorCode.NO_CHANGES);
54
+ }
55
+ return diff;
56
+ }
57
+
58
+ async function collapseSimple(src: Worktree): Promise<string> {
59
+ await requireGitSuccess(await git(["add", "-A"], src.path), "Failed to stage changes");
60
+ return ensureHasChanges(await git(["diff", "HEAD"], src.path));
61
+ }
62
+
63
+ async function collapseMergeBase(src: Worktree, dst: Worktree): Promise<string> {
64
+ await requireGitSuccess(await git(["add", "-A"], src.path), "Failed to stage changes");
65
+
66
+ const baseResult = await git(["merge-base", "HEAD", dst.branch ?? "HEAD"], src.path);
67
+ if (baseResult.code !== 0) {
68
+ throw new WorktreeError("Could not find merge base", WorktreeErrorCode.COLLAPSE_FAILED);
69
+ }
70
+
71
+ const base = baseResult.stdout.trim();
72
+ if (!base) {
73
+ throw new WorktreeError("Could not find merge base", WorktreeErrorCode.COLLAPSE_FAILED);
74
+ }
75
+
76
+ return ensureHasChanges(await git(["diff", base], src.path));
77
+ }
78
+
79
+ async function collapseRebase(src: Worktree, dst: Worktree): Promise<string> {
80
+ await requireGitSuccess(await git(["add", "-A"], src.path), "Failed to stage changes");
81
+
82
+ const stagedResult = await git(["diff", "--cached", "--name-only"], src.path);
83
+ if (!stagedResult.stdout.trim()) {
84
+ throw new WorktreeError("No changes to collapse", WorktreeErrorCode.NO_CHANGES);
85
+ }
86
+
87
+ const headResult = await git(["rev-parse", "HEAD"], src.path);
88
+ if (headResult.code !== 0) {
89
+ throw new WorktreeError("Failed to resolve HEAD", WorktreeErrorCode.COLLAPSE_FAILED);
90
+ }
91
+ const originalHead = headResult.stdout.trim();
92
+ const tempBranch = `wt-collapse-${Date.now()}`;
93
+
94
+ await requireGitSuccess(await git(["checkout", "-b", tempBranch], src.path), "Failed to create temp branch");
95
+
96
+ const commitResult = await git(["commit", "--allow-empty-message", "-m", ""], src.path);
97
+ if (commitResult.code !== 0) {
98
+ await git(["checkout", originalHead], src.path);
99
+ await git(["branch", "-D", tempBranch], src.path);
100
+ throw new WorktreeError("Failed to commit changes", WorktreeErrorCode.COLLAPSE_FAILED);
101
+ }
102
+
103
+ const rebaseResult = await git(["rebase", dst.branch ?? "HEAD"], src.path);
104
+ if (rebaseResult.code !== 0) {
105
+ await git(["rebase", "--abort"], src.path);
106
+ await git(["checkout", originalHead], src.path);
107
+ await git(["branch", "-D", tempBranch], src.path);
108
+ throw new WorktreeError(
109
+ `Rebase conflicts:${rebaseResult.stderr ? `\n${rebaseResult.stderr.trim()}` : ""}`,
110
+ WorktreeErrorCode.REBASE_CONFLICTS,
111
+ );
112
+ }
113
+
114
+ const diffResult = await git(["diff", `${dst.branch ?? "HEAD"}..HEAD`], src.path);
115
+
116
+ await git(["checkout", originalHead], src.path);
117
+ await git(["branch", "-D", tempBranch], src.path);
118
+
119
+ return ensureHasChanges(diffResult);
120
+ }
121
+
122
+ async function applyDiff(diff: string, targetPath: string): Promise<void> {
123
+ let result = await gitWithStdin(["apply"], diff, targetPath);
124
+ if (result.code === 0) return;
125
+
126
+ result = await gitWithStdin(["apply", "--3way"], diff, targetPath);
127
+ if (result.code === 0) return;
128
+
129
+ throw new WorktreeError(
130
+ `Failed to apply diff:${result.stderr ? `\n${result.stderr.trim()}` : ""}`,
131
+ WorktreeErrorCode.APPLY_FAILED,
132
+ );
133
+ }
134
+
135
+ /**
136
+ * Collapse changes from source worktree into destination.
137
+ */
138
+ export async function collapse(
139
+ source: string,
140
+ destination: string,
141
+ options?: CollapseOptions,
142
+ ): Promise<CollapseResult> {
143
+ const src = await find(source);
144
+ const dst = await find(destination);
145
+
146
+ if (src.path === dst.path) {
147
+ throw new WorktreeError("Source and destination are the same", WorktreeErrorCode.COLLAPSE_FAILED);
148
+ }
149
+
150
+ if (!options?.keepSource && src.isMain) {
151
+ throw new WorktreeError("Cannot remove main worktree", WorktreeErrorCode.CANNOT_MODIFY_MAIN);
152
+ }
153
+
154
+ const strategy = options?.strategy ?? "rebase";
155
+ let diff: string;
156
+
157
+ switch (strategy) {
158
+ case "simple":
159
+ diff = await collapseSimple(src);
160
+ break;
161
+ case "merge-base":
162
+ diff = await collapseMergeBase(src, dst);
163
+ break;
164
+ case "rebase":
165
+ diff = await collapseRebase(src, dst);
166
+ break;
167
+ default:
168
+ throw new WorktreeError(`Unknown strategy: ${strategy}`, WorktreeErrorCode.COLLAPSE_FAILED);
169
+ }
170
+
171
+ const stats = diffStats(diff);
172
+ await applyDiff(diff, dst.path);
173
+
174
+ if (!options?.keepSource) {
175
+ await remove(src.path, { force: true });
176
+ }
177
+
178
+ return stats;
179
+ }
@@ -0,0 +1,14 @@
1
+ import { accessSync, constants } from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+
5
+ function getWorktreeBase(): string {
6
+ try {
7
+ accessSync("/work", constants.W_OK);
8
+ return "/work/.tree";
9
+ } catch {
10
+ return path.join(os.tmpdir(), ".tree");
11
+ }
12
+ }
13
+
14
+ export const WORKTREE_BASE = getWorktreeBase();
@@ -0,0 +1,23 @@
1
+ export enum WorktreeErrorCode {
2
+ NOT_GIT_REPO = "NOT_GIT_REPO",
3
+ WORKTREE_NOT_FOUND = "WORKTREE_NOT_FOUND",
4
+ WORKTREE_EXISTS = "WORKTREE_EXISTS",
5
+ CANNOT_MODIFY_MAIN = "CANNOT_MODIFY_MAIN",
6
+ NO_CHANGES = "NO_CHANGES",
7
+ COLLAPSE_FAILED = "COLLAPSE_FAILED",
8
+ REBASE_CONFLICTS = "REBASE_CONFLICTS",
9
+ APPLY_FAILED = "APPLY_FAILED",
10
+ OVERLAPPING_SCOPES = "OVERLAPPING_SCOPES",
11
+ }
12
+
13
+ export class WorktreeError extends Error {
14
+ readonly code: WorktreeErrorCode;
15
+ readonly cause?: Error;
16
+
17
+ constructor(message: string, code: WorktreeErrorCode, cause?: Error) {
18
+ super(message);
19
+ this.name = "WorktreeError";
20
+ this.code = code;
21
+ this.cause = cause;
22
+ }
23
+ }
@@ -0,0 +1,110 @@
1
+ import * as path from "node:path";
2
+ import type { Subprocess } from "bun";
3
+ import { execCommand } from "../../core/exec";
4
+ import { WorktreeError, WorktreeErrorCode } from "./errors";
5
+
6
+ export interface GitResult {
7
+ code: number;
8
+ stdout: string;
9
+ stderr: string;
10
+ }
11
+
12
+ type WritableLike = {
13
+ write: (chunk: string | Uint8Array) => unknown;
14
+ flush?: () => unknown;
15
+ end?: () => unknown;
16
+ };
17
+
18
+ const textEncoder = new TextEncoder();
19
+
20
+ async function readStream(stream: ReadableStream<Uint8Array> | undefined): Promise<string> {
21
+ if (!stream) return "";
22
+ const reader = stream.getReader();
23
+ const chunks: Uint8Array[] = [];
24
+ try {
25
+ while (true) {
26
+ const { done, value } = await reader.read();
27
+ if (done) break;
28
+ chunks.push(value);
29
+ }
30
+ } finally {
31
+ reader.releaseLock();
32
+ }
33
+ return Buffer.concat(chunks).toString();
34
+ }
35
+
36
+ async function writeStdin(handle: unknown, stdin: string): Promise<void> {
37
+ if (!handle || typeof handle === "number") return;
38
+ if (typeof (handle as WritableStream<Uint8Array>).getWriter === "function") {
39
+ const writer = (handle as WritableStream<Uint8Array>).getWriter();
40
+ try {
41
+ await writer.write(textEncoder.encode(stdin));
42
+ } finally {
43
+ await writer.close();
44
+ }
45
+ return;
46
+ }
47
+
48
+ const sink = handle as WritableLike;
49
+ sink.write(stdin);
50
+ if (sink.flush) sink.flush();
51
+ if (sink.end) sink.end();
52
+ }
53
+
54
+ /**
55
+ * Execute a git command.
56
+ * @param args - Command arguments (excluding 'git')
57
+ * @param cwd - Working directory (optional)
58
+ * @returns Promise<GitResult>
59
+ */
60
+ export async function git(args: string[], cwd?: string): Promise<GitResult> {
61
+ const result = await execCommand("git", args, cwd ?? process.cwd());
62
+ return { code: result.code, stdout: result.stdout, stderr: result.stderr };
63
+ }
64
+
65
+ /**
66
+ * Execute git command with stdin input.
67
+ * Used for piping diffs to `git apply`.
68
+ */
69
+ export async function gitWithStdin(args: string[], stdin: string, cwd?: string): Promise<GitResult> {
70
+ const proc: Subprocess = Bun.spawn(["git", ...args], {
71
+ cwd: cwd ?? process.cwd(),
72
+ stdin: "pipe",
73
+ stdout: "pipe",
74
+ stderr: "pipe",
75
+ });
76
+
77
+ await writeStdin(proc.stdin, stdin);
78
+
79
+ const [stdout, stderr, exitCode] = await Promise.all([
80
+ readStream(proc.stdout as ReadableStream<Uint8Array>),
81
+ readStream(proc.stderr as ReadableStream<Uint8Array>),
82
+ proc.exited,
83
+ ]);
84
+
85
+ return { code: exitCode ?? 0, stdout, stderr };
86
+ }
87
+
88
+ /**
89
+ * Get repository root directory.
90
+ * @throws Error if not in a git repository
91
+ */
92
+ export async function getRepoRoot(cwd?: string): Promise<string> {
93
+ const result = await git(["rev-parse", "--show-toplevel"], cwd ?? process.cwd());
94
+ if (result.code !== 0) {
95
+ throw new WorktreeError("Not a git repository", WorktreeErrorCode.NOT_GIT_REPO);
96
+ }
97
+ const root = result.stdout.trim();
98
+ if (!root) {
99
+ throw new WorktreeError("Not a git repository", WorktreeErrorCode.NOT_GIT_REPO);
100
+ }
101
+ return path.resolve(root);
102
+ }
103
+
104
+ /**
105
+ * Get repository name (directory basename of repo root).
106
+ */
107
+ export async function getRepoName(cwd?: string): Promise<string> {
108
+ const root = await getRepoRoot(cwd);
109
+ return path.basename(root);
110
+ }
@@ -0,0 +1,23 @@
1
+ export { type CollapseOptions, type CollapseResult, type CollapseStrategy, collapse } from "./collapse";
2
+ export { WORKTREE_BASE } from "./constants";
3
+ export { WorktreeError, WorktreeErrorCode } from "./errors";
4
+ export { getRepoName, getRepoRoot, git, gitWithStdin } from "./git";
5
+ export {
6
+ create,
7
+ find,
8
+ list,
9
+ prune,
10
+ remove,
11
+ type Worktree,
12
+ which,
13
+ } from "./operations";
14
+ export {
15
+ cleanupSessions,
16
+ createSession,
17
+ getSession,
18
+ listSessions,
19
+ type SessionStatus,
20
+ updateSession,
21
+ type WorktreeSession,
22
+ } from "./session";
23
+ export { formatStats, getStats, type WorktreeStats } from "./stats";