@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.
@@ -0,0 +1,206 @@
1
+ import { parseDiff } from "../parsers/diff-parser";
2
+ import { renderDiff } from "../render";
3
+ import type { DiffParams, DiffResult, FileDiff, ToolError, ToolResult } from "../types";
4
+ import { git } from "../utils";
5
+
6
+ interface StatusInfo {
7
+ status: FileDiff["status"];
8
+ oldPath?: string;
9
+ }
10
+
11
+ function parseNameStatus(output: string): Map<string, StatusInfo> {
12
+ const map = new Map<string, StatusInfo>();
13
+ for (const line of output.split("\n")) {
14
+ if (!line) continue;
15
+ const parts = line.split("\t");
16
+ const code = parts[0];
17
+ if (!code) continue;
18
+ const status = code[0];
19
+ if (status === "R" || status === "C") {
20
+ const oldPath = parts[1];
21
+ const newPath = parts[2];
22
+ if (newPath) {
23
+ map.set(newPath, {
24
+ status: status === "R" ? "renamed" : "copied",
25
+ oldPath,
26
+ });
27
+ }
28
+ continue;
29
+ }
30
+ const path = parts[1];
31
+ if (!path) continue;
32
+ const statusMap: Record<string, FileDiff["status"]> = {
33
+ A: "added",
34
+ M: "modified",
35
+ D: "deleted",
36
+ };
37
+ map.set(path, { status: statusMap[status] ?? "modified" });
38
+ }
39
+ return map;
40
+ }
41
+
42
+ function parseRenamePath(path: string): { oldPath: string; newPath: string } | null {
43
+ if (!path.includes("=>")) return null;
44
+ const braceMatch = path.match(/^(.*)\{(.*) => (.*)\}(.*)$/);
45
+ if (braceMatch) {
46
+ const prefix = braceMatch[1];
47
+ const oldMid = braceMatch[2];
48
+ const newMid = braceMatch[3];
49
+ const suffix = braceMatch[4];
50
+ return {
51
+ oldPath: `${prefix}${oldMid}${suffix}`,
52
+ newPath: `${prefix}${newMid}${suffix}`,
53
+ };
54
+ }
55
+ const parts = path.split("=>").map((part) => part.trim());
56
+ if (parts.length !== 2) return null;
57
+ return { oldPath: parts[0], newPath: parts[1] };
58
+ }
59
+
60
+ function parseNumstat(
61
+ output: string,
62
+ ): Map<string, { additions: number; deletions: number; binary: boolean; oldPath?: string }> {
63
+ const map = new Map<string, { additions: number; deletions: number; binary: boolean; oldPath?: string }>();
64
+ for (const line of output.split("\n")) {
65
+ if (!line) continue;
66
+ const parts = line.split("\t");
67
+ if (parts.length < 3) continue;
68
+ const additionsRaw = parts[0];
69
+ const deletionsRaw = parts[1];
70
+ const pathRaw = parts.slice(2).join("\t");
71
+ const additions = additionsRaw === "-" ? 0 : Number.parseInt(additionsRaw, 10);
72
+ const deletions = deletionsRaw === "-" ? 0 : Number.parseInt(deletionsRaw, 10);
73
+ const binary = additionsRaw === "-" || deletionsRaw === "-";
74
+ const rename = parseRenamePath(pathRaw);
75
+ if (rename) {
76
+ map.set(rename.newPath, { additions, deletions, binary, oldPath: rename.oldPath });
77
+ continue;
78
+ }
79
+ map.set(pathRaw, { additions, deletions, binary });
80
+ }
81
+ return map;
82
+ }
83
+
84
+ function buildDiffArgs(params: DiffParams): string[] {
85
+ const args: string[] = ["diff"];
86
+ if (params.target === "staged") {
87
+ args.push("--cached");
88
+ } else if (params.target === "head") {
89
+ args.push("HEAD");
90
+ } else if (params.target && typeof params.target === "object") {
91
+ // Ambiguity: when `to` is omitted, default to diffing against HEAD.
92
+ const toRef = params.target.to ?? "HEAD";
93
+ args.push(`${params.target.from}..${toRef}`);
94
+ }
95
+
96
+ if (params.ignore_whitespace) {
97
+ args.push("--ignore-all-space");
98
+ }
99
+ return args;
100
+ }
101
+
102
+ export async function diff(params: DiffParams, cwd?: string): Promise<ToolResult<DiffResult> | ToolError> {
103
+ const baseArgs = buildDiffArgs(params);
104
+ const paths = params.paths ?? [];
105
+
106
+ const nameStatusArgs = [...baseArgs, "--name-status"];
107
+ if (paths.length > 0) nameStatusArgs.push("--", ...paths);
108
+ const nameStatusResult = await git(nameStatusArgs, { cwd });
109
+ if (nameStatusResult.error) {
110
+ return { error: nameStatusResult.error.message, code: nameStatusResult.error.code };
111
+ }
112
+ const statusMap = parseNameStatus(nameStatusResult.stdout);
113
+
114
+ if (params.name_only || params.stat_only) {
115
+ const numstatArgs = [...baseArgs, "--numstat"];
116
+ if (paths.length > 0) numstatArgs.push("--", ...paths);
117
+ const numstatResult = await git(numstatArgs, { cwd });
118
+ if (numstatResult.error) {
119
+ return { error: numstatResult.error.message, code: numstatResult.error.code };
120
+ }
121
+ const statsMap = parseNumstat(numstatResult.stdout);
122
+ const files: FileDiff[] = [];
123
+ const seen = new Set<string>();
124
+
125
+ for (const [path, info] of statusMap.entries()) {
126
+ const stats = statsMap.get(path);
127
+ files.push({
128
+ path,
129
+ oldPath: info.oldPath ?? stats?.oldPath,
130
+ status: info.status,
131
+ binary: stats?.binary ?? false,
132
+ additions: stats?.additions ?? 0,
133
+ deletions: stats?.deletions ?? 0,
134
+ });
135
+ seen.add(path);
136
+ }
137
+
138
+ for (const [path, stats] of statsMap.entries()) {
139
+ if (seen.has(path)) continue;
140
+ files.push({
141
+ path,
142
+ oldPath: stats.oldPath,
143
+ status: "modified",
144
+ binary: stats.binary,
145
+ additions: stats.additions,
146
+ deletions: stats.deletions,
147
+ });
148
+ }
149
+
150
+ const summary = files.reduce(
151
+ (acc, file) => {
152
+ acc.filesChanged += 1;
153
+ acc.insertions += file.additions;
154
+ acc.deletions += file.deletions;
155
+ return acc;
156
+ },
157
+ { filesChanged: 0, insertions: 0, deletions: 0 },
158
+ );
159
+
160
+ const result: DiffResult = {
161
+ files,
162
+ stats: summary,
163
+ truncated: false,
164
+ };
165
+ return { data: result, _rendered: renderDiff(result) };
166
+ }
167
+
168
+ const diffArgs = [...baseArgs];
169
+ if (params.context !== undefined) {
170
+ diffArgs.push(`--unified=${params.context}`);
171
+ }
172
+ if (paths.length > 0) diffArgs.push("--", ...paths);
173
+
174
+ const diffResult = await git(diffArgs, { cwd });
175
+ if (diffResult.error) {
176
+ return { error: diffResult.error.message, code: diffResult.error.code };
177
+ }
178
+
179
+ const parsed = parseDiff(diffResult.stdout, { maxLines: params.max_lines });
180
+ for (const file of parsed.files) {
181
+ const info = statusMap.get(file.path);
182
+ if (info) {
183
+ file.status = info.status;
184
+ file.oldPath = info.oldPath ?? file.oldPath;
185
+ }
186
+ }
187
+
188
+ const summary = parsed.files.reduce(
189
+ (acc, file) => {
190
+ acc.filesChanged += 1;
191
+ acc.insertions += file.additions;
192
+ acc.deletions += file.deletions;
193
+ return acc;
194
+ },
195
+ { filesChanged: 0, insertions: 0, deletions: 0 },
196
+ );
197
+
198
+ const result: DiffResult = {
199
+ files: parsed.files,
200
+ stats: summary,
201
+ truncated: parsed.truncated,
202
+ truncatedFiles: parsed.truncatedFiles.length > 0 ? parsed.truncatedFiles : undefined,
203
+ };
204
+
205
+ return { data: result, _rendered: renderDiff(result) };
206
+ }
@@ -0,0 +1,49 @@
1
+ import { renderFetch } from "../render";
2
+ import type { FetchParams, FetchResult, ToolError, ToolResult } from "../types";
3
+ import { git } from "../utils";
4
+
5
+ async function getRemoteRefs(remote: string, branch?: string, cwd?: string): Promise<Map<string, string>> {
6
+ const refPrefix = branch ? `refs/remotes/${remote}/${branch}` : `refs/remotes/${remote}`;
7
+ const result = await git(["for-each-ref", refPrefix, "--format=%(refname)\t%(objectname)"], { cwd });
8
+ if (result.error) return new Map();
9
+ const map = new Map<string, string>();
10
+ for (const line of result.stdout.split("\n")) {
11
+ if (!line) continue;
12
+ const [ref, sha] = line.split("\t");
13
+ if (ref && sha) map.set(ref, sha);
14
+ }
15
+ return map;
16
+ }
17
+
18
+ export async function fetch(params: FetchParams, cwd?: string): Promise<ToolResult<FetchResult> | ToolError> {
19
+ const remote = params.remote ?? "origin";
20
+ const before = await getRemoteRefs(remote, params.branch, cwd);
21
+
22
+ const args = ["fetch"];
23
+ if (params.all) {
24
+ args.push("--all");
25
+ } else {
26
+ args.push(remote);
27
+ if (params.branch) args.push(params.branch);
28
+ }
29
+ if (params.prune) args.push("--prune");
30
+ if (params.tags) args.push("--tags");
31
+
32
+ const result = await git(args, { cwd });
33
+ if (result.error) {
34
+ return { error: result.error.message, code: result.error.code };
35
+ }
36
+
37
+ const after = await getRemoteRefs(remote, params.branch, cwd);
38
+ const updated: FetchResult["updated"] = [];
39
+ for (const [ref, newSha] of after.entries()) {
40
+ const oldSha = before.get(ref);
41
+ if (!oldSha || oldSha !== newSha) {
42
+ updated.push({ ref, oldSha: oldSha ?? "", newSha });
43
+ }
44
+ }
45
+ const pruned = params.prune ? Array.from(before.keys()).filter((ref) => !after.has(ref)) : undefined;
46
+
47
+ const data: FetchResult = { updated, ...(pruned && pruned.length > 0 ? { pruned } : {}) };
48
+ return { data, _rendered: renderFetch(data) };
49
+ }
@@ -0,0 +1,155 @@
1
+ import type {
2
+ CIActionResult,
3
+ CIParams,
4
+ CIResult,
5
+ JobInfo,
6
+ RunInfo,
7
+ RunListResult,
8
+ RunViewResult,
9
+ ToolError,
10
+ ToolResult,
11
+ } from "../../types";
12
+ import { gh } from "../../utils";
13
+
14
+ type GhRun = {
15
+ databaseId?: number;
16
+ id?: number;
17
+ displayTitle?: string;
18
+ name?: string;
19
+ status: RunInfo["status"];
20
+ conclusion?: RunInfo["conclusion"];
21
+ headBranch?: string;
22
+ branch?: string;
23
+ headSha?: string;
24
+ sha?: string;
25
+ url: string;
26
+ createdAt: string;
27
+ updatedAt: string;
28
+ jobs?: GhJob[];
29
+ };
30
+
31
+ type GhJob = {
32
+ databaseId?: number;
33
+ id?: number;
34
+ name: string;
35
+ status: string;
36
+ conclusion?: string;
37
+ steps?: Array<{ name: string; status: string; conclusion?: string }>;
38
+ };
39
+
40
+ function mapRunInfo(raw: GhRun): RunInfo {
41
+ return {
42
+ id: raw.databaseId ?? raw.id ?? 0,
43
+ name: raw.displayTitle ?? raw.name ?? "",
44
+ status: raw.status,
45
+ conclusion: raw.conclusion ?? undefined,
46
+ branch: raw.headBranch ?? raw.branch ?? "",
47
+ sha: raw.headSha ?? raw.sha ?? "",
48
+ url: raw.url,
49
+ createdAt: raw.createdAt,
50
+ updatedAt: raw.updatedAt,
51
+ };
52
+ }
53
+
54
+ function mapJobInfo(raw: GhJob): JobInfo {
55
+ return {
56
+ id: raw.databaseId ?? raw.id ?? 0,
57
+ name: raw.name,
58
+ status: raw.status,
59
+ conclusion: raw.conclusion ?? undefined,
60
+ steps: (raw.steps ?? []).map((step) => ({
61
+ name: step.name,
62
+ status: step.status,
63
+ conclusion: step.conclusion ?? undefined,
64
+ })),
65
+ };
66
+ }
67
+
68
+ export async function ci(params: CIParams, cwd?: string): Promise<ToolResult<CIResult> | ToolError> {
69
+ if (params.action === "list") {
70
+ const args = [
71
+ "run",
72
+ "list",
73
+ "--json",
74
+ "databaseId,displayTitle,status,conclusion,headBranch,headSha,url,createdAt,updatedAt",
75
+ ];
76
+ if (params.limit) args.push("--limit", String(params.limit));
77
+ if (params.branch) args.push("--branch", params.branch);
78
+ if (params.workflow) args.push("--workflow", params.workflow);
79
+ const result = await gh(args, { cwd });
80
+ if (result.error) {
81
+ return { error: result.error.message, code: result.error.code };
82
+ }
83
+ const raw = JSON.parse(result.stdout) as GhRun[];
84
+ const runs = raw.map((item) => mapRunInfo(item));
85
+ const data: RunListResult = { runs };
86
+ return { data, _rendered: `Runs: ${runs.length}` };
87
+ }
88
+
89
+ if (params.action === "view") {
90
+ if (!params.run_id) {
91
+ return { error: "run_id required" };
92
+ }
93
+ const result = await gh(
94
+ [
95
+ "run",
96
+ "view",
97
+ String(params.run_id),
98
+ "--json",
99
+ "databaseId,name,status,conclusion,headBranch,headSha,url,createdAt,updatedAt,jobs",
100
+ ],
101
+ { cwd },
102
+ );
103
+ if (result.error) {
104
+ return { error: result.error.message, code: result.error.code };
105
+ }
106
+ const raw = JSON.parse(result.stdout) as GhRun;
107
+ const run = mapRunInfo(raw);
108
+ const jobs: JobInfo[] = (raw.jobs ?? []).map((job) => mapJobInfo(job));
109
+ let logs: string | undefined;
110
+ if (params.logs_failed) {
111
+ const logsResult = await gh(["run", "view", String(params.run_id), "--log-failed"], { cwd });
112
+ if (!logsResult.error) logs = logsResult.stdout;
113
+ }
114
+ const data: RunViewResult = { run, jobs, ...(logs ? { logs } : {}) };
115
+ return { data, _rendered: `Run ${run.id} ${run.status}` };
116
+ }
117
+
118
+ if (params.action === "watch") {
119
+ if (!params.run_id) return { error: "run_id required" };
120
+ const result = await gh(["run", "watch", String(params.run_id), "--exit-status"], { cwd });
121
+ if (result.error) {
122
+ return { error: result.error.message, code: result.error.code };
123
+ }
124
+ const data: CIActionResult = { status: "success" };
125
+ return { data, _rendered: `Watched run ${params.run_id}` };
126
+ }
127
+
128
+ if (params.action === "run") {
129
+ if (!params.workflow) return { error: "workflow required" };
130
+ const args = ["workflow", "run", params.workflow];
131
+ if (params.inputs) {
132
+ for (const [key, value] of Object.entries(params.inputs)) {
133
+ args.push("--field", `${key}=${value}`);
134
+ }
135
+ }
136
+ const result = await gh(args, { cwd });
137
+ if (result.error) {
138
+ return { error: result.error.message, code: result.error.code };
139
+ }
140
+ const data: CIActionResult = { status: "success" };
141
+ return { data, _rendered: `Triggered workflow ${params.workflow}` };
142
+ }
143
+
144
+ if (params.action === "cancel" || params.action === "rerun") {
145
+ if (!params.run_id) return { error: "run_id required" };
146
+ const result = await gh(["run", params.action, String(params.run_id)], { cwd });
147
+ if (result.error) {
148
+ return { error: result.error.message, code: result.error.code };
149
+ }
150
+ const data: CIActionResult = { status: "success" };
151
+ return { data, _rendered: `${params.action} run ${params.run_id}` };
152
+ }
153
+
154
+ return { error: `Unknown CI action: ${params.action}` };
155
+ }
@@ -0,0 +1,132 @@
1
+ import type {
2
+ IssueCreateResult,
3
+ IssueInfo,
4
+ IssueListResult,
5
+ IssueParams,
6
+ IssueResult,
7
+ ToolError,
8
+ ToolResult,
9
+ } from "../../types";
10
+ import { gh } from "../../utils";
11
+
12
+ type GhIssue = {
13
+ number: number;
14
+ title: string;
15
+ state: string;
16
+ author?: { login?: string } | string;
17
+ body?: string | null;
18
+ labels?: Array<{ name?: string } | string>;
19
+ assignees?: Array<{ login?: string } | string>;
20
+ url: string;
21
+ createdAt: string;
22
+ comments?: number;
23
+ };
24
+
25
+ function mapIssueInfo(raw: GhIssue): IssueInfo {
26
+ const author = typeof raw.author === "string" ? raw.author : (raw.author?.login ?? "");
27
+ return {
28
+ number: raw.number,
29
+ title: raw.title,
30
+ state: raw.state,
31
+ author,
32
+ body: raw.body ?? "",
33
+ labels: (raw.labels ?? [])
34
+ .map((label) => (typeof label === "string" ? label : (label.name ?? "")))
35
+ .filter(Boolean),
36
+ assignees: (raw.assignees ?? [])
37
+ .map((assignee) => (typeof assignee === "string" ? assignee : (assignee.login ?? "")))
38
+ .filter(Boolean),
39
+ url: raw.url,
40
+ createdAt: raw.createdAt,
41
+ comments: raw.comments ?? 0,
42
+ };
43
+ }
44
+
45
+ export async function issue(params: IssueParams, cwd?: string): Promise<ToolResult<IssueResult> | ToolError> {
46
+ if (params.action === "list") {
47
+ const args = [
48
+ "issue",
49
+ "list",
50
+ "--json",
51
+ "number,title,state,author,body,labels,assignees,url,createdAt,comments",
52
+ ];
53
+ if (params.state) args.push("--state", params.state);
54
+ if (params.labels && params.labels.length > 0) args.push("--label", params.labels.join(","));
55
+ if (params.assignee) args.push("--assignee", params.assignee);
56
+ if (params.limit) args.push("--limit", String(params.limit));
57
+ const result = await gh(args, { cwd });
58
+ if (result.error) {
59
+ return { error: result.error.message, code: result.error.code };
60
+ }
61
+ const raw = JSON.parse(result.stdout) as GhIssue[];
62
+ const issues = raw.map((item) => mapIssueInfo(item));
63
+ const data: IssueListResult = { issues };
64
+ return { data, _rendered: `Issues: ${issues.length}` };
65
+ }
66
+
67
+ if (params.action === "view") {
68
+ if (!params.number) {
69
+ return { error: "Issue number required" };
70
+ }
71
+ const result = await gh(
72
+ [
73
+ "issue",
74
+ "view",
75
+ String(params.number),
76
+ "--json",
77
+ "number,title,state,author,body,labels,assignees,url,createdAt,comments",
78
+ ],
79
+ { cwd },
80
+ );
81
+ if (result.error) {
82
+ return { error: result.error.message, code: result.error.code };
83
+ }
84
+ const issueInfo = mapIssueInfo(JSON.parse(result.stdout) as GhIssue);
85
+ return { data: { issue: issueInfo }, _rendered: `Issue #${issueInfo.number}: ${issueInfo.title}` };
86
+ }
87
+
88
+ if (params.action === "create") {
89
+ const args = ["issue", "create"];
90
+ if (params.title) args.push("--title", params.title);
91
+ if (params.body) args.push("--body", params.body);
92
+ const result = await gh(args, { cwd });
93
+ if (result.error) {
94
+ return { error: result.error.message, code: result.error.code };
95
+ }
96
+ let number = 0;
97
+ let url = "";
98
+ const match = result.stdout.match(/https?:\/\/\S+/);
99
+ if (match) {
100
+ url = match[0];
101
+ const numMatch = url.match(/issues\/(\d+)/);
102
+ if (numMatch) number = Number.parseInt(numMatch[1], 10);
103
+ }
104
+ const data: IssueCreateResult = { number, url };
105
+ return { data, _rendered: url ? `Created issue ${url}` : "Created issue" };
106
+ }
107
+
108
+ if (params.action === "close" || params.action === "reopen") {
109
+ if (!params.number) {
110
+ return { error: "Issue number required" };
111
+ }
112
+ const result = await gh(["issue", params.action, String(params.number)], { cwd });
113
+ if (result.error) {
114
+ return { error: result.error.message, code: result.error.code };
115
+ }
116
+ const verb = params.action === "close" ? "Closed" : "Reopened";
117
+ return { data: { status: "success" }, _rendered: `${verb} issue #${params.number}` };
118
+ }
119
+
120
+ if (params.action === "comment") {
121
+ if (!params.number || !params.comment_body) {
122
+ return { error: "Issue number and comment_body required" };
123
+ }
124
+ const result = await gh(["issue", "comment", String(params.number), "--body", params.comment_body], { cwd });
125
+ if (result.error) {
126
+ return { error: result.error.message, code: result.error.code };
127
+ }
128
+ return { data: { status: "success" }, _rendered: `Commented on issue #${params.number}` };
129
+ }
130
+
131
+ return { error: `Unknown issue action: ${params.action}` };
132
+ }