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 CHANGED
@@ -1,11 +1,16 @@
1
1
  # pi-graphite
2
2
 
3
- Structured pi tools that wrap the [Graphite](https://graphite.com) `gt` CLI so
4
- agents (and humans) can drive stacked PR workflows safely from pi.
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
- This package is **Layer A** of the planned design — one tool per Graphite
7
- domain resource (repo / stack / branch / PR / recovery), not one tool per `gt`
8
- subcommand and not a workflow orchestrator.
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,37 +32,67 @@ 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 | Resource | Wraps |
33
- | ---------------------------- | -------- | ------------------------------------------------- |
34
- | `graphite_repo` | repo | `gt trunk`, `gt init`, `gt log short` |
35
- | `graphite_stack_view` | stack | `gt log` / `gt log short` / `gt log long` |
36
- | `graphite_stack_restack` | stack | `gt restack` (+ `--branch/--downstack/--upstack/--only`) |
37
- | `graphite_stack_reorganize` | stack | `gt move`, `gt fold`, `gt split --by-file` |
38
- | `graphite_branch_inspect` | branch | `gt info` (+ `gt parent`, `gt children`) |
39
- | `graphite_branch_create` | branch | `gt create` |
40
- | `graphite_branch_update` | branch | `gt modify`, `gt absorb`, `gt squash`, `gt pop`, `gt rename`, `gt delete` |
41
- | `graphite_branch_tracking` | branch | `gt track`, `gt untrack`, `gt freeze`, `gt unfreeze` |
42
- | `graphite_branch_navigate` | branch | `gt checkout`, `gt up`, `gt down`, `gt top`, `gt bottom` |
43
- | `graphite_remote_sync` | remote | `gt sync`, `gt get` |
44
- | `graphite_pr_submit` | PR | `gt submit` (dry-run by default) |
45
- | `graphite_pr_lifecycle` | PR | `gt pr`, `gt merge`, `gt unlink` |
46
- | `graphite_recovery` | recovery | `gt continue`, `gt abort`, `gt undo` |
47
-
48
- ## Conventions
49
-
50
- - Every tool requires an absolute `cwd`.
51
- - `gt` is invoked with `--cwd <cwd> --no-interactive` by default. No shell strings.
52
- - Remote / destructive operations require explicit ack flags:
53
- - `graphite_pr_submit` defaults to `--dry-run`; `apply: true` needs `confirmRemote: true`.
54
- - `graphite_pr_lifecycle action=merge` defaults to `--dry-run`; `apply: true` needs `confirmRemote: true`.
55
- - `graphite_remote_sync` with `force` or `deleteAll` needs `confirmDestructive: true`.
56
- - `graphite_branch_update action=delete close:true` needs `confirmRemote: true`.
57
- - `graphite_stack_reorganize action=fold foldClose:true` needs `confirmRemote: true`.
58
- - Output is ANSI-stripped and truncated to ~50 KB / 2000 lines.
59
- - Stderr is parsed into structured `hints` (e.g. `notInitialized`, `conflictHalted`,
60
- `checkedOutElsewhere`, `restackNeeded`, `trunkOutOfSync`).
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
+ - `graphite_setup action=track_branch` requires explicit `branch`, explicit
83
+ `parent`, and `confirmParent:true`; do not guess parent if unclear.
84
+ - `graphite_setup action=init_repo reset:true` needs `confirmDestructive:true`.
85
+ - `graphite_submit_stack` defaults to `--dry-run`; `apply:true` also needs
86
+ `confirmRemote:true`. `--force` push also requires `confirmRemote:true`.
87
+ - `graphite_sync` with `force` or `deleteAll` needs `confirmDestructive:true`.
88
+ - `graphite_recover action=continue` refuses to proceed if tracked files
89
+ still contain `<<<<<<<` markers, unless `allowConflictMarkers:true`.
90
+ - Output is ANSI-stripped, branded ("Graphite" not "Charcoal"), and truncated
91
+ to ~50 KB / 2000 lines.
92
+ - Stderr is parsed into structured `hints`
93
+ (`notInitialized`, `conflictHalted`, `restackNeeded`, `trunkOutOfSync`,
94
+ `branchNotTracked`, `noChangesStaged`, `checkedOutElsewhere`,
95
+ `operatingOnTrunk`, …).
61
96
 
62
97
  ## License
63
98
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-graphite",
3
- "version": "0.2.2",
4
- "description": "Structured pi tools for the Graphite (gt) CLI.",
3
+ "version": "0.3.0",
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,65 +1,52 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
- import { registerRepo } from "./tools/repo";
4
- import {
5
- registerStackView,
6
- registerStackRestack,
7
- registerStackReorganize,
8
- } from "./tools/stack";
9
- import {
10
- registerBranchInspect,
11
- registerBranchCreate,
12
- registerBranchUpdate,
13
- registerBranchTracking,
14
- registerBranchNavigate,
15
- } from "./tools/branch";
16
- import { registerRemoteSync } from "./tools/remote";
17
- import { registerPrSubmit, registerPrLifecycle } from "./tools/pr";
18
- 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";
19
10
 
20
11
  /**
21
- * pi-graphite — Layer A (Domain Resource).
12
+ * pi-graphite — opinionated `gt` wrapper for stacked PR workflows.
22
13
  *
23
- * Registers structured tools that wrap the Graphite (`gt`) CLI:
14
+ * Seven workflow tools, one correct path:
24
15
  *
25
- * graphite_repo
26
- * graphite_stack_view
27
- * graphite_stack_restack
28
- * graphite_stack_reorganize
29
- * graphite_branch_inspect
30
- * graphite_branch_create
31
- * graphite_branch_update
32
- * graphite_branch_tracking
33
- * graphite_branch_navigate
34
- * graphite_remote_sync
35
- * graphite_pr_submit
36
- * graphite_pr_lifecycle
37
- * graphite_recovery
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
38
31
  *
39
32
  * Conventions:
40
33
  * - Every tool requires absolute `cwd`.
41
- * - `gt` is invoked with --cwd <cwd> --no-interactive by default.
42
- * - Remote / destructive operations require explicit `confirmRemote` /
43
- * `confirmDestructive` flags. Submit/merge default to dry-run.
44
- * - Output is ANSI-stripped and truncated to ~50KB / 2000 lines.
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.
45
43
  */
46
44
  export default function (pi: ExtensionAPI) {
47
- registerRepo(pi);
48
-
49
- registerStackView(pi);
50
- registerStackRestack(pi);
51
- registerStackReorganize(pi);
52
-
53
- registerBranchInspect(pi);
54
- registerBranchCreate(pi);
55
- registerBranchUpdate(pi);
56
- registerBranchTracking(pi);
57
- registerBranchNavigate(pi);
58
-
59
- registerRemoteSync(pi);
60
-
61
- registerPrSubmit(pi);
62
- registerPrLifecycle(pi);
63
-
64
- registerRecovery(pi);
45
+ registerStatus(pi);
46
+ registerSetup(pi);
47
+ registerSync(pi);
48
+ registerNavigate(pi);
49
+ registerChange(pi);
50
+ registerSubmitStack(pi);
51
+ registerRecover(pi);
65
52
  }
@@ -0,0 +1,59 @@
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
+ }