@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.
- package/CHANGELOG.md +51 -1
- package/docs/extensions.md +1055 -0
- package/docs/rpc.md +69 -13
- package/docs/session-tree-plan.md +1 -1
- package/examples/extensions/README.md +141 -0
- package/examples/extensions/api-demo.ts +87 -0
- package/examples/extensions/chalk-logger.ts +26 -0
- package/examples/extensions/hello.ts +33 -0
- package/examples/extensions/pirate.ts +44 -0
- package/examples/extensions/plan-mode.ts +551 -0
- package/examples/extensions/subagent/agents/reviewer.md +35 -0
- package/examples/extensions/todo.ts +299 -0
- package/examples/extensions/tools.ts +145 -0
- package/examples/extensions/with-deps/index.ts +36 -0
- package/examples/extensions/with-deps/package-lock.json +31 -0
- package/examples/extensions/with-deps/package.json +16 -0
- package/examples/sdk/02-custom-model.ts +3 -3
- package/examples/sdk/05-tools.ts +7 -3
- package/examples/sdk/06-extensions.ts +81 -0
- package/examples/sdk/06-hooks.ts +14 -13
- package/examples/sdk/08-prompt-templates.ts +42 -0
- package/examples/sdk/08-slash-commands.ts +17 -12
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/12-full-control.ts +6 -6
- package/package.json +11 -7
- package/src/capability/extension-module.ts +34 -0
- package/src/cli/args.ts +22 -7
- package/src/cli/file-processor.ts +38 -67
- package/src/cli/list-models.ts +1 -1
- package/src/config.ts +25 -14
- package/src/core/agent-session.ts +505 -242
- package/src/core/auth-storage.ts +33 -21
- package/src/core/compaction/branch-summarization.ts +4 -4
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/custom-commands/bundled/wt/index.ts +430 -0
- package/src/core/custom-commands/loader.ts +9 -0
- package/src/core/custom-tools/wrapper.ts +5 -0
- package/src/core/event-bus.ts +59 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/extensions/index.ts +100 -0
- package/src/core/extensions/loader.ts +501 -0
- package/src/core/extensions/runner.ts +477 -0
- package/src/core/extensions/types.ts +712 -0
- package/src/core/extensions/wrapper.ts +147 -0
- package/src/core/hooks/types.ts +2 -2
- package/src/core/index.ts +10 -21
- package/src/core/keybindings.ts +199 -0
- package/src/core/messages.ts +26 -7
- package/src/core/model-registry.ts +123 -46
- package/src/core/model-resolver.ts +7 -5
- package/src/core/prompt-templates.ts +242 -0
- package/src/core/sdk.ts +378 -295
- package/src/core/session-manager.ts +72 -58
- package/src/core/settings-manager.ts +118 -22
- package/src/core/system-prompt.ts +24 -1
- package/src/core/terminal-notify.ts +37 -0
- package/src/core/tools/context.ts +4 -4
- package/src/core/tools/exa/mcp-client.ts +5 -4
- package/src/core/tools/exa/render.ts +176 -131
- package/src/core/tools/gemini-image.ts +361 -0
- package/src/core/tools/git.ts +216 -0
- package/src/core/tools/index.ts +28 -15
- package/src/core/tools/lsp/config.ts +5 -4
- package/src/core/tools/lsp/index.ts +17 -12
- package/src/core/tools/lsp/render.ts +39 -47
- package/src/core/tools/read.ts +66 -29
- package/src/core/tools/render-utils.ts +268 -0
- package/src/core/tools/renderers.ts +243 -225
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +66 -58
- package/src/core/tools/task/index.ts +29 -10
- package/src/core/tools/task/model-resolver.ts +8 -13
- package/src/core/tools/task/omp-command.ts +24 -0
- package/src/core/tools/task/render.ts +35 -60
- package/src/core/tools/task/types.ts +3 -0
- package/src/core/tools/web-fetch.ts +29 -28
- package/src/core/tools/web-search/index.ts +6 -5
- package/src/core/tools/web-search/providers/exa.ts +6 -5
- package/src/core/tools/web-search/render.ts +66 -111
- package/src/core/voice-controller.ts +135 -0
- package/src/core/voice-supervisor.ts +1003 -0
- package/src/core/voice.ts +308 -0
- package/src/discovery/builtin.ts +75 -1
- package/src/discovery/claude.ts +47 -1
- package/src/discovery/codex.ts +54 -2
- package/src/discovery/gemini.ts +55 -2
- package/src/discovery/helpers.ts +100 -1
- package/src/discovery/index.ts +2 -0
- package/src/index.ts +14 -9
- package/src/lib/worktree/collapse.ts +179 -0
- package/src/lib/worktree/constants.ts +14 -0
- package/src/lib/worktree/errors.ts +23 -0
- package/src/lib/worktree/git.ts +110 -0
- package/src/lib/worktree/index.ts +23 -0
- package/src/lib/worktree/operations.ts +216 -0
- package/src/lib/worktree/session.ts +114 -0
- package/src/lib/worktree/stats.ts +67 -0
- package/src/main.ts +61 -37
- package/src/migrations.ts +37 -7
- package/src/modes/interactive/components/bash-execution.ts +6 -4
- package/src/modes/interactive/components/custom-editor.ts +55 -0
- package/src/modes/interactive/components/custom-message.ts +95 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
- package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
- package/src/modes/interactive/components/extensions/types.ts +1 -0
- package/src/modes/interactive/components/footer.ts +324 -0
- package/src/modes/interactive/components/hook-selector.ts +3 -3
- package/src/modes/interactive/components/model-selector.ts +7 -6
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/settings-defs.ts +55 -6
- package/src/modes/interactive/components/status-line.ts +45 -37
- package/src/modes/interactive/components/tool-execution.ts +95 -23
- package/src/modes/interactive/interactive-mode.ts +643 -113
- package/src/modes/interactive/theme/defaults/index.ts +16 -16
- package/src/modes/print-mode.ts +14 -72
- package/src/modes/rpc/rpc-client.ts +23 -9
- package/src/modes/rpc/rpc-mode.ts +137 -125
- package/src/modes/rpc/rpc-types.ts +46 -24
- package/src/prompts/task.md +1 -0
- package/src/prompts/tools/gemini-image.md +4 -0
- package/src/prompts/tools/git.md +9 -0
- package/src/prompts/voice-summary.md +12 -0
- package/src/utils/image-convert.ts +26 -0
- package/src/utils/image-resize.ts +215 -0
- package/src/utils/shell-snapshot.ts +22 -20
package/src/discovery/helpers.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared helpers for discovery providers.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { join, resolve } from "
|
|
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
|
+
}
|
package/src/discovery/index.ts
CHANGED
|
@@ -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
|
-
|
|
100
|
-
|
|
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";
|