pi-graphite 0.1.1 → 0.2.1
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/package.json +1 -1
- package/src/lib/exec.ts +12 -4
- package/src/lib/result.ts +223 -27
- package/src/tools/branch.ts +118 -42
- package/src/tools/pr.ts +75 -29
- package/src/tools/recovery.ts +4 -3
- package/src/tools/remote.ts +4 -3
- package/src/tools/repo.ts +29 -29
- package/src/tools/stack.ts +42 -33
package/package.json
CHANGED
package/src/lib/exec.ts
CHANGED
|
@@ -28,6 +28,14 @@ export function stripAnsi(s: string): string {
|
|
|
28
28
|
return s.replace(ANSI, "");
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Replace Graphite's internal product name "Charcoal" with "Graphite" so
|
|
33
|
+
* users and the LLM see a consistent product name. Casing is preserved.
|
|
34
|
+
*/
|
|
35
|
+
export function sanitizeBranding(s: string): string {
|
|
36
|
+
return s.replace(/charcoal/gi, (m) => (m[0] === m[0].toUpperCase() ? "Graphite" : "graphite"));
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
export function truncateOutput(s: string): string {
|
|
32
40
|
if (s.length <= MAX_BYTES) {
|
|
33
41
|
const lines = s.split("\n");
|
|
@@ -109,8 +117,8 @@ export async function runGt(
|
|
|
109
117
|
args,
|
|
110
118
|
cwd,
|
|
111
119
|
exitCode: -1,
|
|
112
|
-
stdout: stripAnsi(stdout),
|
|
113
|
-
stderr: stripAnsi(stderr),
|
|
120
|
+
stdout: sanitizeBranding(stripAnsi(stdout)),
|
|
121
|
+
stderr: sanitizeBranding(stripAnsi(stderr)),
|
|
114
122
|
timedOut: false,
|
|
115
123
|
spawnError: err.message,
|
|
116
124
|
});
|
|
@@ -123,8 +131,8 @@ export async function runGt(
|
|
|
123
131
|
args,
|
|
124
132
|
cwd,
|
|
125
133
|
exitCode: code ?? -1,
|
|
126
|
-
stdout: truncateOutput(stripAnsi(stdout)),
|
|
127
|
-
stderr: truncateOutput(stripAnsi(stderr)),
|
|
134
|
+
stdout: truncateOutput(sanitizeBranding(stripAnsi(stdout))),
|
|
135
|
+
stderr: truncateOutput(sanitizeBranding(stripAnsi(stderr))),
|
|
128
136
|
timedOut: killed,
|
|
129
137
|
});
|
|
130
138
|
});
|
package/src/lib/result.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import type { GtRunResult } from "./exec";
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Structured failure hints. Only populated when the underlying `gt` command
|
|
6
|
+
* exited non-zero. Never inferred from successful output.
|
|
7
|
+
*/
|
|
3
8
|
export interface GtHints {
|
|
4
9
|
notInGitRepo?: boolean;
|
|
5
10
|
notInitialized?: boolean;
|
|
@@ -11,58 +16,104 @@ export interface GtHints {
|
|
|
11
16
|
branchNotTracked?: boolean;
|
|
12
17
|
noChangesStaged?: boolean;
|
|
13
18
|
prMissing?: boolean;
|
|
19
|
+
operatingOnTrunk?: boolean;
|
|
20
|
+
invalidArgument?: string;
|
|
14
21
|
}
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
[
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
[
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
// Patterns run only on failure text.
|
|
24
|
+
const PATTERNS: Array<[Exclude<keyof GtHints, "checkedOutElsewhere" | "invalidArgument">, RegExp]> = [
|
|
25
|
+
[
|
|
26
|
+
"notInGitRepo",
|
|
27
|
+
/(?:fatal|ERROR)[^\n]*not\s+(?:a|in a)\s+git\s+repository|No \.git repository found/i,
|
|
28
|
+
],
|
|
29
|
+
[
|
|
30
|
+
"notInitialized",
|
|
31
|
+
// Output is sanitized before hint parsing, so Charcoal is replaced with
|
|
32
|
+
// Graphite. We still allow the original spelling defensively in case the
|
|
33
|
+
// sanitizer is bypassed.
|
|
34
|
+
/Graphite has not been initialized|Charcoal has not been initialized|graphite is not (?:yet )?initialized|run\s+`?gt\s+init`?/i,
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
"notAuthenticated",
|
|
38
|
+
/Run\s+`?gt\s+auth`?|invalid auth token|unauthorized|please log in|missing auth token/i,
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
"conflictHalted",
|
|
42
|
+
/merge\s+conflict|rebase\s+conflict|halted\s+by|run\s+`?gt\s+continue`?|gt\s+abort/i,
|
|
43
|
+
],
|
|
44
|
+
[
|
|
45
|
+
"restackNeeded",
|
|
46
|
+
/needs?\s+to\s+be\s+restacked|run\s+`?gt\s+restack`?|stack\s+is\s+out\s+of\s+date/i,
|
|
47
|
+
],
|
|
48
|
+
[
|
|
49
|
+
"trunkOutOfSync",
|
|
50
|
+
/trunk\s+is\s+out\s+of\s+sync|--ignore-out-of-sync-trunk/i,
|
|
51
|
+
],
|
|
52
|
+
[
|
|
53
|
+
"branchNotTracked",
|
|
54
|
+
/not\s+tracked\s+by\s+graphite|on\s+(?:an\s+)?untracked\s+branch|Cannot perform this operation on (?:an\s+)?untracked/i,
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
"noChangesStaged",
|
|
58
|
+
/no\s+(?:staged\s+)?changes\s+to\s+(?:commit|amend)/i,
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
"prMissing",
|
|
62
|
+
/no\s+pull\s+request|PR\s+not\s+found|branch\s+has\s+no\s+associated\s+pull\s+request/i,
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
"operatingOnTrunk",
|
|
66
|
+
/Cannot perform this operation on the trunk branch/i,
|
|
67
|
+
],
|
|
27
68
|
];
|
|
28
69
|
|
|
29
|
-
|
|
70
|
+
function parseHints(r: GtRunResult): GtHints {
|
|
30
71
|
const text = `${r.stdout}\n${r.stderr}`;
|
|
31
72
|
const hints: GtHints = {};
|
|
32
73
|
for (const [key, re] of PATTERNS) {
|
|
33
|
-
if (re.test(text))
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
74
|
+
if (re.test(text)) (hints as Record<string, unknown>)[key] = true;
|
|
75
|
+
}
|
|
76
|
+
if (/checked\s+out\s+in\s+(?:another|the)\s+worktree/i.test(text)) {
|
|
77
|
+
const m = text.match(
|
|
78
|
+
/branch\s+`?([^\s`'"]+)`?[^\n]*?checked\s+out\s+in[^\n]*?(\/[^\s)]+)?/i,
|
|
79
|
+
);
|
|
80
|
+
hints.checkedOutElsewhere = { branch: m?.[1], worktree: m?.[2] };
|
|
41
81
|
}
|
|
82
|
+
const invalid = text.match(/Unknown argument:\s*([^\n]+)/i);
|
|
83
|
+
if (invalid) hints.invalidArgument = invalid[1].trim();
|
|
42
84
|
return hints;
|
|
43
85
|
}
|
|
44
86
|
|
|
45
87
|
export interface FormattedResult {
|
|
46
88
|
ok: boolean;
|
|
89
|
+
isFailure: boolean;
|
|
47
90
|
result: GtRunResult;
|
|
91
|
+
/** Empty object on success. Populated only on failure. */
|
|
48
92
|
hints: GtHints;
|
|
93
|
+
/** Recovery suggestion derived from hints + auxiliary probes. */
|
|
94
|
+
suggestion?: string;
|
|
49
95
|
}
|
|
50
96
|
|
|
51
97
|
export function formatResult(r: GtRunResult): FormattedResult {
|
|
98
|
+
const isFailure = r.exitCode !== 0 || r.timedOut || !!r.spawnError;
|
|
52
99
|
return {
|
|
53
|
-
ok:
|
|
100
|
+
ok: !isFailure,
|
|
101
|
+
isFailure,
|
|
54
102
|
result: r,
|
|
55
|
-
hints: parseHints(r),
|
|
103
|
+
hints: isFailure ? parseHints(r) : {},
|
|
56
104
|
};
|
|
57
105
|
}
|
|
58
106
|
|
|
59
|
-
/** Render a formatted result into a single text block
|
|
107
|
+
/** Render a formatted result into a single text block. */
|
|
60
108
|
export function renderText(label: string, f: FormattedResult): string {
|
|
61
109
|
const r = f.result;
|
|
62
110
|
const lines: string[] = [];
|
|
63
111
|
lines.push(`$ gt ${r.args.join(" ")}`);
|
|
64
|
-
lines.push(
|
|
65
|
-
|
|
112
|
+
lines.push(
|
|
113
|
+
`# cwd=${r.cwd} exit=${r.exitCode}${r.timedOut ? " (aborted)" : ""}${
|
|
114
|
+
r.spawnError ? ` (spawn-error: ${r.spawnError})` : ""
|
|
115
|
+
}`,
|
|
116
|
+
);
|
|
66
117
|
if (r.stdout.trim()) {
|
|
67
118
|
lines.push("--- stdout ---");
|
|
68
119
|
lines.push(r.stdout.replace(/\s+$/, ""));
|
|
@@ -71,10 +122,155 @@ export function renderText(label: string, f: FormattedResult): string {
|
|
|
71
122
|
lines.push("--- stderr ---");
|
|
72
123
|
lines.push(r.stderr.replace(/\s+$/, ""));
|
|
73
124
|
}
|
|
74
|
-
|
|
75
|
-
if (hintKeys.length) {
|
|
125
|
+
if (f.isFailure && Object.keys(f.hints).length) {
|
|
76
126
|
lines.push("--- hints ---");
|
|
77
127
|
lines.push(JSON.stringify(f.hints));
|
|
78
128
|
}
|
|
129
|
+
if (f.isFailure && f.suggestion) {
|
|
130
|
+
lines.push("--- suggestion ---");
|
|
131
|
+
lines.push(f.suggestion);
|
|
132
|
+
}
|
|
79
133
|
return `[${label}] ${f.ok ? "ok" : "fail"}\n${lines.join("\n")}`;
|
|
80
134
|
}
|
|
135
|
+
|
|
136
|
+
/* ----------------------- auxiliary probes (best-effort) ----------------------- */
|
|
137
|
+
|
|
138
|
+
function execText(cmd: string, args: string[], cwd: string): Promise<string> {
|
|
139
|
+
return new Promise((resolve) => {
|
|
140
|
+
let out = "";
|
|
141
|
+
let child;
|
|
142
|
+
try {
|
|
143
|
+
child = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "ignore"] });
|
|
144
|
+
} catch {
|
|
145
|
+
resolve("");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
child.stdout.on("data", (d) => {
|
|
149
|
+
out += d.toString();
|
|
150
|
+
if (out.length > 4096) out = out.slice(-4096);
|
|
151
|
+
});
|
|
152
|
+
child.on("error", () => resolve(""));
|
|
153
|
+
child.on("close", () => resolve(out));
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function detectCurrentBranch(cwd: string): Promise<string | undefined> {
|
|
158
|
+
const t = (await execText("git", ["-C", cwd, "branch", "--show-current"], cwd)).trim();
|
|
159
|
+
return t || undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function detectTrunk(cwd: string): Promise<string | undefined> {
|
|
163
|
+
const out = await execText("gt", ["--cwd", cwd, "--no-interactive", "trunk"], cwd);
|
|
164
|
+
// gt trunk prints the trunk name on its own line. Take last non-empty line.
|
|
165
|
+
const cleaned = out
|
|
166
|
+
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "")
|
|
167
|
+
.replace(/charcoal/gi, (m) => (m[0] === m[0].toUpperCase() ? "Graphite" : "graphite"));
|
|
168
|
+
const line = cleaned
|
|
169
|
+
.split("\n")
|
|
170
|
+
.map((s) => s.trim())
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
// Skip Graphite banner / init lines so we keep the actual trunk name.
|
|
173
|
+
.filter((s) => !/Graphite|Welcome/i.test(s) && !/initialized/i.test(s))
|
|
174
|
+
.pop();
|
|
175
|
+
return line || undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Enrich a failed FormattedResult with an actionable suggestion. */
|
|
179
|
+
export async function enrichFailure(cwd: string, f: FormattedResult): Promise<void> {
|
|
180
|
+
if (!f.isFailure) return;
|
|
181
|
+
const parts: string[] = [];
|
|
182
|
+
|
|
183
|
+
if (f.hints.branchNotTracked) {
|
|
184
|
+
const [branch, trunk] = await Promise.all([
|
|
185
|
+
detectCurrentBranch(cwd),
|
|
186
|
+
detectTrunk(cwd),
|
|
187
|
+
]);
|
|
188
|
+
const b = branch ?? "<current-branch>";
|
|
189
|
+
const p = trunk ?? "<trunk>";
|
|
190
|
+
parts.push(
|
|
191
|
+
`Current branch (${b}) is not tracked by Graphite. To track it, call: ` +
|
|
192
|
+
`graphite_branch_tracking({ action: "track", branch: "${b}", parent: "${p}" }). ` +
|
|
193
|
+
`Verify parent is correct before applying.`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (f.hints.notInitialized) {
|
|
197
|
+
parts.push(
|
|
198
|
+
`Graphite not initialized in this repo. Call: graphite_repo({ action: "init", trunk: "<trunk-branch>" }).`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (f.hints.conflictHalted) {
|
|
202
|
+
parts.push(
|
|
203
|
+
`A Graphite command is halted by a conflict. After resolving in git, call: ` +
|
|
204
|
+
`graphite_recovery({ action: "continue" }) (or "abort").`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (f.hints.restackNeeded) {
|
|
208
|
+
parts.push(`Stack is out of date. Call: graphite_stack_restack().`);
|
|
209
|
+
}
|
|
210
|
+
if (f.hints.trunkOutOfSync) {
|
|
211
|
+
parts.push(
|
|
212
|
+
`Trunk is out of sync with remote. Call: graphite_remote_sync({ action: "sync" }) first.`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (f.hints.notAuthenticated) {
|
|
216
|
+
parts.push(
|
|
217
|
+
`Graphite is not authenticated. Run \`gt auth\` interactively or set GRAPHITE_AUTH_TOKEN.`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (f.hints.checkedOutElsewhere) {
|
|
221
|
+
const b = f.hints.checkedOutElsewhere.branch ?? "<branch>";
|
|
222
|
+
parts.push(
|
|
223
|
+
`Branch ${b} is checked out in another worktree. Switch to that worktree, or use ` +
|
|
224
|
+
`\`graphite_branch_create({ onto: "${b}", ... })\` to stack a new branch on top.`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (f.hints.invalidArgument) {
|
|
228
|
+
parts.push(
|
|
229
|
+
`gt rejected an unknown argument (${f.hints.invalidArgument}). The local gt version may not support this flag; remove it and retry.`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (f.hints.operatingOnTrunk) {
|
|
233
|
+
parts.push(
|
|
234
|
+
`Operation refused on the trunk branch. Check out a non-trunk branch first (graphite_branch_navigate).`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (parts.length) f.suggestion = parts.join(" ");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Format a GtRunResult; if it failed, enrich and throw an Error whose message
|
|
243
|
+
* contains the rendered output plus the recovery suggestion. The thrown Error
|
|
244
|
+
* makes the tool result `isError: true` so the agent sees a real failure.
|
|
245
|
+
*/
|
|
246
|
+
export async function ensureSuccess(
|
|
247
|
+
label: string,
|
|
248
|
+
r: GtRunResult,
|
|
249
|
+
cwd: string,
|
|
250
|
+
): Promise<FormattedResult> {
|
|
251
|
+
const f = formatResult(r);
|
|
252
|
+
if (f.isFailure) {
|
|
253
|
+
await enrichFailure(cwd, f);
|
|
254
|
+
throw new Error(renderText(label, f));
|
|
255
|
+
}
|
|
256
|
+
return f;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Run multiple gt calls and ensure all succeed. If any failed, enriches each
|
|
261
|
+
* failed result and throws an Error containing every rendered block so the
|
|
262
|
+
* agent sees the full picture, not just the first error.
|
|
263
|
+
*/
|
|
264
|
+
export async function ensureAllSuccess(
|
|
265
|
+
items: Array<{ label: string; result: GtRunResult }>,
|
|
266
|
+
cwd: string,
|
|
267
|
+
): Promise<FormattedResult[]> {
|
|
268
|
+
const formatted = items.map((i) => ({ label: i.label, f: formatResult(i.result) }));
|
|
269
|
+
const failed = formatted.filter((x) => x.f.isFailure);
|
|
270
|
+
if (failed.length) {
|
|
271
|
+
await Promise.all(failed.map((x) => enrichFailure(cwd, x.f)));
|
|
272
|
+
const blocks = formatted.map((x) => renderText(x.label, x.f));
|
|
273
|
+
throw new Error(blocks.join("\n\n"));
|
|
274
|
+
}
|
|
275
|
+
return formatted.map((x) => x.f);
|
|
276
|
+
}
|
package/src/tools/branch.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { runGt } from "../lib/exec";
|
|
3
|
-
import {
|
|
2
|
+
import { runGt, type GtRunResult } from "../lib/exec";
|
|
3
|
+
import {
|
|
4
|
+
ensureAllSuccess,
|
|
5
|
+
ensureSuccess,
|
|
6
|
+
renderText,
|
|
7
|
+
type FormattedResult,
|
|
8
|
+
} from "../lib/result";
|
|
4
9
|
import {
|
|
5
10
|
CwdParam,
|
|
6
11
|
StageMode,
|
|
@@ -13,61 +18,128 @@ import {
|
|
|
13
18
|
|
|
14
19
|
/* --------------------------- branch_inspect --------------------------- */
|
|
15
20
|
|
|
21
|
+
interface InspectSection {
|
|
22
|
+
label: string;
|
|
23
|
+
args: string[];
|
|
24
|
+
/** Display heading; null hides the section in the rendered output. */
|
|
25
|
+
heading: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
export function registerBranchInspect(pi: ExtensionAPI) {
|
|
17
29
|
pi.registerTool({
|
|
18
30
|
name: "graphite_branch_inspect",
|
|
19
31
|
label: "Graphite: branch inspect",
|
|
20
32
|
description:
|
|
21
|
-
"Inspect a branch: parent, children,
|
|
33
|
+
"Inspect a branch with separately-labeled sections: summary (parent/PR), parent, children, diffstat, body, and (optionally) diff or patch. Read-only.",
|
|
22
34
|
promptSnippet:
|
|
23
|
-
"graphite_branch_inspect:
|
|
35
|
+
"graphite_branch_inspect: structured `gt info` + parent/children sections, optional diffstat/body/diff",
|
|
24
36
|
parameters: Type.Object({
|
|
25
37
|
cwd: CwdParam,
|
|
26
|
-
branch: Type.Optional(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
branch: Type.Optional(
|
|
39
|
+
Type.String({ description: "Branch to inspect (default current)." }),
|
|
40
|
+
),
|
|
41
|
+
body: Type.Optional(
|
|
42
|
+
Type.Boolean({ description: "Include the PR body section." }),
|
|
43
|
+
),
|
|
44
|
+
stat: Type.Optional(
|
|
45
|
+
Type.Boolean({ description: "Include the diffstat section." }),
|
|
46
|
+
),
|
|
47
|
+
diff: Type.Optional(
|
|
48
|
+
Type.Boolean({ description: "Include the full diff. Takes precedence over `patch`." }),
|
|
49
|
+
),
|
|
50
|
+
patch: Type.Optional(
|
|
51
|
+
Type.Boolean({ description: "Include per-commit patch. Ignored if `diff` is set." }),
|
|
32
52
|
),
|
|
33
53
|
withParentChildren: Type.Optional(
|
|
34
|
-
Type.Boolean({
|
|
54
|
+
Type.Boolean({
|
|
55
|
+
description:
|
|
56
|
+
"Also run `gt parent` and `gt children` (only meaningful when inspecting the currently checked-out branch).",
|
|
57
|
+
}),
|
|
35
58
|
),
|
|
36
59
|
}),
|
|
37
60
|
async execute(_id, p, signal): Promise<ToolReturn> {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
61
|
+
const branchArg = p.branch ? [p.branch] : [];
|
|
62
|
+
|
|
63
|
+
const sections: InspectSection[] = [
|
|
64
|
+
{
|
|
65
|
+
label: "gt info",
|
|
66
|
+
heading: "## summary",
|
|
67
|
+
args: ["info", ...branchArg],
|
|
68
|
+
},
|
|
69
|
+
];
|
|
45
70
|
|
|
46
|
-
const tasks: Promise<unknown>[] = [runGt(infoArgs, { cwd: p.cwd, signal })];
|
|
47
71
|
if (p.withParentChildren) {
|
|
48
|
-
|
|
49
|
-
|
|
72
|
+
sections.push({ label: "gt parent", heading: "## parent", args: ["parent"] });
|
|
73
|
+
sections.push({ label: "gt children", heading: "## children", args: ["children"] });
|
|
50
74
|
}
|
|
51
|
-
const [info, parent, children] = (await Promise.all(tasks)) as Awaited<
|
|
52
|
-
ReturnType<typeof runGt>
|
|
53
|
-
>[];
|
|
54
75
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
76
|
+
if (p.stat && !p.diff && !p.patch) {
|
|
77
|
+
sections.push({
|
|
78
|
+
label: "gt info --stat",
|
|
79
|
+
heading: "## diffstat",
|
|
80
|
+
args: ["info", ...branchArg, "--stat"],
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (p.body) {
|
|
85
|
+
sections.push({
|
|
86
|
+
label: "gt info --body",
|
|
87
|
+
heading: "## body",
|
|
88
|
+
args: ["info", ...branchArg, "--body"],
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (p.diff) {
|
|
93
|
+
const args = ["info", ...branchArg, "--diff"];
|
|
94
|
+
if (p.stat) args.push("--stat");
|
|
95
|
+
sections.push({ label: "gt info --diff", heading: "## diff", args });
|
|
96
|
+
} else if (p.patch) {
|
|
97
|
+
const args = ["info", ...branchArg, "--patch"];
|
|
98
|
+
if (p.stat) args.push("--stat");
|
|
99
|
+
sections.push({ label: "gt info --patch", heading: "## patch", args });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const results = await Promise.all(
|
|
103
|
+
sections.map((s) => runGt(s.args, { cwd: p.cwd, signal })),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const formatted = await ensureAllSuccess(
|
|
107
|
+
sections.map((s, i) => ({ label: s.label, result: results[i] })),
|
|
108
|
+
p.cwd,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Compose section-by-section structured output.
|
|
112
|
+
const blocks: string[] = [];
|
|
113
|
+
formatted.forEach((f, i) => {
|
|
114
|
+
const s = sections[i];
|
|
115
|
+
if (s.heading) blocks.push(s.heading);
|
|
116
|
+
blocks.push(stripChrome(f.result.stdout));
|
|
117
|
+
blocks.push("");
|
|
118
|
+
});
|
|
119
|
+
blocks.push("--- raw ---");
|
|
120
|
+
formatted.forEach((f, i) => {
|
|
121
|
+
blocks.push(renderText(sections[i].label, f));
|
|
122
|
+
blocks.push("");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const details: Record<string, FormattedResult> = {};
|
|
126
|
+
sections.forEach((s, i) => {
|
|
127
|
+
const key = s.label.replace(/^gt\s+/, "").replace(/[^a-z0-9]/gi, "_");
|
|
128
|
+
details[key] = formatted[i];
|
|
129
|
+
});
|
|
58
130
|
|
|
59
131
|
return {
|
|
60
|
-
content: [{ type: "text", text: blocks.join("\n
|
|
61
|
-
details
|
|
62
|
-
info: formatResult(info),
|
|
63
|
-
parent: parent ? formatResult(parent) : undefined,
|
|
64
|
-
children: children ? formatResult(children) : undefined,
|
|
65
|
-
},
|
|
132
|
+
content: [{ type: "text", text: blocks.join("\n").trim() }],
|
|
133
|
+
details,
|
|
66
134
|
};
|
|
67
135
|
},
|
|
68
136
|
});
|
|
69
137
|
}
|
|
70
138
|
|
|
139
|
+
function stripChrome(s: string): string {
|
|
140
|
+
return s.replace(/\s+$/, "");
|
|
141
|
+
}
|
|
142
|
+
|
|
71
143
|
/* --------------------------- branch_create --------------------------- */
|
|
72
144
|
|
|
73
145
|
export function registerBranchCreate(pi: ExtensionAPI) {
|
|
@@ -107,10 +179,11 @@ export function registerBranchCreate(pi: ExtensionAPI) {
|
|
|
107
179
|
if (p.onto) args.push("--onto", p.onto);
|
|
108
180
|
if (p.insert) args.push("--insert");
|
|
109
181
|
args.push(p.ai ? "--ai" : "--no-ai");
|
|
182
|
+
const label = `gt ${args.join(" ")}`;
|
|
110
183
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
111
|
-
const f =
|
|
184
|
+
const f = await ensureSuccess(label, r, p.cwd);
|
|
112
185
|
return {
|
|
113
|
-
content: [{ type: "text", text: renderText(
|
|
186
|
+
content: [{ type: "text", text: renderText(label, f) }],
|
|
114
187
|
details: { result: f },
|
|
115
188
|
};
|
|
116
189
|
},
|
|
@@ -224,10 +297,11 @@ export function registerBranchUpdate(pi: ExtensionAPI) {
|
|
|
224
297
|
}
|
|
225
298
|
}
|
|
226
299
|
|
|
227
|
-
const
|
|
228
|
-
const
|
|
300
|
+
const label = `gt ${args.join(" ")}`;
|
|
301
|
+
const r: GtRunResult = await runGt(args, { cwd: p.cwd, signal });
|
|
302
|
+
const f = await ensureSuccess(label, r, p.cwd);
|
|
229
303
|
return {
|
|
230
|
-
content: [{ type: "text", text: renderText(
|
|
304
|
+
content: [{ type: "text", text: renderText(label, f) }],
|
|
231
305
|
details: { action: p.action, result: f },
|
|
232
306
|
};
|
|
233
307
|
},
|
|
@@ -276,10 +350,11 @@ export function registerBranchTracking(pi: ExtensionAPI) {
|
|
|
276
350
|
if (p.branch) args.push(p.branch);
|
|
277
351
|
break;
|
|
278
352
|
}
|
|
353
|
+
const label = `gt ${args.join(" ")}`;
|
|
279
354
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
280
|
-
const f =
|
|
355
|
+
const f = await ensureSuccess(label, r, p.cwd);
|
|
281
356
|
return {
|
|
282
|
-
content: [{ type: "text", text: renderText(
|
|
357
|
+
content: [{ type: "text", text: renderText(label, f) }],
|
|
283
358
|
details: { action: p.action, result: f },
|
|
284
359
|
};
|
|
285
360
|
},
|
|
@@ -341,10 +416,11 @@ export function registerBranchNavigate(pi: ExtensionAPI) {
|
|
|
341
416
|
args = ["bottom"];
|
|
342
417
|
break;
|
|
343
418
|
}
|
|
419
|
+
const label = `gt ${args.join(" ")}`;
|
|
344
420
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
345
|
-
const f =
|
|
421
|
+
const f = await ensureSuccess(label, r, p.cwd);
|
|
346
422
|
return {
|
|
347
|
-
content: [{ type: "text", text: renderText(
|
|
423
|
+
content: [{ type: "text", text: renderText(label, f) }],
|
|
348
424
|
details: { action: p.action, result: f },
|
|
349
425
|
};
|
|
350
426
|
},
|
package/src/tools/pr.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { runGt } from "../lib/exec";
|
|
3
|
-
import {
|
|
3
|
+
import { ensureSuccess, renderText } from "../lib/result";
|
|
4
4
|
import {
|
|
5
5
|
CwdParam,
|
|
6
6
|
StringEnum,
|
|
@@ -11,6 +11,12 @@ import {
|
|
|
11
11
|
|
|
12
12
|
/* ------------------------------ pr_submit ------------------------------ */
|
|
13
13
|
|
|
14
|
+
function shellQuote(s: string): string {
|
|
15
|
+
if (s === "") return "''";
|
|
16
|
+
if (/^[A-Za-z0-9_./:@%+=-]+$/.test(s)) return s;
|
|
17
|
+
return `'${s.replace(/'/g, `'"'"'`)}'`;
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
export function registerPrSubmit(pi: ExtensionAPI) {
|
|
15
21
|
pi.registerTool({
|
|
16
22
|
name: "graphite_pr_submit",
|
|
@@ -21,6 +27,7 @@ export function registerPrSubmit(pi: ExtensionAPI) {
|
|
|
21
27
|
"graphite_pr_submit: plan or apply `gt submit` for a branch or stack",
|
|
22
28
|
promptGuidelines: [
|
|
23
29
|
"Always call graphite_pr_submit with apply:false (default) first to see the dry-run plan, then call again with apply:true and confirmRemote:true to actually submit.",
|
|
30
|
+
"`gt submit` cannot set PR title/body inline. If you pass `title`/`body` to graphite_pr_submit, the tool will return a `gh pr edit` command for you to run via bash to apply the metadata.",
|
|
24
31
|
],
|
|
25
32
|
parameters: Type.Object({
|
|
26
33
|
cwd: CwdParam,
|
|
@@ -62,12 +69,27 @@ export function registerPrSubmit(pi: ExtensionAPI) {
|
|
|
62
69
|
ignoreOutOfSyncTrunk: Type.Optional(Type.Boolean()),
|
|
63
70
|
view: Type.Optional(Type.Boolean({ description: "--view (open PR in browser after submit)." })),
|
|
64
71
|
confirmRemote: Type.Optional(Type.Boolean()),
|
|
72
|
+
|
|
73
|
+
title: Type.Optional(
|
|
74
|
+
Type.String({
|
|
75
|
+
description:
|
|
76
|
+
"Desired PR title. `gt submit` has no inline flag for this; the tool emits a `gh pr edit` command to run after submit.",
|
|
77
|
+
}),
|
|
78
|
+
),
|
|
79
|
+
body: Type.Optional(
|
|
80
|
+
Type.String({
|
|
81
|
+
description:
|
|
82
|
+
"Desired PR body. `gt submit` has no inline flag for this; the tool emits a `gh pr edit` command to run after submit.",
|
|
83
|
+
}),
|
|
84
|
+
),
|
|
65
85
|
}),
|
|
66
86
|
async execute(_id, p, signal): Promise<ToolReturn> {
|
|
67
87
|
const apply = p.apply === true;
|
|
68
88
|
if (apply) requireConfirm(p.confirmRemote, "gt submit (push branches + create/update PRs)");
|
|
69
89
|
if (p.forcePush) requireConfirm(p.confirmRemote, "gt submit --force");
|
|
70
90
|
|
|
91
|
+
const wantsCustomMetadata = p.title != null || p.body != null;
|
|
92
|
+
|
|
71
93
|
const args: string[] = ["submit"];
|
|
72
94
|
if (!apply) args.push("--dry-run");
|
|
73
95
|
|
|
@@ -88,7 +110,10 @@ export function registerPrSubmit(pi: ExtensionAPI) {
|
|
|
88
110
|
if (p.comment) args.push("--comment", p.comment);
|
|
89
111
|
if (p.targetTrunk) args.push("--target-trunk", p.targetTrunk);
|
|
90
112
|
|
|
91
|
-
|
|
113
|
+
// If the caller supplied title/body, force --no-edit so gt doesn't try
|
|
114
|
+
// to prompt or open a web editor with conflicting metadata. The actual
|
|
115
|
+
// metadata is applied via the suggested `gh pr edit` command instead.
|
|
116
|
+
const editMode = wantsCustomMetadata ? "none" : (p.editMode ?? "none");
|
|
92
117
|
if (editMode === "none") args.push("--no-edit");
|
|
93
118
|
else if (editMode === "cli") args.push("--edit", "--cli");
|
|
94
119
|
else if (editMode === "web") args.push("--web");
|
|
@@ -99,11 +124,45 @@ export function registerPrSubmit(pi: ExtensionAPI) {
|
|
|
99
124
|
if (p.ignoreOutOfSyncTrunk) args.push("--ignore-out-of-sync-trunk");
|
|
100
125
|
if (p.view) args.push("--view");
|
|
101
126
|
|
|
127
|
+
const label = `gt ${args.join(" ")}`;
|
|
102
128
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
103
|
-
const f =
|
|
129
|
+
const f = await ensureSuccess(label, r, p.cwd);
|
|
130
|
+
|
|
131
|
+
const blocks: string[] = [renderText(label, f)];
|
|
132
|
+
|
|
133
|
+
let metadataNote: string | undefined;
|
|
134
|
+
if (wantsCustomMetadata) {
|
|
135
|
+
const ghParts: string[] = ["gh", "pr", "edit"];
|
|
136
|
+
if (p.branch) ghParts.push(p.branch);
|
|
137
|
+
if (p.title != null) ghParts.push("--title", shellQuote(p.title));
|
|
138
|
+
if (p.body != null) ghParts.push("--body", shellQuote(p.body));
|
|
139
|
+
const ghCmd = ghParts.join(" ");
|
|
140
|
+
|
|
141
|
+
metadataNote = [
|
|
142
|
+
"## metadata note",
|
|
143
|
+
"`gt submit` has no flag to set PR title/body inline.",
|
|
144
|
+
"To apply the title/body you supplied, run the following via the bash tool" +
|
|
145
|
+
" (gt does not run gh for you; this keeps the tool composable):",
|
|
146
|
+
ghCmd,
|
|
147
|
+
p.stack === true || (!p.branch && p.stack !== false)
|
|
148
|
+
? "If this submit covered multiple PRs, repeat `gh pr edit <branch>` for each PR that needs metadata."
|
|
149
|
+
: undefined,
|
|
150
|
+
]
|
|
151
|
+
.filter((x): x is string => Boolean(x))
|
|
152
|
+
.join("\n");
|
|
153
|
+
|
|
154
|
+
blocks.push(metadataNote);
|
|
155
|
+
}
|
|
156
|
+
|
|
104
157
|
return {
|
|
105
|
-
content: [{ type: "text", text:
|
|
106
|
-
details: {
|
|
158
|
+
content: [{ type: "text", text: blocks.join("\n\n") }],
|
|
159
|
+
details: {
|
|
160
|
+
apply,
|
|
161
|
+
editMode,
|
|
162
|
+
wantsCustomMetadata,
|
|
163
|
+
metadataNote,
|
|
164
|
+
result: f,
|
|
165
|
+
},
|
|
107
166
|
};
|
|
108
167
|
},
|
|
109
168
|
});
|
|
@@ -139,40 +198,27 @@ export function registerPrLifecycle(pi: ExtensionAPI) {
|
|
|
139
198
|
confirmRemote: Type.Optional(Type.Boolean()),
|
|
140
199
|
}),
|
|
141
200
|
async execute(_id, p, signal): Promise<ToolReturn> {
|
|
201
|
+
let args: string[];
|
|
142
202
|
if (p.action === "open_url") {
|
|
143
|
-
|
|
203
|
+
args = ["pr"];
|
|
144
204
|
if (p.branch) args.push(p.branch);
|
|
145
205
|
if (p.stack) args.push("--stack");
|
|
146
|
-
|
|
147
|
-
const f = formatResult(r);
|
|
148
|
-
return {
|
|
149
|
-
content: [{ type: "text", text: renderText(`gt ${args.join(" ")}`, f) }],
|
|
150
|
-
details: { result: f },
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (p.action === "merge") {
|
|
206
|
+
} else if (p.action === "merge") {
|
|
155
207
|
const apply = p.apply === true;
|
|
156
208
|
if (apply) requireConfirm(p.confirmRemote, "gt merge (merges PRs on GitHub)");
|
|
157
|
-
|
|
209
|
+
args = ["merge"];
|
|
158
210
|
if (!apply) args.push("--dry-run");
|
|
159
211
|
if (p.confirm) args.push("--confirm");
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
content: [{ type: "text", text: renderText(`gt ${args.join(" ")}`, f) }],
|
|
164
|
-
details: { apply, result: f },
|
|
165
|
-
};
|
|
212
|
+
} else {
|
|
213
|
+
args = ["unlink"];
|
|
214
|
+
if (p.branch) args.push(p.branch);
|
|
166
215
|
}
|
|
167
|
-
|
|
168
|
-
// unlink
|
|
169
|
-
const args = ["unlink"];
|
|
170
|
-
if (p.branch) args.push(p.branch);
|
|
216
|
+
const label = `gt ${args.join(" ")}`;
|
|
171
217
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
172
|
-
const f =
|
|
218
|
+
const f = await ensureSuccess(label, r, p.cwd);
|
|
173
219
|
return {
|
|
174
|
-
content: [{ type: "text", text: renderText(
|
|
175
|
-
details: { result: f },
|
|
220
|
+
content: [{ type: "text", text: renderText(label, f) }],
|
|
221
|
+
details: { action: p.action, result: f },
|
|
176
222
|
};
|
|
177
223
|
},
|
|
178
224
|
});
|
package/src/tools/recovery.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { runGt } from "../lib/exec";
|
|
3
|
-
import {
|
|
3
|
+
import { ensureSuccess, renderText } from "../lib/result";
|
|
4
4
|
import { CwdParam, StringEnum, Type, type ToolReturn } from "../lib/schema";
|
|
5
5
|
|
|
6
6
|
export function registerRecovery(pi: ExtensionAPI) {
|
|
@@ -41,10 +41,11 @@ export function registerRecovery(pi: ExtensionAPI) {
|
|
|
41
41
|
if (p.force) args.push("--force");
|
|
42
42
|
break;
|
|
43
43
|
}
|
|
44
|
+
const label = `gt ${args.join(" ")}`;
|
|
44
45
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
45
|
-
const f =
|
|
46
|
+
const f = await ensureSuccess(label, r, p.cwd);
|
|
46
47
|
return {
|
|
47
|
-
content: [{ type: "text", text: renderText(
|
|
48
|
+
content: [{ type: "text", text: renderText(label, f) }],
|
|
48
49
|
details: { action: p.action, result: f },
|
|
49
50
|
};
|
|
50
51
|
},
|
package/src/tools/remote.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { runGt } from "../lib/exec";
|
|
3
|
-
import {
|
|
3
|
+
import { ensureSuccess, renderText } from "../lib/result";
|
|
4
4
|
import {
|
|
5
5
|
CwdParam,
|
|
6
6
|
StringEnum,
|
|
@@ -96,10 +96,11 @@ export function registerRemoteSync(pi: ExtensionAPI) {
|
|
|
96
96
|
if (p.unfrozen) args.push("--unfrozen");
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
const label = `gt ${args.join(" ")}`;
|
|
99
100
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
100
|
-
const f =
|
|
101
|
+
const f = await ensureSuccess(label, r, p.cwd);
|
|
101
102
|
return {
|
|
102
|
-
content: [{ type: "text", text: renderText(
|
|
103
|
+
content: [{ type: "text", text: renderText(label, f) }],
|
|
103
104
|
details: { action: p.action, result: f },
|
|
104
105
|
};
|
|
105
106
|
},
|
package/src/tools/repo.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { runGt } from "../lib/exec";
|
|
3
|
-
import {
|
|
3
|
+
import { ensureAllSuccess, ensureSuccess, renderText } from "../lib/result";
|
|
4
4
|
import { CwdParam, StringEnum, Type, type ToolReturn } from "../lib/schema";
|
|
5
5
|
|
|
6
6
|
const params = Type.Object({
|
|
@@ -42,8 +42,13 @@ export function registerRepo(pi: ExtensionAPI) {
|
|
|
42
42
|
runGt(["trunk"], { cwd, signal }),
|
|
43
43
|
runGt(["log", "short"], { cwd, signal }),
|
|
44
44
|
]);
|
|
45
|
-
const ft =
|
|
46
|
-
|
|
45
|
+
const [ft, fl] = await ensureAllSuccess(
|
|
46
|
+
[
|
|
47
|
+
{ label: "gt trunk", result: trunk },
|
|
48
|
+
{ label: "gt log short", result: log },
|
|
49
|
+
],
|
|
50
|
+
cwd,
|
|
51
|
+
);
|
|
47
52
|
return {
|
|
48
53
|
content: [
|
|
49
54
|
{
|
|
@@ -60,40 +65,34 @@ export function registerRepo(pi: ExtensionAPI) {
|
|
|
60
65
|
if (p.trunk) args.push("--trunk", p.trunk);
|
|
61
66
|
if (p.reset) args.push("--reset");
|
|
62
67
|
const r = await runGt(args, { cwd, signal });
|
|
63
|
-
const f =
|
|
68
|
+
const f = await ensureSuccess(`gt ${args.join(" ")}`, r, cwd);
|
|
64
69
|
return {
|
|
65
|
-
content: [{ type: "text", text: renderText(
|
|
70
|
+
content: [{ type: "text", text: renderText(`gt ${args.join(" ")}`, f) }],
|
|
66
71
|
details: { result: f },
|
|
67
72
|
};
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
if (p.action === "set_trunk") {
|
|
71
76
|
if (!p.trunk) throw new Error("action=set_trunk requires `trunk`.");
|
|
72
|
-
const args = ["trunk"];
|
|
73
|
-
if (p.addAdditionalTrunk) args.push("--add");
|
|
74
|
-
// gt trunk --add prompts for trunk name; not all gt versions accept positional.
|
|
75
|
-
// Fall back to gt init --trunk for non-additional sets.
|
|
76
77
|
if (!p.addAdditionalTrunk) {
|
|
77
|
-
const
|
|
78
|
-
const
|
|
78
|
+
const args = ["init", "--trunk", p.trunk];
|
|
79
|
+
const r = await runGt(args, { cwd, signal });
|
|
80
|
+
const f = await ensureSuccess(`gt ${args.join(" ")}`, r, cwd);
|
|
79
81
|
return {
|
|
80
82
|
content: [
|
|
81
|
-
{ type: "text", text: renderText(`gt
|
|
83
|
+
{ type: "text", text: renderText(`gt ${args.join(" ")}`, f) },
|
|
82
84
|
],
|
|
83
85
|
details: { result: f },
|
|
84
86
|
};
|
|
85
87
|
}
|
|
86
|
-
//
|
|
87
|
-
// --no-interactive
|
|
88
|
-
//
|
|
89
|
-
const r = await runGt(
|
|
90
|
-
const f =
|
|
88
|
+
// `gt trunk --add` is interactive (prompts for the new trunk name).
|
|
89
|
+
// With --no-interactive this will fail; surface that failure rather
|
|
90
|
+
// than silently misleading the caller.
|
|
91
|
+
const r = await runGt(["trunk", "--add"], { cwd, signal });
|
|
92
|
+
const f = await ensureSuccess("gt trunk --add", r, cwd);
|
|
91
93
|
return {
|
|
92
94
|
content: [{ type: "text", text: renderText("gt trunk --add", f) }],
|
|
93
|
-
details: {
|
|
94
|
-
result: f,
|
|
95
|
-
note: "`gt trunk --add` is interactive; this call runs with --no-interactive and may fail. Add the trunk via `gt config` or `gt init --trunk` instead.",
|
|
96
|
-
},
|
|
95
|
+
details: { result: f },
|
|
97
96
|
};
|
|
98
97
|
}
|
|
99
98
|
|
|
@@ -102,20 +101,21 @@ export function registerRepo(pi: ExtensionAPI) {
|
|
|
102
101
|
runGt(["trunk"], { cwd, signal }),
|
|
103
102
|
runGt(["trunk", "--all"], { cwd, signal }),
|
|
104
103
|
]);
|
|
104
|
+
const [ft, fta] = await ensureAllSuccess(
|
|
105
|
+
[
|
|
106
|
+
{ label: "gt trunk", result: trunk },
|
|
107
|
+
{ label: "gt trunk --all", result: trunkAll },
|
|
108
|
+
],
|
|
109
|
+
cwd,
|
|
110
|
+
);
|
|
105
111
|
return {
|
|
106
112
|
content: [
|
|
107
113
|
{
|
|
108
114
|
type: "text",
|
|
109
|
-
text: [
|
|
110
|
-
renderText("gt trunk", formatResult(trunk)),
|
|
111
|
-
renderText("gt trunk --all", formatResult(trunkAll)),
|
|
112
|
-
].join("\n\n"),
|
|
115
|
+
text: [renderText("gt trunk", ft), renderText("gt trunk --all", fta)].join("\n\n"),
|
|
113
116
|
},
|
|
114
117
|
],
|
|
115
|
-
details: {
|
|
116
|
-
trunk: formatResult(trunk),
|
|
117
|
-
trunkAll: formatResult(trunkAll),
|
|
118
|
-
},
|
|
118
|
+
details: { trunk: ft, trunkAll: fta },
|
|
119
119
|
};
|
|
120
120
|
},
|
|
121
121
|
});
|
package/src/tools/stack.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { runGt } from "../lib/exec";
|
|
3
|
-
import {
|
|
3
|
+
import { ensureSuccess, renderText } from "../lib/result";
|
|
4
4
|
import {
|
|
5
5
|
CwdParam,
|
|
6
6
|
StringEnum,
|
|
@@ -29,7 +29,7 @@ export function registerStackView(pi: ExtensionAPI) {
|
|
|
29
29
|
scope: Type.Optional(
|
|
30
30
|
StringEnum(["all_trunks", "current_stack", "default"] as const, {
|
|
31
31
|
description:
|
|
32
|
-
"all_trunks adds --all
|
|
32
|
+
"all_trunks adds --all (only supported on mode=full/default). current_stack adds --stack. default does neither.",
|
|
33
33
|
}),
|
|
34
34
|
),
|
|
35
35
|
showUntracked: Type.Optional(Type.Boolean()),
|
|
@@ -37,17 +37,34 @@ export function registerStackView(pi: ExtensionAPI) {
|
|
|
37
37
|
steps: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
38
38
|
}),
|
|
39
39
|
async execute(_id, p, signal): Promise<ToolReturn> {
|
|
40
|
-
|
|
40
|
+
// Empirically, current gt rejects `--all` on `gt log short` / `gt log long`
|
|
41
|
+
// ("Unknown argument: all"). Block that combination at the tool layer
|
|
42
|
+
// instead of exposing it to the model.
|
|
43
|
+
if (p.scope === "all_trunks" && (p.mode === "short" || p.mode === "long")) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"graphite_stack_view: scope='all_trunks' is not supported with mode='short'/'long' " +
|
|
46
|
+
"(current `gt` rejects `--all` on those forms). Omit `mode` (or use mode='full') to use --all.",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const sub =
|
|
51
|
+
p.mode === "short"
|
|
52
|
+
? ["log", "short"]
|
|
53
|
+
: p.mode === "long"
|
|
54
|
+
? ["log", "long"]
|
|
55
|
+
: ["log"];
|
|
41
56
|
const args = [...sub];
|
|
42
57
|
if (p.scope === "all_trunks") args.push("--all");
|
|
43
58
|
if (p.scope === "current_stack") args.push("--stack");
|
|
44
59
|
if (p.showUntracked) args.push("--show-untracked");
|
|
45
60
|
if (p.reverse) args.push("--reverse");
|
|
46
61
|
if (p.steps != null) args.push("--steps", String(p.steps));
|
|
62
|
+
|
|
63
|
+
const label = `gt ${args.join(" ")}`;
|
|
47
64
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
48
|
-
const f =
|
|
65
|
+
const f = await ensureSuccess(label, r, p.cwd);
|
|
49
66
|
return {
|
|
50
|
-
content: [{ type: "text", text: renderText(
|
|
67
|
+
content: [{ type: "text", text: renderText(label, f) }],
|
|
51
68
|
details: { result: f },
|
|
52
69
|
};
|
|
53
70
|
},
|
|
@@ -85,10 +102,12 @@ export function registerStackRestack(pi: ExtensionAPI) {
|
|
|
85
102
|
if (p.scope === "downstack") args.push("--downstack");
|
|
86
103
|
else if (p.scope === "upstack") args.push("--upstack");
|
|
87
104
|
else if (p.scope === "only") args.push("--only");
|
|
105
|
+
|
|
106
|
+
const label = `gt ${args.join(" ")}`;
|
|
88
107
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
89
|
-
const f =
|
|
108
|
+
const f = await ensureSuccess(label, r, p.cwd);
|
|
90
109
|
return {
|
|
91
|
-
content: [{ type: "text", text: renderText(
|
|
110
|
+
content: [{ type: "text", text: renderText(label, f) }],
|
|
92
111
|
details: { result: f },
|
|
93
112
|
};
|
|
94
113
|
},
|
|
@@ -132,44 +151,34 @@ export function registerStackReorganize(pi: ExtensionAPI) {
|
|
|
132
151
|
confirmRemote: Type.Optional(Type.Boolean()),
|
|
133
152
|
}),
|
|
134
153
|
async execute(_id, p, signal): Promise<ToolReturn> {
|
|
154
|
+
let args: string[];
|
|
135
155
|
if (p.action === "move_branch") {
|
|
136
156
|
if (!p.onto) throw new Error("action=move_branch requires `onto`.");
|
|
137
|
-
|
|
157
|
+
args = ["move", "--onto", p.onto];
|
|
138
158
|
if (p.source) args.push("--source", p.source);
|
|
139
159
|
if (p.onlyMove) args.push("--only");
|
|
140
|
-
|
|
141
|
-
const f = formatResult(r);
|
|
142
|
-
return {
|
|
143
|
-
content: [{ type: "text", text: renderText(`gt ${args.join(" ")}`, f) }],
|
|
144
|
-
details: { result: f },
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (p.action === "fold") {
|
|
160
|
+
} else if (p.action === "fold") {
|
|
149
161
|
if (p.foldClose) requireConfirm(p.confirmRemote, "fold --close");
|
|
150
|
-
|
|
162
|
+
args = ["fold"];
|
|
151
163
|
if (p.foldKeep) args.push("--keep");
|
|
152
164
|
if (p.foldStack) args.push("--stack");
|
|
153
165
|
if (p.foldClose) args.push("--close");
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
166
|
+
} else {
|
|
167
|
+
if (!p.filePatterns || p.filePatterns.length === 0) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
"action=split_by_file requires `filePatterns` (one or more pathspecs).",
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
args = ["split", "--by-file"];
|
|
173
|
+
for (const pat of p.filePatterns) args.push("-f", pat);
|
|
160
174
|
}
|
|
161
175
|
|
|
162
|
-
|
|
163
|
-
if (!p.filePatterns || p.filePatterns.length === 0) {
|
|
164
|
-
throw new Error("action=split_by_file requires `filePatterns` (one or more pathspecs).");
|
|
165
|
-
}
|
|
166
|
-
const args = ["split", "--by-file"];
|
|
167
|
-
for (const pat of p.filePatterns) args.push("-f", pat);
|
|
176
|
+
const label = `gt ${args.join(" ")}`;
|
|
168
177
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
169
|
-
const f =
|
|
178
|
+
const f = await ensureSuccess(label, r, p.cwd);
|
|
170
179
|
return {
|
|
171
|
-
content: [{ type: "text", text: renderText(
|
|
172
|
-
details: { result: f },
|
|
180
|
+
content: [{ type: "text", text: renderText(label, f) }],
|
|
181
|
+
details: { action: p.action, result: f },
|
|
173
182
|
};
|
|
174
183
|
},
|
|
175
184
|
});
|