pi-graphite 0.2.3 → 0.3.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/README.md +80 -37
- package/package.json +6 -2
- package/skills/graphite/SKILL.md +218 -0
- package/src/index.ts +40 -57
- package/src/lib/argv.ts +81 -0
- package/src/lib/exec.ts +35 -3
- package/src/lib/result.ts +17 -12
- package/src/tools/change.ts +140 -0
- package/src/tools/navigate.ts +99 -0
- package/src/tools/{recovery.ts → recover.ts} +39 -25
- package/src/tools/setup.ts +121 -0
- package/src/tools/status.ts +55 -0
- package/src/tools/submit.ts +139 -0
- package/src/tools/sync.ts +80 -0
- package/src/tools/branch.ts +0 -432
- package/src/tools/pr.ts +0 -298
- package/src/tools/remote.ts +0 -108
- package/src/tools/repo.ts +0 -114
- package/src/tools/stack.ts +0 -572
package/README.md
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# pi-graphite
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Opinionated pi tools + skill that wrap the [Graphite](https://graphite.com)
|
|
4
|
+
`gt` CLI for stacked PR workflows. Seven tools, one correct path.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
```
|
|
7
|
+
graphite_status → (graphite_setup if needed) → graphite_sync → graphite_navigate
|
|
8
|
+
→ graphite_change → graphite_submit_stack (dry-run) → graphite_submit_stack (apply)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The extension wraps `gt` only. It deliberately does **not** call `gh`, edit PR
|
|
12
|
+
titles/bodies, fetch review comments, or perform stack surgery
|
|
13
|
+
(split/fold/move/squash). Use the `gt` or `gh` CLI directly for those.
|
|
9
14
|
|
|
10
15
|
## Requirements
|
|
11
16
|
|
|
@@ -27,40 +32,78 @@ pi install /path/to/pi-graphite
|
|
|
27
32
|
pi -e /path/to/pi-graphite
|
|
28
33
|
```
|
|
29
34
|
|
|
35
|
+
The package also ships a `graphite` skill (`skills/graphite/SKILL.md`) that pi
|
|
36
|
+
auto-discovers. It describes the golden path and per-recipe tool calls; the
|
|
37
|
+
agent loads it on demand.
|
|
38
|
+
|
|
30
39
|
## Registered tools
|
|
31
40
|
|
|
32
|
-
| Tool
|
|
33
|
-
|
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
38
|
-
| `
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
41
|
+
| Tool | Purpose | Wraps |
|
|
42
|
+
| ------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------- |
|
|
43
|
+
| `graphite_status` | Read-only snapshot: current stack + current branch + PR + restack hints | `gt log --stack`, `gt info` |
|
|
44
|
+
| `graphite_setup` | Initialize Graphite or track an existing Git branch with explicit parent | `gt init --trunk`, `gt track --parent` |
|
|
45
|
+
| `graphite_sync` | Start-of-day / after-merge cleanup + restack | `gt sync` |
|
|
46
|
+
| `graphite_navigate` | Move around the stack | `gt checkout`, `gt up`/`down`/`top`/`bottom` |
|
|
47
|
+
| `graphite_change` | Create / amend a stacked branch | `gt create -am`, `gt modify -am`, `gt modify --into`, `gt absorb` |
|
|
48
|
+
| `graphite_submit_stack` | Push the entire stack and open/update PRs (dry-run by default) | `gt submit --stack --no-edit --no-ai` |
|
|
49
|
+
| `graphite_recover` | Continue / abort / undo | `gt continue`, `gt abort`, `gt undo` |
|
|
50
|
+
|
|
51
|
+
## Golden path
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
graphite_status
|
|
55
|
+
graphite_setup # only if repo not initialized or branch untracked
|
|
56
|
+
graphite_sync # at session start, or after merges
|
|
57
|
+
graphite_navigate action=checkout branch=… # move to the target PR / parent
|
|
58
|
+
# user edits files
|
|
59
|
+
graphite_change action=create message="…" # or action=amend
|
|
60
|
+
graphite_submit_stack apply=false # review the dry-run plan
|
|
61
|
+
graphite_submit_stack apply=true confirmRemote=true
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Conflict path:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
# resolve files, git add them
|
|
68
|
+
graphite_recover action=continue
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Never run `git rebase --continue` after a gt command — use
|
|
72
|
+
`graphite_recover action=continue` so Graphite propagates the resolution to
|
|
73
|
+
dependent branches.
|
|
74
|
+
|
|
75
|
+
## Conventions and guardrails
|
|
76
|
+
|
|
77
|
+
- Every tool requires absolute `cwd`.
|
|
78
|
+
- `gt` is invoked with `--cwd <cwd> --no-interactive`, no shell strings. Tools that support AI metadata pass `--no-ai`.
|
|
79
|
+
- Editor / pager / browser env is forced safe (`GT_EDITOR=true`, `GT_PAGER=`,
|
|
80
|
+
`BROWSER=true`, …). Commands have a hard timeout.
|
|
81
|
+
- Interactive editor / hunk / browser / reorder paths are not exposed.
|
|
82
|
+
- Rendered `$ gt …` command lines in tool output are POSIX shell-quoted so
|
|
83
|
+
copy-paste cannot trigger command substitution or word-splitting from
|
|
84
|
+
user-controlled args.
|
|
85
|
+
- `graphite_setup action=track_branch` requires explicit `branch`, explicit
|
|
86
|
+
`parent`, and `confirmParent:true`; do not guess parent if unclear.
|
|
87
|
+
- `graphite_setup action=init_repo reset:true` needs `confirmDestructive:true`.
|
|
88
|
+
- `graphite_submit_stack` defaults to `--dry-run`; `apply:true` also needs
|
|
89
|
+
`confirmRemote:true`. `--force` push also requires `confirmRemote:true`.
|
|
90
|
+
- `graphite_sync` with `force` or `deleteAll` needs `confirmDestructive:true`.
|
|
91
|
+
- `graphite_recover action=continue` refuses to proceed if tracked files
|
|
92
|
+
still contain `<<<<<<<` markers, unless `allowConflictMarkers:true`.
|
|
93
|
+
- Output is ANSI-stripped, branded ("Graphite" not "Charcoal"), and truncated
|
|
94
|
+
to ~50 KB / 2000 lines.
|
|
95
|
+
- Stderr is parsed into structured `hints`
|
|
96
|
+
(`notInitialized`, `conflictHalted`, `restackNeeded`, `trunkOutOfSync`,
|
|
97
|
+
`branchNotTracked`, `noChangesStaged`, `checkedOutElsewhere`,
|
|
98
|
+
`operatingOnTrunk`, …).
|
|
99
|
+
|
|
100
|
+
### Known surface: git hooks
|
|
101
|
+
|
|
102
|
+
This extension does not pass `--no-verify` to `gt` / `git`. Any
|
|
103
|
+
`pre-commit`, `commit-msg`, `pre-push`, or related hook configured in the
|
|
104
|
+
target repo will execute as part of mutating operations (create, amend,
|
|
105
|
+
submit, …). Hooks are arbitrary user code and are intentionally not
|
|
106
|
+
bypassed; treat hook content as part of the repo's trust boundary.
|
|
64
107
|
|
|
65
108
|
## License
|
|
66
109
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-graphite",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Opinionated pi tools + skill for stacked PR workflows with the Graphite (gt) CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
7
7
|
"pi-package",
|
|
@@ -12,12 +12,16 @@
|
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"files": [
|
|
14
14
|
"src",
|
|
15
|
+
"skills",
|
|
15
16
|
"README.md",
|
|
16
17
|
"LICENSE"
|
|
17
18
|
],
|
|
18
19
|
"pi": {
|
|
19
20
|
"extensions": [
|
|
20
21
|
"./src/index.ts"
|
|
22
|
+
],
|
|
23
|
+
"skills": [
|
|
24
|
+
"./skills"
|
|
21
25
|
]
|
|
22
26
|
},
|
|
23
27
|
"peerDependencies": {
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: graphite
|
|
3
|
+
description: Manage stacked PRs with the Graphite (gt) CLI via the pi-graphite extension. Use when creating, updating, navigating, or pushing a Graphite stack, or when recovering from a halted gt command. Wraps `gt` only — does not touch PR titles/bodies, reviews, or stack surgery.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Graphite (pi-graphite)
|
|
7
|
+
|
|
8
|
+
This skill drives the [pi-graphite](https://www.npmjs.com/package/pi-graphite)
|
|
9
|
+
extension. The extension is a deliberately small, opinionated wrapper around
|
|
10
|
+
the Graphite (`gt`) CLI. There is exactly one correct workflow; follow it.
|
|
11
|
+
|
|
12
|
+
## When to use
|
|
13
|
+
|
|
14
|
+
Use this skill whenever the user wants to:
|
|
15
|
+
|
|
16
|
+
- start work on a stacked-PR repo
|
|
17
|
+
- create a new PR on top of the current branch
|
|
18
|
+
- amend an existing PR with new changes
|
|
19
|
+
- push the current stack to GitHub
|
|
20
|
+
- sync after PRs merged on `main`
|
|
21
|
+
- recover from a gt conflict
|
|
22
|
+
|
|
23
|
+
Do not use it for:
|
|
24
|
+
|
|
25
|
+
- editing PR titles / bodies / labels / reviewers metadata (use `gh` directly)
|
|
26
|
+
- reading PR review comments or CI status (use `gh` directly)
|
|
27
|
+
- rewriting history beyond create/amend (split/fold/move/squash) — use the
|
|
28
|
+
raw `gt` CLI via bash if the user explicitly asks; the extension does not
|
|
29
|
+
expose stack surgery
|
|
30
|
+
|
|
31
|
+
## Tools
|
|
32
|
+
|
|
33
|
+
The extension registers seven tools. Prefer them over `gt`/`git`/`gh` in bash.
|
|
34
|
+
|
|
35
|
+
| Tool | Purpose |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `graphite_status` | Read-only snapshot: current stack + current branch + PR + restack hint |
|
|
38
|
+
| `graphite_setup` | Initialize Graphite or track an existing Git branch with explicit parent |
|
|
39
|
+
| `graphite_sync` | `gt sync` — pull trunk, drop merged branches, restack |
|
|
40
|
+
| `graphite_navigate` | `gt checkout` / `up` / `down` / `top` / `bottom` / trunk |
|
|
41
|
+
| `graphite_change` | `gt create` / `gt modify` / `gt modify --into` / `gt absorb` |
|
|
42
|
+
| `graphite_submit_stack` | `gt submit --stack --no-edit` (dry-run by default) |
|
|
43
|
+
| `graphite_recover` | `gt continue` / `gt abort` / `gt undo` |
|
|
44
|
+
|
|
45
|
+
All tools require an absolute `cwd`.
|
|
46
|
+
|
|
47
|
+
## Golden path
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
graphite_status
|
|
51
|
+
↓
|
|
52
|
+
graphite_setup (only if repo not initialized or branch untracked)
|
|
53
|
+
↓
|
|
54
|
+
graphite_sync (start of session, or after PRs merged)
|
|
55
|
+
↓
|
|
56
|
+
graphite_navigate (move to the branch you want to mutate)
|
|
57
|
+
↓
|
|
58
|
+
graphite_change (create or amend)
|
|
59
|
+
↓
|
|
60
|
+
graphite_submit_stack apply=false (review dry-run plan)
|
|
61
|
+
↓
|
|
62
|
+
graphite_submit_stack apply=true (push, with confirmRemote=true)
|
|
63
|
+
confirmRemote=true
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
When a `gt` command halts on conflict:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
resolve files in editor → git add <files> → graphite_recover action="continue"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Never run `git rebase --continue` after a Graphite-initiated rebase; use
|
|
73
|
+
`graphite_recover action="continue"` so Graphite propagates the resolution
|
|
74
|
+
to dependent branches.
|
|
75
|
+
|
|
76
|
+
## Recipes
|
|
77
|
+
|
|
78
|
+
### Initialize repo if Graphite is missing
|
|
79
|
+
|
|
80
|
+
If a tool reports `notInitialized`:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
# Ask user for trunk if unclear.
|
|
84
|
+
graphite_setup({ cwd, action: "init_repo", trunk: "main" })
|
|
85
|
+
graphite_status({ cwd })
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
If resetting existing Graphite metadata is explicitly intended:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
graphite_setup({ cwd, action: "init_repo", trunk: "main", reset: true, confirmDestructive: true })
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Track existing Git branch if untracked
|
|
95
|
+
|
|
96
|
+
If a tool reports `branchNotTracked`:
|
|
97
|
+
|
|
98
|
+
1. Identify the intended Graphite parent branch.
|
|
99
|
+
2. If parent is unclear, ask the user.
|
|
100
|
+
3. Track only after parent is confirmed.
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
graphite_setup({
|
|
104
|
+
cwd,
|
|
105
|
+
action: "track_branch",
|
|
106
|
+
branch: "<existing-git-branch>",
|
|
107
|
+
parent: "<intended-parent-branch>",
|
|
108
|
+
confirmParent: true,
|
|
109
|
+
})
|
|
110
|
+
graphite_status({ cwd })
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Never guess parent silently. Wrong parent means wrong stack shape.
|
|
114
|
+
|
|
115
|
+
### Start a session
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
graphite_status({ cwd })
|
|
119
|
+
graphite_sync({ cwd }) # pull trunk, drop merged, restack
|
|
120
|
+
graphite_status({ cwd }) # confirm state
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Create a new PR on top of the current branch
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
graphite_status({ cwd }) # confirm position
|
|
127
|
+
# ... user makes code changes ...
|
|
128
|
+
graphite_change({ cwd, action: "create", message: "..." })
|
|
129
|
+
graphite_submit_stack({ cwd, apply: false })
|
|
130
|
+
# review plan with user; then:
|
|
131
|
+
graphite_submit_stack({ cwd, apply: true, confirmRemote: true })
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Update an existing PR
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
graphite_status({ cwd })
|
|
138
|
+
graphite_navigate({ cwd, action: "checkout", branch: "<pr-branch>" })
|
|
139
|
+
# ... user makes code changes ...
|
|
140
|
+
graphite_change({ cwd, action: "amend", message: "..." })
|
|
141
|
+
graphite_submit_stack({ cwd, apply: false })
|
|
142
|
+
graphite_submit_stack({ cwd, apply: true, confirmRemote: true })
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Add a child PR off a specific parent
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
graphite_navigate({ cwd, action: "checkout", branch: "<parent-branch>" })
|
|
149
|
+
# ... changes ...
|
|
150
|
+
graphite_change({ cwd, action: "create", message: "..." })
|
|
151
|
+
graphite_submit_stack({ cwd, apply: false })
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Land changes into a downstack branch
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
# Make the change locally (working tree dirty).
|
|
158
|
+
graphite_change({ cwd, action: "amend_into", into: "<downstack-branch>", message: "..." })
|
|
159
|
+
graphite_status({ cwd })
|
|
160
|
+
graphite_submit_stack({ cwd, apply: false })
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
For larger reshuffles touching several downstack commits, prefer `absorb`:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
graphite_change({ cwd, action: "absorb" }) # dry-run
|
|
167
|
+
graphite_change({ cwd, action: "absorb", apply: true }) # apply
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### After PRs in the stack merge
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
graphite_sync({ cwd })
|
|
174
|
+
graphite_status({ cwd })
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
If `gt sync` halts on conflict, use the conflict recipe below.
|
|
178
|
+
|
|
179
|
+
### Resolve a conflict
|
|
180
|
+
|
|
181
|
+
1. Read the failing tool's `--- stderr ---` and `hints` block.
|
|
182
|
+
2. Resolve markers in the listed files.
|
|
183
|
+
3. `git add <files>` from bash.
|
|
184
|
+
4. `graphite_recover({ cwd, action: "continue" })`.
|
|
185
|
+
5. If you want to bail entirely: `graphite_recover({ cwd, action: "abort" })`.
|
|
186
|
+
|
|
187
|
+
If you made a mistake with the last gt command:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
graphite_recover({ cwd, action: "undo" })
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Rules
|
|
194
|
+
|
|
195
|
+
- **Submit stacks, not branches.** `graphite_submit_stack` always passes
|
|
196
|
+
`--stack`. Do not look for a single-branch submit; if the user truly wants
|
|
197
|
+
to push only one branch, use `gt submit --branch=<name>` via bash and
|
|
198
|
+
explain why.
|
|
199
|
+
- **Use `graphite_setup` only for preconditions.** Initialize missing repos
|
|
200
|
+
or track existing Git branches. Do not use it for daily branch creation;
|
|
201
|
+
use `graphite_change action="create"` instead.
|
|
202
|
+
- **Never guess tracking parent.** `track_branch` requires explicit branch,
|
|
203
|
+
explicit parent, and `confirmParent:true`.
|
|
204
|
+
- **Prefer `graphite_sync` over manual restack.** The extension does not
|
|
205
|
+
expose a standalone restack tool. Sync covers both pull + restack.
|
|
206
|
+
- **Always dry-run first.** Show the user the dry-run plan from
|
|
207
|
+
`graphite_submit_stack apply=false` before pushing.
|
|
208
|
+
- **`apply:true` requires `confirmRemote:true`.** The tool will refuse
|
|
209
|
+
otherwise. This is intentional friction.
|
|
210
|
+
- **Destructive sync flags require `confirmDestructive:true`** (`force`,
|
|
211
|
+
`deleteAll`).
|
|
212
|
+
- **Never use `git rebase --continue` after a gt command.** Use
|
|
213
|
+
`graphite_recover action="continue"`.
|
|
214
|
+
- **This extension wraps gt only.** For PR body/title edits, review
|
|
215
|
+
comments, check runs, etc., shell out to `gh` directly in bash — do not
|
|
216
|
+
expect a tool from this extension.
|
|
217
|
+
- **No interactive editor / browser / hunk picker.** All paths are
|
|
218
|
+
non-interactive; pass explicit messages.
|
package/src/index.ts
CHANGED
|
@@ -1,69 +1,52 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "./tools/
|
|
10
|
-
import {
|
|
11
|
-
registerBranchInspect,
|
|
12
|
-
registerBranchCreate,
|
|
13
|
-
registerBranchUpdate,
|
|
14
|
-
registerBranchTracking,
|
|
15
|
-
registerBranchNavigate,
|
|
16
|
-
} from "./tools/branch";
|
|
17
|
-
import { registerRemoteSync } from "./tools/remote";
|
|
18
|
-
import { registerPrSubmit, registerPrLifecycle } from "./tools/pr";
|
|
19
|
-
import { registerRecovery } from "./tools/recovery";
|
|
3
|
+
import { registerStatus } from "./tools/status";
|
|
4
|
+
import { registerSetup } from "./tools/setup";
|
|
5
|
+
import { registerSync } from "./tools/sync";
|
|
6
|
+
import { registerNavigate } from "./tools/navigate";
|
|
7
|
+
import { registerChange } from "./tools/change";
|
|
8
|
+
import { registerSubmitStack } from "./tools/submit";
|
|
9
|
+
import { registerRecover } from "./tools/recover";
|
|
20
10
|
|
|
21
11
|
/**
|
|
22
|
-
* pi-graphite —
|
|
12
|
+
* pi-graphite — opinionated `gt` wrapper for stacked PR workflows.
|
|
23
13
|
*
|
|
24
|
-
*
|
|
14
|
+
* Seven workflow tools, one correct path:
|
|
25
15
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
16
|
+
* graphite_status — see where you are in the stack
|
|
17
|
+
* graphite_setup — init repo / track existing branch when needed
|
|
18
|
+
* graphite_sync — start-of-day / after-merge cleanup + restack
|
|
19
|
+
* graphite_navigate — move to the branch / PR you want to mutate
|
|
20
|
+
* graphite_change — create or amend a stacked branch
|
|
21
|
+
* graphite_submit_stack — push the whole stack and open/update PRs
|
|
22
|
+
* graphite_recover — continue / abort / undo after conflicts or mistakes
|
|
23
|
+
*
|
|
24
|
+
* Golden path:
|
|
25
|
+
*
|
|
26
|
+
* status → (setup if needed) → sync → navigate → change → submit_stack(dry-run) → submit_stack(apply)
|
|
27
|
+
*
|
|
28
|
+
* Conflict path:
|
|
29
|
+
*
|
|
30
|
+
* resolve files → graphite_recover continue
|
|
40
31
|
*
|
|
41
32
|
* Conventions:
|
|
42
33
|
* - Every tool requires absolute `cwd`.
|
|
43
|
-
* - `gt` is invoked with --cwd <cwd> --no-interactive by default.
|
|
44
|
-
* - Editor/pager/browser env is forced safe; interactive
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
34
|
+
* - `gt` is invoked with --cwd <cwd> --no-interactive by default; tools that support AI metadata pass --no-ai.
|
|
35
|
+
* - Editor / pager / browser env is forced safe; interactive editor / hunk /
|
|
36
|
+
* browser flows are not exposed.
|
|
37
|
+
* - graphite_submit_stack defaults to --dry-run; apply requires
|
|
38
|
+
* `apply:true` AND `confirmRemote:true`.
|
|
39
|
+
* - graphite_sync with force / deleteAll requires `confirmDestructive:true`.
|
|
40
|
+
* - This extension wraps `gt` only. It deliberately does not call `gh`,
|
|
41
|
+
* touch PR titles/bodies, run reviews, or do stack surgery
|
|
42
|
+
* (split/fold/move/squash). Use the gt CLI or another tool for those.
|
|
48
43
|
*/
|
|
49
44
|
export default function (pi: ExtensionAPI) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
registerBranchInspect(pi);
|
|
58
|
-
registerBranchCreate(pi);
|
|
59
|
-
registerBranchUpdate(pi);
|
|
60
|
-
registerBranchTracking(pi);
|
|
61
|
-
registerBranchNavigate(pi);
|
|
62
|
-
|
|
63
|
-
registerRemoteSync(pi);
|
|
64
|
-
|
|
65
|
-
registerPrSubmit(pi);
|
|
66
|
-
registerPrLifecycle(pi);
|
|
67
|
-
|
|
68
|
-
registerRecovery(pi);
|
|
45
|
+
registerStatus(pi);
|
|
46
|
+
registerSetup(pi);
|
|
47
|
+
registerSync(pi);
|
|
48
|
+
registerNavigate(pi);
|
|
49
|
+
registerChange(pi);
|
|
50
|
+
registerSubmitStack(pi);
|
|
51
|
+
registerRecover(pi);
|
|
69
52
|
}
|
package/src/lib/argv.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argv construction helpers used by every gt tool to harden against
|
|
3
|
+
* argv/flag-injection from user-controlled strings.
|
|
4
|
+
*
|
|
5
|
+
* Threat model: tool parameters (branch, message, onto, target, ...) end up
|
|
6
|
+
* as argv tokens passed to `gt`. `gt`'s yargs parser will happily interpret
|
|
7
|
+
* any token that starts with `-` as an option — including `--interactive`,
|
|
8
|
+
* which silently overrides our earlier `--no-interactive` and re-enables
|
|
9
|
+
* TTY prompts. It will also swallow the *next* argv token as the value of a
|
|
10
|
+
* preceding option (e.g. `--message` `--interactive` => message is dropped,
|
|
11
|
+
* --interactive becomes a flag).
|
|
12
|
+
*
|
|
13
|
+
* Two defenses:
|
|
14
|
+
*
|
|
15
|
+
* 1) For positional/ref values (branch names, refs, paths, PR numbers) we
|
|
16
|
+
* require that the value does not start with `-`. Git branch names
|
|
17
|
+
* cannot validly start with `-`; pathspecs and PR numbers shouldn't for
|
|
18
|
+
* these tools either.
|
|
19
|
+
*
|
|
20
|
+
* 2) For option *values* we emit `--flag=value` as a single argv token
|
|
21
|
+
* instead of two tokens `--flag` `value`. yargs binds the value to the
|
|
22
|
+
* option literally regardless of leading `-`, so the value can never be
|
|
23
|
+
* re-parsed as a flag.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export function assertSafeRef(value: string, label: string): string {
|
|
27
|
+
if (typeof value !== "string") {
|
|
28
|
+
throw new Error(`${label} must be a string.`);
|
|
29
|
+
}
|
|
30
|
+
if (value === "") {
|
|
31
|
+
throw new Error(`${label} must not be empty.`);
|
|
32
|
+
}
|
|
33
|
+
if (value === "--") {
|
|
34
|
+
throw new Error(`${label} must not be "--".`);
|
|
35
|
+
}
|
|
36
|
+
if (value.startsWith("-")) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`${label} must not start with "-" (got ${JSON.stringify(value)}). ` +
|
|
39
|
+
`Refused to prevent flag injection into the gt CLI.`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build a single argv token `--flag=value`. Use for option values supplied
|
|
47
|
+
* by the caller. The `=` form binds the value to the flag literally so a
|
|
48
|
+
* value like `--interactive` is preserved as data instead of being parsed
|
|
49
|
+
* as the next option.
|
|
50
|
+
*/
|
|
51
|
+
export function flagEq(flag: string, value: string | number): string {
|
|
52
|
+
if (!flag.startsWith("--")) {
|
|
53
|
+
throw new Error(`flagEq: flag must start with "--", got ${flag}`);
|
|
54
|
+
}
|
|
55
|
+
if (flag.includes("=")) {
|
|
56
|
+
throw new Error(`flagEq: flag must not already contain "=" (got ${flag}).`);
|
|
57
|
+
}
|
|
58
|
+
return `${flag}=${value}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* POSIX shell single-quote a value so the rendered command line is safe to
|
|
63
|
+
* copy-paste into a shell. argv execution itself never goes through a shell
|
|
64
|
+
* (we use spawn with an argv array), but rendered commands appear in tool
|
|
65
|
+
* output and labels; a user pasting them must not trigger command
|
|
66
|
+
* substitution, word splitting, or metacharacter interpretation.
|
|
67
|
+
*
|
|
68
|
+
* Rule: wrap in single quotes, and replace each embedded single quote with
|
|
69
|
+
* the POSIX-portable sequence '\''. Tokens consisting solely of
|
|
70
|
+
* [A-Za-z0-9_=:,.@/+-] are left unquoted for readability.
|
|
71
|
+
*/
|
|
72
|
+
export function shellQuote(arg: string): string {
|
|
73
|
+
if (arg.length === 0) return "''";
|
|
74
|
+
if (/^[A-Za-z0-9_=:,.@\/+\-]+$/.test(arg)) return arg;
|
|
75
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Join argv tokens into a shell-safe single line. */
|
|
79
|
+
export function shellJoin(args: readonly string[]): string {
|
|
80
|
+
return args.map(shellQuote).join(" ");
|
|
81
|
+
}
|
package/src/lib/exec.ts
CHANGED
|
@@ -41,6 +41,10 @@ export const SAFE_NONINTERACTIVE_ENV: Record<string, string> = {
|
|
|
41
41
|
LESS: "FRX",
|
|
42
42
|
BROWSER: "true",
|
|
43
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",
|
|
44
48
|
};
|
|
45
49
|
|
|
46
50
|
export function safeNoninteractiveEnv(extra?: Record<string, string>): NodeJS.ProcessEnv {
|
|
@@ -86,12 +90,27 @@ export function truncateOutput(s: string): string {
|
|
|
86
90
|
return `${kept}\n... [truncated: ${s.length - MAX_BYTES} more bytes]`;
|
|
87
91
|
}
|
|
88
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
|
+
|
|
89
105
|
/**
|
|
90
106
|
* Run `gt` with structured args. Never builds a shell string.
|
|
91
107
|
*
|
|
92
|
-
* - Always injects --cwd <abs
|
|
93
|
-
* -
|
|
94
|
-
*
|
|
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.
|
|
95
114
|
* - Does not inject --quiet (we want stderr diagnostics).
|
|
96
115
|
*/
|
|
97
116
|
export async function runGt(
|
|
@@ -99,8 +118,21 @@ export async function runGt(
|
|
|
99
118
|
opts: GtRunOptions,
|
|
100
119
|
): Promise<GtRunResult> {
|
|
101
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
|
+
}
|
|
102
131
|
const args = ["--cwd", cwd, "--no-interactive"];
|
|
103
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");
|
|
104
136
|
|
|
105
137
|
return new Promise<GtRunResult>((resolve) => {
|
|
106
138
|
let child: ChildProcessByStdio<null, Readable, Readable>;
|