pi-graphite 0.2.2 → 0.3.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/README.md +69 -34
- package/package.json +6 -2
- package/skills/graphite/SKILL.md +218 -0
- package/src/index.ts +40 -53
- package/src/lib/argv.ts +59 -0
- package/src/lib/exec.ts +98 -17
- package/src/lib/result.ts +29 -13
- package/src/lib/schema.ts +5 -4
- package/src/tools/change.ts +140 -0
- package/src/tools/navigate.ts +99 -0
- package/src/tools/recover.ts +147 -0
- package/src/tools/setup.ts +121 -0
- package/src/tools/status.ts +55 -0
- package/src/tools/submit.ts +127 -0
- package/src/tools/sync.ts +79 -0
- package/src/tools/branch.ts +0 -428
- package/src/tools/pr.ts +0 -225
- package/src/tools/recovery.ts +0 -53
- package/src/tools/remote.ts +0 -108
- package/src/tools/repo.ts +0 -122
- package/src/tools/stack.ts +0 -185
package/src/lib/exec.ts
CHANGED
|
@@ -5,8 +5,10 @@ import { resolve as resolvePath } from "node:path";
|
|
|
5
5
|
export interface GtRunOptions {
|
|
6
6
|
cwd: string;
|
|
7
7
|
signal?: AbortSignal;
|
|
8
|
-
/** Extra env vars merged into process.env */
|
|
8
|
+
/** Extra env vars merged into process.env. Safety vars always win. */
|
|
9
9
|
env?: Record<string, string>;
|
|
10
|
+
/** Hard timeout in ms. Default 120s. */
|
|
11
|
+
timeoutMs?: number;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export interface GtRunResult {
|
|
@@ -23,6 +25,47 @@ export interface GtRunResult {
|
|
|
23
25
|
const ANSI = /\x1b\[[0-9;]*[a-zA-Z]/g;
|
|
24
26
|
const MAX_BYTES = 50 * 1024;
|
|
25
27
|
const MAX_LINES = 2000;
|
|
28
|
+
export const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
|
|
29
|
+
|
|
30
|
+
/** Env guardrails: disable editors, pagers, browsers. Must override caller env. */
|
|
31
|
+
export const SAFE_NONINTERACTIVE_ENV: Record<string, string> = {
|
|
32
|
+
GT_EDITOR: "true",
|
|
33
|
+
TEST_GT_EDITOR: "true",
|
|
34
|
+
GIT_EDITOR: "true",
|
|
35
|
+
EDITOR: "true",
|
|
36
|
+
VISUAL: "true",
|
|
37
|
+
GIT_SEQUENCE_EDITOR: "true",
|
|
38
|
+
GT_PAGER: "",
|
|
39
|
+
GIT_PAGER: "cat",
|
|
40
|
+
PAGER: "cat",
|
|
41
|
+
LESS: "FRX",
|
|
42
|
+
BROWSER: "true",
|
|
43
|
+
GH_BROWSER: "true",
|
|
44
|
+
// gt-gh treats this as "invoked from Graphite Interactive" and forces
|
|
45
|
+
// non-interactive behavior regardless of argv. Other gt builds should
|
|
46
|
+
// ignore it if unsupported; keep --no-interactive argv guards too.
|
|
47
|
+
GRAPHITE_INTERACTIVE: "1",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function safeNoninteractiveEnv(extra?: Record<string, string>): NodeJS.ProcessEnv {
|
|
51
|
+
return {
|
|
52
|
+
...process.env,
|
|
53
|
+
...(extra ?? {}),
|
|
54
|
+
...SAFE_NONINTERACTIVE_ENV,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function killProcessGroup(child: { pid?: number; kill(signal?: NodeJS.Signals): boolean }, signal: NodeJS.Signals): void {
|
|
59
|
+
try {
|
|
60
|
+
if (child.pid && process.platform !== "win32") {
|
|
61
|
+
process.kill(-child.pid, signal);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
} catch {}
|
|
65
|
+
try {
|
|
66
|
+
child.kill(signal);
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
26
69
|
|
|
27
70
|
export function stripAnsi(s: string): string {
|
|
28
71
|
return s.replace(ANSI, "");
|
|
@@ -47,12 +90,27 @@ export function truncateOutput(s: string): string {
|
|
|
47
90
|
return `${kept}\n... [truncated: ${s.length - MAX_BYTES} more bytes]`;
|
|
48
91
|
}
|
|
49
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Tokens that re-enable interactive flows in gt and must never appear in
|
|
95
|
+
* rawArgs. Tool authors should not pass these directly, and user-supplied
|
|
96
|
+
* strings should be routed through argv helpers (assertSafeRef / flagEq)
|
|
97
|
+
* so values starting with `-` can never reach this list. We still scan
|
|
98
|
+
* defensively here as belt-and-braces.
|
|
99
|
+
*/
|
|
100
|
+
const FORBIDDEN_RAW_TOKENS = new Set<string>([
|
|
101
|
+
"--interactive",
|
|
102
|
+
"--interactive-rebase",
|
|
103
|
+
]);
|
|
104
|
+
|
|
50
105
|
/**
|
|
51
106
|
* Run `gt` with structured args. Never builds a shell string.
|
|
52
107
|
*
|
|
53
|
-
* - Always injects --cwd <abs
|
|
54
|
-
* -
|
|
55
|
-
*
|
|
108
|
+
* - Always injects --cwd <abs> and --no-interactive at the *start*.
|
|
109
|
+
* - Also appends a trailing --no-interactive after rawArgs as defense in
|
|
110
|
+
* depth: yargs lets a later `--interactive` override an earlier
|
|
111
|
+
* `--no-interactive`, so we ensure --no-interactive is always the last
|
|
112
|
+
* word on the global option.
|
|
113
|
+
* - Refuses to run if rawArgs contains a known interactive-toggle token.
|
|
56
114
|
* - Does not inject --quiet (we want stderr diagnostics).
|
|
57
115
|
*/
|
|
58
116
|
export async function runGt(
|
|
@@ -60,16 +118,32 @@ export async function runGt(
|
|
|
60
118
|
opts: GtRunOptions,
|
|
61
119
|
): Promise<GtRunResult> {
|
|
62
120
|
const cwd = resolvePath(opts.cwd);
|
|
121
|
+
for (const tok of rawArgs) {
|
|
122
|
+
// Match both `--interactive` and `--interactive=...` forms.
|
|
123
|
+
const head = tok.split("=", 1)[0];
|
|
124
|
+
if (FORBIDDEN_RAW_TOKENS.has(head)) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`runGt: refused to pass forbidden token ${JSON.stringify(tok)} to gt. ` +
|
|
127
|
+
`Interactive flows are disabled in this extension.`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
63
131
|
const args = ["--cwd", cwd, "--no-interactive"];
|
|
64
132
|
args.push(...rawArgs);
|
|
133
|
+
// Trailing --no-interactive wins against any later `--interactive` that
|
|
134
|
+
// might still slip in via an unaudited code path.
|
|
135
|
+
args.push("--no-interactive");
|
|
65
136
|
|
|
66
137
|
return new Promise<GtRunResult>((resolve) => {
|
|
67
138
|
let child: ChildProcessByStdio<null, Readable, Readable>;
|
|
68
139
|
try {
|
|
69
140
|
child = spawn("gt", args, {
|
|
70
141
|
cwd,
|
|
71
|
-
|
|
142
|
+
// Force any editor/pager/browser invocation to no-op instead of
|
|
143
|
+
// hanging. Safety vars override opts.env by design.
|
|
144
|
+
env: safeNoninteractiveEnv(opts.env),
|
|
72
145
|
stdio: ["ignore", "pipe", "pipe"],
|
|
146
|
+
detached: process.platform !== "win32",
|
|
73
147
|
});
|
|
74
148
|
} catch (e) {
|
|
75
149
|
resolve({
|
|
@@ -88,6 +162,16 @@ export async function runGt(
|
|
|
88
162
|
let stdout = "";
|
|
89
163
|
let stderr = "";
|
|
90
164
|
let killed = false;
|
|
165
|
+
let settled = false;
|
|
166
|
+
|
|
167
|
+
const killChild = () => {
|
|
168
|
+
killed = true;
|
|
169
|
+
killProcessGroup(child, "SIGTERM");
|
|
170
|
+
setTimeout(() => killProcessGroup(child, "SIGKILL"), 1500).unref?.();
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const timeout = setTimeout(killChild, opts.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS);
|
|
174
|
+
timeout.unref?.();
|
|
91
175
|
|
|
92
176
|
child.stdout?.on("data", (d) => {
|
|
93
177
|
stdout += d.toString();
|
|
@@ -98,20 +182,14 @@ export async function runGt(
|
|
|
98
182
|
if (stderr.length > MAX_BYTES * 4) stderr = stderr.slice(-MAX_BYTES * 2);
|
|
99
183
|
});
|
|
100
184
|
|
|
101
|
-
const onAbort =
|
|
102
|
-
killed = true;
|
|
103
|
-
try {
|
|
104
|
-
child.kill("SIGTERM");
|
|
105
|
-
setTimeout(() => {
|
|
106
|
-
try {
|
|
107
|
-
child.kill("SIGKILL");
|
|
108
|
-
} catch {}
|
|
109
|
-
}, 1500).unref?.();
|
|
110
|
-
} catch {}
|
|
111
|
-
};
|
|
185
|
+
const onAbort = killChild;
|
|
112
186
|
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
113
187
|
|
|
114
188
|
child.on("error", (err) => {
|
|
189
|
+
if (settled) return;
|
|
190
|
+
settled = true;
|
|
191
|
+
clearTimeout(timeout);
|
|
192
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
115
193
|
resolve({
|
|
116
194
|
command: "gt",
|
|
117
195
|
args,
|
|
@@ -119,12 +197,15 @@ export async function runGt(
|
|
|
119
197
|
exitCode: -1,
|
|
120
198
|
stdout: sanitizeBranding(stripAnsi(stdout)),
|
|
121
199
|
stderr: sanitizeBranding(stripAnsi(stderr)),
|
|
122
|
-
timedOut:
|
|
200
|
+
timedOut: killed,
|
|
123
201
|
spawnError: err.message,
|
|
124
202
|
});
|
|
125
203
|
});
|
|
126
204
|
|
|
127
205
|
child.on("close", (code) => {
|
|
206
|
+
if (settled) return;
|
|
207
|
+
settled = true;
|
|
208
|
+
clearTimeout(timeout);
|
|
128
209
|
opts.signal?.removeEventListener("abort", onAbort);
|
|
129
210
|
resolve({
|
|
130
211
|
command: "gt",
|
package/src/lib/result.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import type
|
|
2
|
+
import { DEFAULT_COMMAND_TIMEOUT_MS, killProcessGroup, safeNoninteractiveEnv, type GtRunResult } from "./exec";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Structured failure hints. Only populated when the underlying `gt` command
|
|
@@ -140,17 +140,33 @@ function execText(cmd: string, args: string[], cwd: string): Promise<string> {
|
|
|
140
140
|
let out = "";
|
|
141
141
|
let child;
|
|
142
142
|
try {
|
|
143
|
-
child = spawn(cmd, args, {
|
|
143
|
+
child = spawn(cmd, args, {
|
|
144
|
+
cwd,
|
|
145
|
+
env: safeNoninteractiveEnv(),
|
|
146
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
147
|
+
detached: process.platform !== "win32",
|
|
148
|
+
});
|
|
144
149
|
} catch {
|
|
145
150
|
resolve("");
|
|
146
151
|
return;
|
|
147
152
|
}
|
|
153
|
+
const timeout = setTimeout(() => {
|
|
154
|
+
killProcessGroup(child, "SIGTERM");
|
|
155
|
+
setTimeout(() => killProcessGroup(child, "SIGKILL"), 1500).unref?.();
|
|
156
|
+
}, DEFAULT_COMMAND_TIMEOUT_MS);
|
|
157
|
+
timeout.unref?.();
|
|
148
158
|
child.stdout.on("data", (d) => {
|
|
149
159
|
out += d.toString();
|
|
150
160
|
if (out.length > 4096) out = out.slice(-4096);
|
|
151
161
|
});
|
|
152
|
-
child.on("error", () =>
|
|
153
|
-
|
|
162
|
+
child.on("error", () => {
|
|
163
|
+
clearTimeout(timeout);
|
|
164
|
+
resolve("");
|
|
165
|
+
});
|
|
166
|
+
child.on("close", () => {
|
|
167
|
+
clearTimeout(timeout);
|
|
168
|
+
resolve(out);
|
|
169
|
+
});
|
|
154
170
|
});
|
|
155
171
|
}
|
|
156
172
|
|
|
@@ -188,28 +204,28 @@ export async function enrichFailure(cwd: string, f: FormattedResult): Promise<vo
|
|
|
188
204
|
const b = branch ?? "<current-branch>";
|
|
189
205
|
const p = trunk ?? "<trunk>";
|
|
190
206
|
parts.push(
|
|
191
|
-
`Current branch (${b}) is not tracked by Graphite. To track it, call: ` +
|
|
192
|
-
`
|
|
193
|
-
`
|
|
207
|
+
`Current branch (${b}) is not tracked by Graphite. To track it after verifying the intended parent, call: ` +
|
|
208
|
+
`graphite_setup({ action: "track_branch", branch: "${b}", parent: "${p}", confirmParent: true }). ` +
|
|
209
|
+
`Do not guess the parent if unclear; ask the user first.`,
|
|
194
210
|
);
|
|
195
211
|
}
|
|
196
212
|
if (f.hints.notInitialized) {
|
|
197
213
|
parts.push(
|
|
198
|
-
`Graphite not initialized in this repo. Call:
|
|
214
|
+
`Graphite not initialized in this repo. Call: graphite_setup({ action: "init_repo", trunk: "<trunk-branch>" }).`,
|
|
199
215
|
);
|
|
200
216
|
}
|
|
201
217
|
if (f.hints.conflictHalted) {
|
|
202
218
|
parts.push(
|
|
203
219
|
`A Graphite command is halted by a conflict. After resolving in git, call: ` +
|
|
204
|
-
`
|
|
220
|
+
`graphite_recover({ action: "continue" }) (or "abort").`,
|
|
205
221
|
);
|
|
206
222
|
}
|
|
207
223
|
if (f.hints.restackNeeded) {
|
|
208
|
-
parts.push(`Stack is out of date. Call:
|
|
224
|
+
parts.push(`Stack is out of date. Call: graphite_sync() to sync and restack.`);
|
|
209
225
|
}
|
|
210
226
|
if (f.hints.trunkOutOfSync) {
|
|
211
227
|
parts.push(
|
|
212
|
-
`Trunk is out of sync with remote. Call:
|
|
228
|
+
`Trunk is out of sync with remote. Call: graphite_sync() first.`,
|
|
213
229
|
);
|
|
214
230
|
}
|
|
215
231
|
if (f.hints.notAuthenticated) {
|
|
@@ -221,7 +237,7 @@ export async function enrichFailure(cwd: string, f: FormattedResult): Promise<vo
|
|
|
221
237
|
const b = f.hints.checkedOutElsewhere.branch ?? "<branch>";
|
|
222
238
|
parts.push(
|
|
223
239
|
`Branch ${b} is checked out in another worktree. Switch to that worktree, or use ` +
|
|
224
|
-
|
|
240
|
+
`switch to a different branch with graphite_navigate before mutating the stack.`,
|
|
225
241
|
);
|
|
226
242
|
}
|
|
227
243
|
if (f.hints.invalidArgument) {
|
|
@@ -231,7 +247,7 @@ export async function enrichFailure(cwd: string, f: FormattedResult): Promise<vo
|
|
|
231
247
|
}
|
|
232
248
|
if (f.hints.operatingOnTrunk) {
|
|
233
249
|
parts.push(
|
|
234
|
-
`Operation refused on the trunk branch. Check out a non-trunk branch first
|
|
250
|
+
`Operation refused on the trunk branch. Check out a non-trunk branch first with graphite_navigate.`,
|
|
235
251
|
);
|
|
236
252
|
}
|
|
237
253
|
|
package/src/lib/schema.ts
CHANGED
|
@@ -9,10 +9,11 @@ export const StageMode = StringEnum([
|
|
|
9
9
|
"none",
|
|
10
10
|
"all",
|
|
11
11
|
"update",
|
|
12
|
-
"patch",
|
|
13
12
|
] as const);
|
|
14
13
|
|
|
15
|
-
export
|
|
14
|
+
export type StageModeValue = "none" | "all" | "update";
|
|
15
|
+
|
|
16
|
+
export function stageArgs(mode: StageModeValue): string[] {
|
|
16
17
|
switch (mode) {
|
|
17
18
|
case "none":
|
|
18
19
|
return [];
|
|
@@ -20,9 +21,9 @@ export function stageArgs(mode: "none" | "all" | "update" | "patch"): string[] {
|
|
|
20
21
|
return ["--all"];
|
|
21
22
|
case "update":
|
|
22
23
|
return ["--update"];
|
|
23
|
-
case "patch":
|
|
24
|
-
return ["--patch"];
|
|
25
24
|
}
|
|
25
|
+
const _exhaustive: never = mode;
|
|
26
|
+
return _exhaustive;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
/** Common cwd param. Required so we always pass an absolute path to gt. */
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { runGt } from "../lib/exec";
|
|
3
|
+
import { assertSafeRef, flagEq } from "../lib/argv";
|
|
4
|
+
import { ensureSuccess, renderText } from "../lib/result";
|
|
5
|
+
import {
|
|
6
|
+
CwdParam,
|
|
7
|
+
StringEnum,
|
|
8
|
+
Type,
|
|
9
|
+
type ToolReturn,
|
|
10
|
+
} from "../lib/schema";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* graphite_change — the only blessed branch-mutation path.
|
|
14
|
+
*
|
|
15
|
+
* action=create gt create -am "<message>" (new branch on top of current)
|
|
16
|
+
* action=amend gt modify -am "<message>" (amend current branch's commit)
|
|
17
|
+
* action=amend_into gt modify --into <branch> -am "<message>"
|
|
18
|
+
* action=absorb gt absorb (dry-run by default)
|
|
19
|
+
*
|
|
20
|
+
* Always stages all changes (matches the golden-path `-am`). No editor, no
|
|
21
|
+
* patch/hunk picker, no AI metadata.
|
|
22
|
+
*/
|
|
23
|
+
export function registerChange(pi: ExtensionAPI) {
|
|
24
|
+
pi.registerTool({
|
|
25
|
+
name: "graphite_change",
|
|
26
|
+
label: "Graphite: change",
|
|
27
|
+
description:
|
|
28
|
+
"Create or amend a branch commit in the Graphite stack. action=create stacks a new branch on top of the current one. action=amend updates the current branch's commit. action=amend_into pushes staged hunks into a downstack branch. action=absorb auto-routes staged hunks to the correct commits (dry-run by default).",
|
|
29
|
+
promptSnippet:
|
|
30
|
+
"graphite_change: create | amend | amend_into | absorb — the only branch mutation tool",
|
|
31
|
+
promptGuidelines: [
|
|
32
|
+
"Use graphite_change action=create to start a new PR branch on top of the current branch. Always provide `message`.",
|
|
33
|
+
"Use graphite_change action=amend to update the current PR's commit. Always provide `message`.",
|
|
34
|
+
"Run graphite_status first to confirm you are on the intended branch.",
|
|
35
|
+
"graphite_change always stages all changes (matches `gt create -am` / `gt modify -am`). Stage selectively with `git add -p` outside this tool if you need a partial commit, then call graphite_change.",
|
|
36
|
+
],
|
|
37
|
+
parameters: Type.Object({
|
|
38
|
+
cwd: CwdParam,
|
|
39
|
+
action: StringEnum([
|
|
40
|
+
"create",
|
|
41
|
+
"amend",
|
|
42
|
+
"amend_into",
|
|
43
|
+
"absorb",
|
|
44
|
+
] as const),
|
|
45
|
+
message: Type.Optional(
|
|
46
|
+
Type.String({
|
|
47
|
+
description:
|
|
48
|
+
"Commit message. Required for create/amend/amend_into.",
|
|
49
|
+
}),
|
|
50
|
+
),
|
|
51
|
+
name: Type.Optional(
|
|
52
|
+
Type.String({
|
|
53
|
+
description: "action=create: branch name (default generated from message).",
|
|
54
|
+
}),
|
|
55
|
+
),
|
|
56
|
+
insert: Type.Optional(
|
|
57
|
+
Type.Boolean({
|
|
58
|
+
description:
|
|
59
|
+
"action=create: insert between current branch and its child, rebasing children (--insert).",
|
|
60
|
+
}),
|
|
61
|
+
),
|
|
62
|
+
includeUntracked: Type.Optional(
|
|
63
|
+
Type.Boolean({
|
|
64
|
+
description:
|
|
65
|
+
"action=create|amend|amend_into: include untracked files (--update). Default false; staged + tracked-modified are always included via --all.",
|
|
66
|
+
}),
|
|
67
|
+
),
|
|
68
|
+
into: Type.Optional(
|
|
69
|
+
Type.String({
|
|
70
|
+
description: "action=amend_into: target downstack branch to amend into.",
|
|
71
|
+
}),
|
|
72
|
+
),
|
|
73
|
+
apply: Type.Optional(
|
|
74
|
+
Type.Boolean({
|
|
75
|
+
description:
|
|
76
|
+
"action=absorb: false (default) => --dry-run; true => --force (apply).",
|
|
77
|
+
}),
|
|
78
|
+
),
|
|
79
|
+
}),
|
|
80
|
+
async execute(_id, p, signal): Promise<ToolReturn> {
|
|
81
|
+
let args: string[];
|
|
82
|
+
switch (p.action) {
|
|
83
|
+
case "create": {
|
|
84
|
+
if (!p.message) {
|
|
85
|
+
throw new Error("graphite_change action=create requires `message`.");
|
|
86
|
+
}
|
|
87
|
+
args = ["create"];
|
|
88
|
+
if (p.name) args.push(assertSafeRef(p.name, "name"));
|
|
89
|
+
args.push(flagEq("--message", p.message));
|
|
90
|
+
// `-am` semantics: always stage tracked modifications.
|
|
91
|
+
args.push("--all");
|
|
92
|
+
if (p.includeUntracked) args.push("--update");
|
|
93
|
+
if (p.insert) args.push("--insert");
|
|
94
|
+
args.push("--no-ai");
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case "amend": {
|
|
98
|
+
if (!p.message) {
|
|
99
|
+
throw new Error("graphite_change action=amend requires `message`.");
|
|
100
|
+
}
|
|
101
|
+
args = ["modify", "--all"];
|
|
102
|
+
if (p.includeUntracked) args.push("--update");
|
|
103
|
+
args.push(flagEq("--message", p.message));
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case "amend_into": {
|
|
107
|
+
if (!p.into) {
|
|
108
|
+
throw new Error("graphite_change action=amend_into requires `into`.");
|
|
109
|
+
}
|
|
110
|
+
if (!p.message) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
"graphite_change action=amend_into requires `message`.",
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
args = ["modify", "--all"];
|
|
116
|
+
if (p.includeUntracked) args.push("--update");
|
|
117
|
+
args.push(flagEq("--into", assertSafeRef(p.into, "into")));
|
|
118
|
+
args.push(flagEq("--message", p.message));
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case "absorb": {
|
|
122
|
+
const apply = p.apply === true;
|
|
123
|
+
args = ["absorb"];
|
|
124
|
+
if (!apply) args.push("--dry-run");
|
|
125
|
+
else args.push("--force");
|
|
126
|
+
// Match `-am` style: include tracked modifications for absorb too.
|
|
127
|
+
args.push("--all");
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const label = `gt ${args.join(" ")}`;
|
|
132
|
+
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
133
|
+
const f = await ensureSuccess(label, r, p.cwd);
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: "text", text: renderText(label, f) }],
|
|
136
|
+
details: { action: p.action, result: f },
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { runGt } from "../lib/exec";
|
|
3
|
+
import { assertSafeRef, flagEq } from "../lib/argv";
|
|
4
|
+
import { ensureSuccess, renderText } from "../lib/result";
|
|
5
|
+
import {
|
|
6
|
+
CwdParam,
|
|
7
|
+
StringEnum,
|
|
8
|
+
Type,
|
|
9
|
+
type ToolReturn,
|
|
10
|
+
} from "../lib/schema";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* graphite_navigate — move around the current stack.
|
|
14
|
+
*
|
|
15
|
+
* Stacked PRs encode "which branch you are on = which PR you will modify",
|
|
16
|
+
* so navigation is a core part of the workflow. Use this before
|
|
17
|
+
* graphite_change to make sure you are on the right branch:
|
|
18
|
+
* - to update an existing PR, checkout that PR's branch
|
|
19
|
+
* - to add a child PR, navigate to its intended parent first
|
|
20
|
+
* - to add a base PR, navigate to trunk
|
|
21
|
+
*/
|
|
22
|
+
export function registerNavigate(pi: ExtensionAPI) {
|
|
23
|
+
pi.registerTool({
|
|
24
|
+
name: "graphite_navigate",
|
|
25
|
+
label: "Graphite: navigate",
|
|
26
|
+
description:
|
|
27
|
+
"Move around the current Graphite stack: checkout a specific branch, jump to trunk, or step up/down/top/bottom. Use this before graphite_change so you are mutating the right PR.",
|
|
28
|
+
promptSnippet:
|
|
29
|
+
"graphite_navigate: checkout / trunk / up / down / top / bottom in the current stack",
|
|
30
|
+
promptGuidelines: [
|
|
31
|
+
"Before any graphite_change, confirm you are on the intended branch via graphite_status or graphite_navigate.",
|
|
32
|
+
"To create a child PR, navigate to its intended parent first. To create a base PR, navigate to trunk.",
|
|
33
|
+
],
|
|
34
|
+
parameters: Type.Object({
|
|
35
|
+
cwd: CwdParam,
|
|
36
|
+
action: StringEnum([
|
|
37
|
+
"checkout",
|
|
38
|
+
"trunk",
|
|
39
|
+
"up",
|
|
40
|
+
"down",
|
|
41
|
+
"top",
|
|
42
|
+
"bottom",
|
|
43
|
+
] as const),
|
|
44
|
+
branch: Type.Optional(
|
|
45
|
+
Type.String({ description: "action=checkout: branch to checkout." }),
|
|
46
|
+
),
|
|
47
|
+
steps: Type.Optional(
|
|
48
|
+
Type.Integer({
|
|
49
|
+
minimum: 1,
|
|
50
|
+
description: "action=up|down: step count.",
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
to: Type.Optional(
|
|
54
|
+
Type.String({
|
|
55
|
+
description:
|
|
56
|
+
"action=up: target descendant when the current branch has multiple children (--to).",
|
|
57
|
+
}),
|
|
58
|
+
),
|
|
59
|
+
}),
|
|
60
|
+
async execute(_id, p, signal): Promise<ToolReturn> {
|
|
61
|
+
let args: string[];
|
|
62
|
+
switch (p.action) {
|
|
63
|
+
case "checkout":
|
|
64
|
+
if (!p.branch) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"action=checkout requires `branch` (interactive selector disabled). Use action=trunk to jump to trunk.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
args = ["checkout", assertSafeRef(p.branch, "branch")];
|
|
70
|
+
break;
|
|
71
|
+
case "trunk":
|
|
72
|
+
args = ["checkout", "--trunk"];
|
|
73
|
+
break;
|
|
74
|
+
case "up":
|
|
75
|
+
args = ["up"];
|
|
76
|
+
if (p.steps != null) args.push(flagEq("--steps", p.steps));
|
|
77
|
+
if (p.to) args.push(flagEq("--to", assertSafeRef(p.to, "to")));
|
|
78
|
+
break;
|
|
79
|
+
case "down":
|
|
80
|
+
args = ["down"];
|
|
81
|
+
if (p.steps != null) args.push(flagEq("--steps", p.steps));
|
|
82
|
+
break;
|
|
83
|
+
case "top":
|
|
84
|
+
args = ["top"];
|
|
85
|
+
break;
|
|
86
|
+
case "bottom":
|
|
87
|
+
args = ["bottom"];
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
const label = `gt ${args.join(" ")}`;
|
|
91
|
+
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
92
|
+
const f = await ensureSuccess(label, r, p.cwd);
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: "text", text: renderText(label, f) }],
|
|
95
|
+
details: { action: p.action, result: f },
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|