@oh-my-pi/pi-git-tool 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 ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [3.20.0] - 2026-01-06
6
+
7
+ ### Breaking Changes
8
+
9
+ ### Added
10
+
11
+ - Added structured git tool with safety guards, caching, and GitHub operations
12
+
13
+ ### Changed
14
+
15
+ ### Fixed
16
+
17
+ ### Removed
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@oh-my-pi/pi-git-tool",
3
+ "version": "3.20.0",
4
+ "description": "Structured Git tool with safety guards and typed output",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "import": "./src/index.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "src",
16
+ "CHANGELOG.md"
17
+ ],
18
+ "scripts": {
19
+ "check": "tsgo --noEmit",
20
+ "build": "tsgo -p tsconfig.build.json",
21
+ "test": "vitest --run"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^24.3.0",
25
+ "vitest": "^3.2.4"
26
+ },
27
+ "keywords": [
28
+ "git",
29
+ "tool",
30
+ "agent"
31
+ ],
32
+ "author": "Can Bölük",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/can1357/oh-my-pi.git",
37
+ "directory": "packages/git-tool"
38
+ },
39
+ "engines": {
40
+ "bun": ">=1.0.0"
41
+ }
42
+ }
@@ -0,0 +1,35 @@
1
+ import type { BranchListResult, Commit, StatusResult } from "../types";
2
+
3
+ export interface CacheEntry<T> {
4
+ value: T;
5
+ timestamp: number;
6
+ ttl: number;
7
+ cwd: string;
8
+ }
9
+
10
+ export interface GitCache {
11
+ branch: CacheEntry<BranchListResult> | null;
12
+ status: CacheEntry<StatusResult> | null;
13
+ remotes: CacheEntry<Array<{ name: string; url: string }>> | null;
14
+ commits: Map<string, CacheEntry<Commit>>;
15
+ }
16
+
17
+ export const DEFAULT_TTL = {
18
+ branch: 30_000,
19
+ status: 5_000,
20
+ remotes: 60_000,
21
+ commits: 300_000,
22
+ };
23
+
24
+ export function createCache(): GitCache {
25
+ return {
26
+ branch: null,
27
+ status: null,
28
+ remotes: null,
29
+ commits: new Map(),
30
+ };
31
+ }
32
+
33
+ export function isExpired(entry: CacheEntry<unknown>): boolean {
34
+ return Date.now() - entry.timestamp > entry.ttl;
35
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,84 @@
1
+ export enum GitErrorCode {
2
+ NOT_A_REPO = "NOT_A_REPO",
3
+ CONFLICT = "CONFLICT",
4
+ UNCOMMITTED_CHANGES = "UNCOMMITTED_CHANGES",
5
+ BRANCH_NOT_FOUND = "BRANCH_NOT_FOUND",
6
+ REF_NOT_FOUND = "REF_NOT_FOUND",
7
+ REMOTE_REJECTED = "REMOTE_REJECTED",
8
+ AUTH_FAILED = "AUTH_FAILED",
9
+ MERGE_CONFLICT = "MERGE_CONFLICT",
10
+ REBASE_CONFLICT = "REBASE_CONFLICT",
11
+ NOTHING_TO_COMMIT = "NOTHING_TO_COMMIT",
12
+ DETACHED_HEAD = "DETACHED_HEAD",
13
+ HOOK_FAILED = "HOOK_FAILED",
14
+ GH_NOT_INSTALLED = "GH_NOT_INSTALLED",
15
+ GH_AUTH_REQUIRED = "GH_AUTH_REQUIRED",
16
+ UNKNOWN = "UNKNOWN",
17
+ }
18
+
19
+ export class GitError extends Error {
20
+ code: GitErrorCode;
21
+ details?: Record<string, unknown>;
22
+
23
+ constructor(message: string, code: GitErrorCode, details?: Record<string, unknown>) {
24
+ super(message);
25
+ this.name = "GitError";
26
+ this.code = code;
27
+ this.details = details;
28
+ }
29
+ }
30
+
31
+ export function detectGitError(stderr: string, exitCode: number): GitError | null {
32
+ if (exitCode === 0) return null;
33
+
34
+ const normalized = stderr.toLowerCase();
35
+ if (normalized.includes("not a git repository")) {
36
+ return new GitError("Not a git repository", GitErrorCode.NOT_A_REPO);
37
+ }
38
+ if (normalized.includes("authentication failed") || normalized.includes("fatal: authentication")) {
39
+ return new GitError("Authentication failed", GitErrorCode.AUTH_FAILED);
40
+ }
41
+ if (normalized.includes("permission denied") || normalized.includes("access denied")) {
42
+ return new GitError("Authentication failed", GitErrorCode.AUTH_FAILED);
43
+ }
44
+ if (normalized.includes("nothing to commit")) {
45
+ return new GitError("Nothing to commit", GitErrorCode.NOTHING_TO_COMMIT);
46
+ }
47
+ if (normalized.includes("detached head")) {
48
+ return new GitError("Detached HEAD", GitErrorCode.DETACHED_HEAD);
49
+ }
50
+ if (normalized.includes("merge conflict") || normalized.includes("conflict")) {
51
+ return new GitError("Merge conflict", GitErrorCode.MERGE_CONFLICT);
52
+ }
53
+ if (normalized.includes("rebase")) {
54
+ return new GitError("Rebase conflict", GitErrorCode.REBASE_CONFLICT);
55
+ }
56
+ if (normalized.includes("unknown revision") || normalized.includes("bad revision")) {
57
+ return new GitError("Ref not found", GitErrorCode.REF_NOT_FOUND);
58
+ }
59
+ if (normalized.includes("pathspec") && normalized.includes("did not match")) {
60
+ return new GitError("Ref not found", GitErrorCode.REF_NOT_FOUND);
61
+ }
62
+ if (normalized.includes("hook") && normalized.includes("failed")) {
63
+ return new GitError("Hook failed", GitErrorCode.HOOK_FAILED);
64
+ }
65
+ if (normalized.includes("remote rejected") || normalized.includes("rejected")) {
66
+ return new GitError("Remote rejected", GitErrorCode.REMOTE_REJECTED);
67
+ }
68
+
69
+ return new GitError(stderr.trim() || "Unknown git error", GitErrorCode.UNKNOWN);
70
+ }
71
+
72
+ export function detectGhError(stderr: string, exitCode: number): GitError | null {
73
+ if (exitCode === 0) return null;
74
+
75
+ const normalized = stderr.toLowerCase();
76
+ if (normalized.includes("not logged") || normalized.includes("authentication required")) {
77
+ return new GitError("GitHub CLI authentication required", GitErrorCode.GH_AUTH_REQUIRED);
78
+ }
79
+ if (normalized.includes("gh: not found") || normalized.includes("gh: command not found")) {
80
+ return new GitError("GitHub CLI is not installed", GitErrorCode.GH_NOT_INSTALLED);
81
+ }
82
+
83
+ return new GitError(stderr.trim() || "GitHub CLI error", GitErrorCode.UNKNOWN);
84
+ }
@@ -0,0 +1,169 @@
1
+ import { createCache, DEFAULT_TTL, isExpired } from "./cache/git-cache";
2
+ import { add } from "./operations/add";
3
+ import { blame } from "./operations/blame";
4
+ import { branch } from "./operations/branch";
5
+ import { checkout } from "./operations/checkout";
6
+ import { cherryPick } from "./operations/cherry-pick";
7
+ import { commit } from "./operations/commit";
8
+ import { diff } from "./operations/diff";
9
+ import { fetch } from "./operations/fetch";
10
+ import { ci } from "./operations/github/ci";
11
+ import { issue } from "./operations/github/issue";
12
+ import { pr } from "./operations/github/pr";
13
+ import { release } from "./operations/github/release";
14
+ import { log } from "./operations/log";
15
+ import { merge } from "./operations/merge";
16
+ import { pull } from "./operations/pull";
17
+ import { push } from "./operations/push";
18
+ import { rebase } from "./operations/rebase";
19
+ import { restore } from "./operations/restore";
20
+ import { show } from "./operations/show";
21
+ import { stash } from "./operations/stash";
22
+ import { status } from "./operations/status";
23
+ import { tag } from "./operations/tag";
24
+ import { renderBranchList, renderStatus } from "./render";
25
+ import { checkSafety } from "./safety/guards";
26
+ import type { BranchListResult, GitParams, Operation, StatusResult, ToolResponse, ToolResult } from "./types";
27
+ import { isTruthy } from "./utils";
28
+
29
+ const cache = createCache();
30
+
31
+ type OperationHandler = (params: GitParams, cwd?: string) => Promise<ToolResponse<unknown>>;
32
+
33
+ const operations: Record<Operation, OperationHandler> = {
34
+ status: status as OperationHandler,
35
+ diff: diff as OperationHandler,
36
+ log: log as OperationHandler,
37
+ show: show as OperationHandler,
38
+ blame: blame as OperationHandler,
39
+ branch: branch as OperationHandler,
40
+ add: add as OperationHandler,
41
+ restore: restore as OperationHandler,
42
+ commit: commit as OperationHandler,
43
+ checkout: checkout as OperationHandler,
44
+ merge: merge as OperationHandler,
45
+ rebase: rebase as OperationHandler,
46
+ stash: stash as OperationHandler,
47
+ "cherry-pick": cherryPick as OperationHandler,
48
+ fetch: fetch as OperationHandler,
49
+ pull: pull as OperationHandler,
50
+ push: push as OperationHandler,
51
+ tag: tag as OperationHandler,
52
+ pr: pr as OperationHandler,
53
+ issue: issue as OperationHandler,
54
+ ci: ci as OperationHandler,
55
+ release: release as OperationHandler,
56
+ };
57
+
58
+ const READ_OPERATIONS: Operation[] = ["status", "diff", "log", "show", "blame", "branch"];
59
+ const WRITE_OPERATIONS: Operation[] = [
60
+ "add",
61
+ "restore",
62
+ "commit",
63
+ "checkout",
64
+ "merge",
65
+ "rebase",
66
+ "stash",
67
+ "cherry-pick",
68
+ "pull",
69
+ "fetch",
70
+ ];
71
+
72
+ function invalidateOnWrite(operation: Operation): void {
73
+ if (WRITE_OPERATIONS.includes(operation)) {
74
+ cache.status = null;
75
+ if (["checkout", "merge", "rebase"].includes(operation)) {
76
+ cache.branch = null;
77
+ }
78
+ }
79
+ }
80
+
81
+ function cacheStatus(result: ToolResult<StatusResult>, cwd: string): void {
82
+ cache.status = { value: result.data, timestamp: Date.now(), ttl: DEFAULT_TTL.status, cwd };
83
+ }
84
+
85
+ function cacheBranch(result: ToolResult<BranchListResult>, cwd: string): void {
86
+ cache.branch = { value: result.data, timestamp: Date.now(), ttl: DEFAULT_TTL.branch, cwd };
87
+ }
88
+
89
+ function getCachedStatus(cwd: string): ToolResult<StatusResult> | null {
90
+ if (!cache.status) return null;
91
+ if (isExpired(cache.status)) {
92
+ cache.status = null;
93
+ return null;
94
+ }
95
+ if (cache.status.cwd !== cwd) return null;
96
+ return {
97
+ data: cache.status.value,
98
+ _rendered: renderStatus(cache.status.value),
99
+ };
100
+ }
101
+
102
+ function getCachedBranch(cwd: string): ToolResult<BranchListResult> | null {
103
+ if (!cache.branch) return null;
104
+ if (isExpired(cache.branch)) {
105
+ cache.branch = null;
106
+ return null;
107
+ }
108
+ if (cache.branch.cwd !== cwd) return null;
109
+ return {
110
+ data: cache.branch.value,
111
+ _rendered: renderBranchList(cache.branch.value),
112
+ };
113
+ }
114
+
115
+ export async function gitTool(params: GitParams, cwd?: string): Promise<ToolResponse<unknown>> {
116
+ const resolvedCwd = cwd ?? process.cwd();
117
+ const operation = params.operation as Operation;
118
+ const handler = operations[operation];
119
+ if (!handler) {
120
+ return { error: `Unknown operation: ${operation}` };
121
+ }
122
+
123
+ const paramRecord = params as unknown as Record<string, unknown>;
124
+ const safety = await checkSafety(operation, paramRecord, resolvedCwd);
125
+ if (safety.blocked) {
126
+ const overrideValue = safety.override ? paramRecord[safety.override] : undefined;
127
+ if (!safety.override || !isTruthy(String(overrideValue ?? ""))) {
128
+ return { error: safety.message ?? "Operation blocked", suggestion: safety.suggestion, code: "SAFETY_BLOCK" };
129
+ }
130
+ }
131
+ if (safety.confirm) {
132
+ const overrideValue = safety.override ? paramRecord[safety.override] : undefined;
133
+ if (!safety.override || !isTruthy(String(overrideValue ?? ""))) {
134
+ return {
135
+ confirm: safety.message ?? "Confirmation required",
136
+ override: safety.override ?? "confirm",
137
+ _rendered: safety.message,
138
+ };
139
+ }
140
+ }
141
+
142
+ if (READ_OPERATIONS.includes(operation)) {
143
+ if (operation === "status") {
144
+ const cached = getCachedStatus(resolvedCwd);
145
+ const statusParams = params as { only?: string; ignored?: boolean };
146
+ if (cached && !statusParams.only && !statusParams.ignored) return cached;
147
+ }
148
+ if (operation === "branch") {
149
+ const cached = getCachedBranch(resolvedCwd);
150
+ const branchParams = params as { action?: string; remotes?: boolean };
151
+ if (cached && (!branchParams.action || branchParams.action === "list") && !branchParams.remotes) return cached;
152
+ }
153
+ }
154
+
155
+ const result = await handler(params, resolvedCwd);
156
+
157
+ invalidateOnWrite(operation);
158
+ if ("data" in result && READ_OPERATIONS.includes(operation)) {
159
+ if (operation === "status") cacheStatus(result as ToolResult<StatusResult>, resolvedCwd);
160
+ if (operation === "branch") cacheBranch(result as ToolResult<BranchListResult>, resolvedCwd);
161
+ }
162
+
163
+ if ("data" in result && safety.warnings.length > 0) {
164
+ const suffix = `\n\nWarnings:\n${safety.warnings.map((warn) => `- ${warn}`).join("\n")}`;
165
+ result._rendered = `${result._rendered ?? ""}${suffix}`;
166
+ }
167
+
168
+ return result;
169
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./errors";
2
+ export { gitTool } from "./git-tool";
3
+ export * from "./types";
@@ -0,0 +1,42 @@
1
+ import { renderAdd } from "../render";
2
+ import type { AddParams, AddResult, ToolError, ToolResult } from "../types";
3
+ import { git } from "../utils";
4
+
5
+ function parseDryRun(output: string): string[] {
6
+ const files: string[] = [];
7
+ for (const line of output.split("\n")) {
8
+ const match = line.match(/add ['"]?(.*?)['"]?$/i);
9
+ if (match) {
10
+ files.push(match[1]);
11
+ }
12
+ }
13
+ return files;
14
+ }
15
+
16
+ export async function add(params: AddParams, cwd?: string): Promise<ToolResult<AddResult> | ToolError> {
17
+ const args = ["add"];
18
+ if (params.dry_run) args.push("--dry-run");
19
+ if (params.update) args.push("-u");
20
+ if (params.all) args.push("-A");
21
+ if (params.paths && params.paths.length > 0) {
22
+ args.push("--", ...params.paths);
23
+ }
24
+
25
+ const result = await git(args, { cwd });
26
+ if (result.error) {
27
+ return { error: result.error.message, code: result.error.code };
28
+ }
29
+
30
+ let staged: string[] = [];
31
+ if (params.dry_run) {
32
+ staged = parseDryRun(result.stdout);
33
+ } else {
34
+ const stagedResult = await git(["diff", "--name-only", "--cached"], { cwd });
35
+ if (!stagedResult.error) {
36
+ staged = stagedResult.stdout.split("\n").filter(Boolean);
37
+ }
38
+ }
39
+
40
+ const data: AddResult = { staged };
41
+ return { data, _rendered: renderAdd(data) };
42
+ }
@@ -0,0 +1,23 @@
1
+ import { parseBlame } from "../parsers/blame-parser";
2
+ import { renderBlame } from "../render";
3
+ import type { BlameParams, BlameResult, ToolError, ToolResult } from "../types";
4
+ import { git } from "../utils";
5
+
6
+ export async function blame(params: BlameParams, cwd?: string): Promise<ToolResult<BlameResult> | ToolError> {
7
+ const args = ["blame", "--porcelain"];
8
+ if (params.root) args.push("--root");
9
+ if (params.ignore_whitespace) args.push("-w");
10
+ if (params.lines) {
11
+ args.push("-L", `${params.lines.start},${params.lines.end}`);
12
+ }
13
+ args.push(params.path);
14
+
15
+ const result = await git(args, { cwd });
16
+ if (result.error) {
17
+ return { error: result.error.message, code: result.error.code };
18
+ }
19
+
20
+ const parsed = parseBlame(result.stdout);
21
+ const data: BlameResult = { lines: parsed };
22
+ return { data, _rendered: renderBlame(data) };
23
+ }
@@ -0,0 +1,115 @@
1
+ import { renderBranchList } from "../render";
2
+ import type { BranchInfo, BranchListResult, BranchParams, ToolError, ToolResult } from "../types";
3
+ import { git } from "../utils";
4
+
5
+ function parseTrack(track: string): { ahead?: number; behind?: number; gone?: boolean } {
6
+ const result: { ahead?: number; behind?: number; gone?: boolean } = {};
7
+ if (!track) return result;
8
+ if (track.includes("gone")) result.gone = true;
9
+ const aheadMatch = track.match(/ahead (\d+)/);
10
+ const behindMatch = track.match(/behind (\d+)/);
11
+ if (aheadMatch) result.ahead = Number.parseInt(aheadMatch[1], 10);
12
+ if (behindMatch) result.behind = Number.parseInt(behindMatch[1], 10);
13
+ return result;
14
+ }
15
+
16
+ function parseBranchLines(output: string): BranchInfo[] {
17
+ const branches: BranchInfo[] = [];
18
+ for (const line of output.split("\n")) {
19
+ if (!line) continue;
20
+ const parts = line.split("\x00");
21
+ if (parts.length < 2) continue;
22
+ const name = parts[0];
23
+ const sha = parts[1];
24
+ const upstream = parts[2] || undefined;
25
+ const track = parts[3] || "";
26
+ const { ahead, behind, gone } = parseTrack(track);
27
+ branches.push({ name, sha, upstream, ahead, behind, gone });
28
+ }
29
+ return branches;
30
+ }
31
+
32
+ export async function branch(params: BranchParams, cwd?: string): Promise<ToolResult<BranchListResult> | ToolError> {
33
+ const action = params.action ?? "list";
34
+
35
+ if (action === "current") {
36
+ const currentResult = await git(["branch", "--show-current"], { cwd });
37
+ if (currentResult.error) {
38
+ return { error: currentResult.error.message, code: currentResult.error.code };
39
+ }
40
+ const data: BranchListResult = { current: currentResult.stdout.trim(), local: [] };
41
+ return { data, _rendered: renderBranchList(data) };
42
+ }
43
+
44
+ if (action === "create") {
45
+ if (!params.name) {
46
+ return { error: "Branch name required" };
47
+ }
48
+ const args = ["branch", params.name];
49
+ if (params.startPoint) args.push(params.startPoint);
50
+ const result = await git(args, { cwd });
51
+ if (result.error) {
52
+ return { error: result.error.message, code: result.error.code };
53
+ }
54
+ }
55
+
56
+ if (action === "delete") {
57
+ if (!params.name) {
58
+ return { error: "Branch name required" };
59
+ }
60
+ const args = ["branch", params.force ? "-D" : "-d", params.name];
61
+ const result = await git(args, { cwd });
62
+ if (result.error) {
63
+ return { error: result.error.message, code: result.error.code };
64
+ }
65
+ }
66
+
67
+ if (action === "rename") {
68
+ if (!params.name || !params.newName) {
69
+ return { error: "Branch name and newName required" };
70
+ }
71
+ const result = await git(["branch", "-m", params.name, params.newName], { cwd });
72
+ if (result.error) {
73
+ return { error: result.error.message, code: result.error.code };
74
+ }
75
+ }
76
+
77
+ const currentResult = await git(["branch", "--show-current"], { cwd });
78
+ if (currentResult.error) {
79
+ return { error: currentResult.error.message, code: currentResult.error.code };
80
+ }
81
+ const current = currentResult.stdout.trim();
82
+
83
+ const listResult = await git(
84
+ [
85
+ "branch",
86
+ "-vv",
87
+ "--format=%(refname:short)%x00%(objectname:short)%x00%(upstream:short)%x00%(upstream:track,nobracket)",
88
+ ],
89
+ { cwd },
90
+ );
91
+ if (listResult.error) {
92
+ return { error: listResult.error.message, code: listResult.error.code };
93
+ }
94
+ const local = parseBranchLines(listResult.stdout);
95
+
96
+ let remote: BranchInfo[] | undefined;
97
+ if (params.remotes) {
98
+ const remoteResult = await git(
99
+ [
100
+ "branch",
101
+ "-r",
102
+ "-vv",
103
+ "--format=%(refname:short)%x00%(objectname:short)%x00%(upstream:short)%x00%(upstream:track,nobracket)",
104
+ ],
105
+ { cwd },
106
+ );
107
+ if (remoteResult.error) {
108
+ return { error: remoteResult.error.message, code: remoteResult.error.code };
109
+ }
110
+ remote = parseBranchLines(remoteResult.stdout);
111
+ }
112
+
113
+ const data: BranchListResult = { current, local, ...(remote ? { remote } : {}) };
114
+ return { data, _rendered: renderBranchList(data) };
115
+ }
@@ -0,0 +1,37 @@
1
+ import type { CheckoutParams, CheckoutResult, ToolError, ToolResult } from "../types";
2
+ import { git } from "../utils";
3
+
4
+ export async function checkout(params: CheckoutParams, cwd?: string): Promise<ToolResult<CheckoutResult> | ToolError> {
5
+ const currentResult = await git(["rev-parse", "--abbrev-ref", "HEAD"], { cwd });
6
+ const previous = currentResult.error ? undefined : currentResult.stdout.trim();
7
+
8
+ if (params.paths && params.paths.length > 0) {
9
+ const args = ["checkout"];
10
+ if (params.ref) args.push(params.ref);
11
+ if (params.force) args.push("--force");
12
+ args.push("--", ...params.paths);
13
+ const result = await git(args, { cwd });
14
+ if (result.error) {
15
+ return { error: result.error.message, code: result.error.code };
16
+ }
17
+ const data: CheckoutResult = { previous, restoredFiles: params.paths };
18
+ return { data, _rendered: `Restored ${params.paths.length} files` };
19
+ }
20
+
21
+ if (!params.ref) {
22
+ return { error: "Ref is required for checkout" };
23
+ }
24
+
25
+ const args = ["checkout"];
26
+ if (params.create) args.push("-b");
27
+ if (params.force) args.push("--force");
28
+ args.push(params.ref);
29
+
30
+ const result = await git(args, { cwd });
31
+ if (result.error) {
32
+ return { error: result.error.message, code: result.error.code };
33
+ }
34
+
35
+ const data: CheckoutResult = { branch: params.ref, previous };
36
+ return { data, _rendered: `Checked out ${params.ref}` };
37
+ }
@@ -0,0 +1,39 @@
1
+ import { GitErrorCode } from "../errors";
2
+ import { renderCherryPick } from "../render";
3
+ import type { CherryPickParams, CherryPickResult, ToolError, ToolResult } from "../types";
4
+ import { git } from "../utils";
5
+
6
+ async function getConflicts(cwd?: string): Promise<string[]> {
7
+ const result = await git(["diff", "--name-only", "--diff-filter=U"], { cwd });
8
+ if (result.error) return [];
9
+ return result.stdout.split("\n").filter(Boolean);
10
+ }
11
+
12
+ export async function cherryPick(
13
+ params: CherryPickParams,
14
+ cwd?: string,
15
+ ): Promise<ToolResult<CherryPickResult> | ToolError> {
16
+ const args = ["cherry-pick"];
17
+ if (params.abort) args.push("--abort");
18
+ if (params.continue) args.push("--continue");
19
+ if (params.no_commit) args.push("--no-commit");
20
+ if (!params.abort && !params.continue) {
21
+ if (!params.commits || params.commits.length === 0) {
22
+ return { error: "Commits are required for cherry-pick" };
23
+ }
24
+ args.push(...params.commits);
25
+ }
26
+
27
+ const result = await git(args, { cwd });
28
+ if (result.error) {
29
+ if (result.error.code === GitErrorCode.MERGE_CONFLICT) {
30
+ const conflicts = await getConflicts(cwd);
31
+ const data: CherryPickResult = { status: "conflict", conflicts };
32
+ return { data, _rendered: renderCherryPick(data) };
33
+ }
34
+ return { error: result.error.message, code: result.error.code };
35
+ }
36
+
37
+ const data: CherryPickResult = { status: "success", appliedCommits: params.commits };
38
+ return { data, _rendered: renderCherryPick(data) };
39
+ }
@@ -0,0 +1,52 @@
1
+ import { renderCommit } from "../render";
2
+ import { markCommitCreated } from "../safety/guards";
3
+ import type { CommitParams, CommitResult, ToolError, ToolResult } from "../types";
4
+ import { git, parseShortstat } from "../utils";
5
+
6
+ export async function commit(params: CommitParams, cwd?: string): Promise<ToolResult<CommitResult> | ToolError> {
7
+ if (!params.message || params.message.trim().length === 0) {
8
+ return { error: "Commit message is required" };
9
+ }
10
+ const args = ["commit", "-m", params.message];
11
+ if (params.all) args.push("--all");
12
+ if (params.allow_empty) args.push("--allow-empty");
13
+ if (params.sign) args.push("-S");
14
+ if (params.no_verify) args.push("--no-verify");
15
+ if (params.amend) args.push("--amend");
16
+
17
+ const result = await git(args, { cwd });
18
+ if (result.error) {
19
+ return { error: result.error.message, code: result.error.code };
20
+ }
21
+
22
+ const shaResult = await git(["rev-parse", "HEAD"], { cwd });
23
+ if (shaResult.error) {
24
+ return { error: shaResult.error.message, code: shaResult.error.code };
25
+ }
26
+ const sha = shaResult.stdout.trim();
27
+ markCommitCreated(sha);
28
+
29
+ const subjectResult = await git(["show", "-s", "--format=%s", "HEAD"], { cwd });
30
+ if (subjectResult.error) {
31
+ return { error: subjectResult.error.message, code: subjectResult.error.code };
32
+ }
33
+ const subject = subjectResult.stdout.trim();
34
+
35
+ const statResult = await git(["show", "-s", "--shortstat", "HEAD"], { cwd });
36
+ let stats = { additions: 0, deletions: 0, files: 0 };
37
+ if (!statResult.error) {
38
+ const line = statResult.stdout.split("\n").find((statLine) => statLine.includes("files changed"));
39
+ const parsed = line ? parseShortstat(line) : null;
40
+ if (parsed) {
41
+ stats = { files: parsed.files, additions: parsed.additions, deletions: parsed.deletions };
42
+ }
43
+ }
44
+
45
+ const data: CommitResult = {
46
+ sha,
47
+ shortSha: sha.slice(0, 7),
48
+ subject,
49
+ stats,
50
+ };
51
+ return { data, _rendered: renderCommit(data) };
52
+ }