diffing 0.1.2 → 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/README.md +103 -7
- package/dist/cli-agent-BLPX3oOP.mjs +443 -0
- package/dist/cli-agent-BNwY5lZE.mjs +446 -0
- package/dist/cli-agent-CbzXDSSF.mjs +446 -0
- package/dist/cli-agent-DsqbS3n7.mjs +443 -0
- package/dist/cli-agent-Vbs-ePHv.mjs +443 -0
- package/dist/cli-agent-ZAkixf4c.mjs +446 -0
- package/dist/cli.mjs +954 -36
- package/dist/client/assets/{angular-html-BABKYppC.js → angular-html-DsssUIMZ.js} +1 -1
- package/dist/client/assets/{angular-ts-CtXLuOQb.js → angular-ts-dTg-sJ77.js} +1 -1
- package/dist/client/assets/{apl-DOi69evI.js → apl-C1yUKbqz.js} +1 -1
- package/dist/client/assets/{astro-J8Em2tfW.js → astro-BI07ha2y.js} +1 -1
- package/dist/client/assets/{blade-B_tOnqRD.js → blade-DkzUGffS.js} +1 -1
- package/dist/client/assets/{c-C0tRZWSi.js → c-KkuqqOvW.js} +1 -1
- package/dist/client/assets/{cobol-Dj_rkgIY.js → cobol-DRh7-jLi.js} +1 -1
- package/dist/client/assets/{coffee-C6Vq4BFF.js → coffee-wR_ZAh9a.js} +1 -1
- package/dist/client/assets/{cpp-Cno48fgF.js → cpp-OD_qyywn.js} +1 -1
- package/dist/client/assets/{crystal-C7tmDnU1.js → crystal-E4ahAddE.js} +1 -1
- package/dist/client/assets/{css-0v4o-iNQ.js → css-2NgE_uxz.js} +1 -1
- package/dist/client/assets/{edge-CvULxRem.js → edge-8mbscxxD.js} +1 -1
- package/dist/client/assets/{elixir-vRTauEw-.js → elixir-CJv1b3Q6.js} +1 -1
- package/dist/client/assets/{elm-fLFTu1xn.js → elm-DUPzqZ9H.js} +1 -1
- package/dist/client/assets/{erb-CtItcikB.js → erb-ssvEF26z.js} +1 -1
- package/dist/client/assets/{git-rebase-5MeRBXZD.js → git-rebase-DiWIK1Oa.js} +1 -1
- package/dist/client/assets/{glimmer-js-B7yA-u8_.js → glimmer-js-DrHg6AuA.js} +1 -1
- package/dist/client/assets/{glimmer-ts-B2g1OaHc.js → glimmer-ts-wQSGOa8A.js} +1 -1
- package/dist/client/assets/{glsl-CqNPWp5Y.js → glsl-OY27QGY0.js} +1 -1
- package/dist/client/assets/{graphql-CF78KbfF.js → graphql-CSWSqe6f.js} +1 -1
- package/dist/client/assets/{hack-Dz8fQ8O9.js → hack-Bi1v624Q.js} +1 -1
- package/dist/client/assets/{haml-3Gbl257r.js → haml-CX94zV9m.js} +1 -1
- package/dist/client/assets/{handlebars-5UZnh5L7.js → handlebars-C1j6wnjG.js} +1 -1
- package/dist/client/assets/{html-JOaQiIkL.js → html-BcAjMlB7.js} +1 -1
- package/dist/client/assets/{html-derivative-GyX6oYy3.js → html-derivative-DcEpOzdx.js} +1 -1
- package/dist/client/assets/{http-t6uVAxgS.js → http-BBxxSc6S.js} +1 -1
- package/dist/client/assets/{hurl-OLQZpg0E.js → hurl-CpKRNTz4.js} +1 -1
- package/dist/client/assets/{index-CghVgrAF.js → index-Bwv7BWUp.js} +181 -78
- package/dist/client/assets/index-CoZyJYkw.css +1 -0
- package/dist/client/assets/{java-BqL_yaya.js → java-CFUlLvAA.js} +1 -1
- package/dist/client/assets/{javascript-CxBNAxR5.js → javascript-C-HnT3sE.js} +1 -1
- package/dist/client/assets/{jinja-CuaK033Z.js → jinja-CPU8RwNP.js} +1 -1
- package/dist/client/assets/{jison-aL_w2vvo.js → jison-CW0TBQOs.js} +1 -1
- package/dist/client/assets/{json-Byruj-EC.js → json-qUjlNuEJ.js} +1 -1
- package/dist/client/assets/{jsx-CosY06uG.js → jsx-KWeo60kf.js} +1 -1
- package/dist/client/assets/{julia-u1y7fV63.js → julia-CnJB8y2e.js} +1 -1
- package/dist/client/assets/{just-DS4T9P_K.js → just-Do22d9PP.js} +1 -1
- package/dist/client/assets/{latex-C9c2IWW4.js → latex-CCPgzvKn.js} +1 -1
- package/dist/client/assets/{liquid-DcXQgkEV.js → liquid-B9btdZoI.js} +1 -1
- package/dist/client/assets/{lua-DWe0hUn1.js → lua-IFRifurW.js} +1 -1
- package/dist/client/assets/{marko-CIW7o2up.js → marko-CCaVBFq1.js} +1 -1
- package/dist/client/assets/{mdc-DKjGzb4k.js → mdc-BtbnOLUc.js} +1 -1
- package/dist/client/assets/{nginx-CBXzDlkk.js → nginx-BkbFZHVt.js} +1 -1
- package/dist/client/assets/{nim-CBf_vdee.js → nim-2uCd5j65.js} +1 -1
- package/dist/client/assets/{perl-xC9qc39I.js → perl-C4ixqSNT.js} +1 -1
- package/dist/client/assets/{php-n4vnn5oz.js → php-DJ8WOtcI.js} +1 -1
- package/dist/client/assets/{pug-Db536lA_.js → pug-BOaafhT5.js} +1 -1
- package/dist/client/assets/{qml-CrATLnNN.js → qml-CURE-1y4.js} +1 -1
- package/dist/client/assets/{r-DBQ-dydl.js → r-CPWmgzo3.js} +1 -1
- package/dist/client/assets/{razor-C_wvK1iP.js → razor-DH83VP_d.js} +1 -1
- package/dist/client/assets/{regexp-DowFj0Xd.js → regexp-BXVWi-t_.js} +1 -1
- package/dist/client/assets/{rst-rY4UPiwe.js → rst-Bbxxs6E2.js} +1 -1
- package/dist/client/assets/{ruby-C4cISY4u.js → ruby-C-TuWES4.js} +1 -1
- package/dist/client/assets/{sas-ui1PNNkD.js → sas-njzNYNOq.js} +1 -1
- package/dist/client/assets/{scss-BZrs95TC.js → scss-D2i6rHB8.js} +1 -1
- package/dist/client/assets/{shellscript-D9YVFS67.js → shellscript-D6Bxi40C.js} +1 -1
- package/dist/client/assets/{shellsession-DK7LMzYg.js → shellsession-Defxq-I6.js} +1 -1
- package/dist/client/assets/{soy-CNkc-Oji.js → soy-D0GKXrtT.js} +1 -1
- package/dist/client/assets/{sql-BvRdqBMa.js → sql-BRW_ADkU.js} +1 -1
- package/dist/client/assets/{stata-Bawjgtiz.js → stata-w7w59F48.js} +1 -1
- package/dist/client/assets/{surrealql-7v7lT5jp.js → surrealql-O-lLuqz7.js} +1 -1
- package/dist/client/assets/{svelte-p7w_kluv.js → svelte-DN3DR5-t.js} +1 -1
- package/dist/client/assets/{templ-0Gi_yZZr.js → templ-CJ2JFoia.js} +1 -1
- package/dist/client/assets/{tex-nZ_J3lks.js → tex-CpWrf5g9.js} +1 -1
- package/dist/client/assets/{ts-tags-CuG-Aj5j.js → ts-tags-CZ6Af6Pe.js} +1 -1
- package/dist/client/assets/{tsx-Cc7GNb_V.js → tsx-gIUSpRih.js} +1 -1
- package/dist/client/assets/{twig-BBlmihVM.js → twig-DwZINhmw.js} +1 -1
- package/dist/client/assets/{typescript-aiXwsZhm.js → typescript-Dj2aMECR.js} +1 -1
- package/dist/client/assets/{vue-Cs5tk2kn.js → vue-B5K0aWrP.js} +1 -1
- package/dist/client/assets/{vue-html-BEI_B8Ml.js → vue-html-DKGgrypd.js} +1 -1
- package/dist/client/assets/{vue-vine-CpWdOkP4.js → vue-vine-B5iDYTCh.js} +1 -1
- package/dist/client/assets/{xml-madLQSkS.js → xml-CbTzxfc-.js} +1 -1
- package/dist/client/assets/{xsl-CqZR37V9.js → xsl-Dj-Be6nx.js} +1 -1
- package/dist/client/assets/{yaml-DHr9ndbK.js → yaml-rTK8uEjh.js} +1 -1
- package/dist/client/index.html +2 -5
- package/dist/mcp-CE77qfjx.mjs +254 -0
- package/dist/mcp-CFeE13n-.mjs +254 -0
- package/dist/mcp-DKlThflu.mjs +253 -0
- package/dist/mcp-SGCrfYGo.mjs +254 -0
- package/dist/server-lock-B4crz9-_.mjs +708 -0
- package/dist/server-lock-CAdqFSfl.mjs +691 -0
- package/dist/server-lock-DpCrvL-Z.mjs +714 -0
- package/dist/update-check-DJuFE9w6.mjs +128 -0
- package/dist/update-check-DSC02MWQ.mjs +129 -0
- package/dist/update-check-KL-2sGxE.mjs +129 -0
- package/package.json +12 -14
- package/dist/client/assets/index-CmfB2R-C.css +0 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# diffing
|
|
2
2
|
|
|
3
|
-
A local-first code review tool and double-sided bridge designed for the modern AI coding agent workflow. Review AI-generated changes in a high-fidelity, GitHub-like web UI, leave inline comments, and hand them back to your coding agent to fix in real time.
|
|
3
|
+
A local-first code review tool and double-sided bridge designed for the modern AI coding agent workflow. Review AI-generated changes in a high-fidelity, GitHub-like web UI, leave inline comments, and hand them back to your coding agent to fix in real time — and review the agent's **plan** the same way *before* it writes any code, approving, rejecting, or requesting changes on specific lines and sections.
|
|
4
4
|
|
|
5
|
-
<img width="
|
|
5
|
+
<img width="1667" height="1087" alt="image" src="https://github.com/user-attachments/assets/52dcd2af-fc85-44ba-a679-4609e667c6f4" />
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -22,6 +22,12 @@ diffing
|
|
|
22
22
|
```
|
|
23
23
|
This instantly spins up a local server, establishes an active repository watcher, and opens your default browser to an interactive code review dashboard.
|
|
24
24
|
|
|
25
|
+
### 3. Update
|
|
26
|
+
Check if a new version is available on npm and upgrade instantly via the CLI:
|
|
27
|
+
```bash
|
|
28
|
+
diffing update
|
|
29
|
+
```
|
|
30
|
+
|
|
25
31
|
---
|
|
26
32
|
|
|
27
33
|
## Web UI Review Dashboard
|
|
@@ -33,7 +39,9 @@ A local Hono-powered review server delivers a full-featured GitHub-like code rev
|
|
|
33
39
|
- **Interactive File Tree** — Hierarchical file navigation sidebar with collapsible folders, viewed/unviewed tracking, and change-type indicators (added, modified, deleted).
|
|
34
40
|
- **Status Dashboard (Comment Tracker)** — Bottom panel tracking open, replied, and resolved comments with filter tabs and click-to-navigate references to the relevant file and line.
|
|
35
41
|
- **Git Diff Stats** — Toolbar displays repo name, branch, file count, and additions/deletions (`+X/-Y`) computed from the patch.
|
|
36
|
-
- **
|
|
42
|
+
- **Server-Side State & Drafts Persistence** — No browser storage is used. All settings, UI panels sizing, session states, and comment drafts are securely stored and persisted server-side in the project's global `.diffing` directory (and global settings inside `~/.config/diffing/settings.json`).
|
|
43
|
+
- **Dynamic Font Customization** — Dynamically load, cache, and apply any Google Fonts or local system fonts for both UI typography and monospace code rendering, easily configurable via the settings font picker.
|
|
44
|
+
- **Resizable Panels** — Drag-to-resize sidebar (240px–640px) and comment tracker panel (100px–600px). Panel states are instantly saved server-side for absolute consistency across browser sessions.
|
|
37
45
|
- **Skeleton Loading Screen** — Full shimmer placeholder UI for toolbar, sidebar, search, tree nodes, and file diffs during initial load.
|
|
38
46
|
- **Image Diff Previews** — Visual side-by-side comparison for added, changed, and deleted image files (PNG, JPEG, GIF, WebP, SVG, BMP, ICO, AVIF).
|
|
39
47
|
|
|
@@ -61,7 +69,8 @@ A local Hono-powered review server delivers a full-featured GitHub-like code rev
|
|
|
61
69
|
- **Instant Switching** — CSS transitions suppressed during theme changes for a snappy, lag-free experience.
|
|
62
70
|
- **Persistent Setting** — Theme choice saved to `~/.config/diffing/settings.json` and restored on next launch.
|
|
63
71
|
|
|
64
|
-
<img width="
|
|
72
|
+
<img width="1667" height="1087" alt="image" src="https://github.com/user-attachments/assets/6e4a4552-fef2-4002-8b8f-b544c95c4c26" />
|
|
73
|
+
|
|
65
74
|
|
|
66
75
|
|
|
67
76
|
---
|
|
@@ -122,6 +131,15 @@ A vim-style status bar at the bottom displays the current mode (NORMAL/INSERT),
|
|
|
122
131
|
<img width="1840" height="1196" alt="image" src="https://github.com/user-attachments/assets/d230d020-4fb2-475a-a8a0-0ae11bb271ff" />
|
|
123
132
|
|
|
124
133
|
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Console Startup Animations & Quotes
|
|
137
|
+
|
|
138
|
+
Every time you launch `diffing` in the terminal, it serves a highly polished, interactive greeting before the browser opens:
|
|
139
|
+
- **256-Color Monochromatic Palettes** — Beautifully rendered box outlines across 6 monochromatic themes (Cyan, Green, Magenta, Yellow, Blue, Orange) with custom faint, base, glow, and text hues.
|
|
140
|
+
- **Dynamic Startup Animations** — Instantly runs one of 6 terminal-based micro-animations (Typewriter, Wave Reveal, Slide-In, Pulse Border, Glitch Noise, Matrix Rain) underneath the local server URL.
|
|
141
|
+
- **Motivational Developer Quotes** — Settles to display one of 30 curated, funny, philosophical, or motivational developer quotes to kick off your review session.
|
|
142
|
+
|
|
125
143
|
---
|
|
126
144
|
|
|
127
145
|
## Performance & Speed
|
|
@@ -189,6 +207,14 @@ diffing comments [--open] [--json] # One-shot query of the comments database
|
|
|
189
207
|
diffing reply <id> --body "..." # Post an agent response or explanation
|
|
190
208
|
diffing resolve <id> # Mark a comment resolved, updating the UI live
|
|
191
209
|
diffing url # Retrieve the active server base URL
|
|
210
|
+
|
|
211
|
+
# Plan review (review a markdown plan before any code is written)
|
|
212
|
+
diffing plan submit <file> [--title T] [--model M] [--id <id>] [--wait] # Submit/resubmit a plan; prints its id
|
|
213
|
+
diffing plan await [--timeout N] # Block until the human approves/rejects/requests-changes; outputs the verdict XML
|
|
214
|
+
diffing plan list [--json] # List submitted plans with their verdicts
|
|
215
|
+
diffing plan show [<id>] [--json] # Show one plan as <plan-review> XML (latest if omitted)
|
|
216
|
+
diffing plan reply <commentId> --body "..." # Reply to an inline plan comment
|
|
217
|
+
diffing plan resolve <commentId> # Mark a plan comment resolved
|
|
192
218
|
```
|
|
193
219
|
|
|
194
220
|
### B. Model Context Protocol (MCP) Server
|
|
@@ -204,17 +230,18 @@ If your agent supports MCP (such as Cursor, Claude Desktop, or Gemini), configur
|
|
|
204
230
|
}
|
|
205
231
|
}
|
|
206
232
|
```
|
|
207
|
-
Exposes
|
|
233
|
+
Exposes tools directly to your agent for both review flows — diff review: `await_review`, `list_comments`, `reply_to_comment`, `resolve_comment`; and plan review: `submit_plan`, `await_plan_review`, `list_plans`, `get_plan`, `reply_to_plan_comment`, `resolve_plan_comment`.
|
|
208
234
|
|
|
209
235
|
### C. Agent Skills
|
|
210
236
|
You can install diffing skills directly into your AI coding assistant:
|
|
211
237
|
```bash
|
|
212
238
|
npx skills add ahmedragab20/diffing
|
|
213
239
|
```
|
|
214
|
-
Provides
|
|
240
|
+
Provides commands to coordinate reviews:
|
|
215
241
|
1. **`/diffing-start-review`** — Launches the review server.
|
|
216
242
|
2. **`/diffing-finish-review`** — Blocks the agent using `await-review` until comments are sent, then applies requested edits.
|
|
217
243
|
3. **`/diffing-review`** — Combined launch-and-wait flow.
|
|
244
|
+
4. **`/diffing-plan-review`** — Submit a markdown plan, block until the human approves/rejects/requests changes, then proceed or revise.
|
|
218
245
|
|
|
219
246
|
### Send Review Popover
|
|
220
247
|
A GitHub-style "finish your review" popover with inline editing of each comment, an optional general/overall comment, and a visual indicator when an agent is waiting. The **"Copy comments"** toolbar button serializes all comments to the XML spec and copies them to the clipboard.
|
|
@@ -225,6 +252,71 @@ A per-repo lockfile (`server.json`) in `~/.diffing/<repo-hash>/` enables all sub
|
|
|
225
252
|
### Monotonic Round Sequencing
|
|
226
253
|
A `ReviewSession` class with a monotonic `round` counter and race-guard logic ensures that if a "Send to agent" lands between polling intervals, the cached payload is delivered immediately. Multiple agents can block on the same review session simultaneously—all are released together on send.
|
|
227
254
|
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Plan Review
|
|
258
|
+
|
|
259
|
+
Review **any agent plan** — not just code. When an AI agent produces a plan
|
|
260
|
+
(an implementation outline, a design proposal, a migration strategy), `diffing`
|
|
261
|
+
renders the markdown line-by-line so you can comment on specific lines or
|
|
262
|
+
sections and **approve**, **request changes**, or **reject** it — then hands the
|
|
263
|
+
structured verdict back to the waiting agent. It's the "agent waits, human
|
|
264
|
+
releases" handoff, applied *before* any code is written.
|
|
265
|
+
|
|
266
|
+
```text
|
|
267
|
+
1. The agent submits a markdown plan and blocks (diffing plan submit … --wait).
|
|
268
|
+
2. You open the "Plans" tab, read the plan, and comment on lines/sections.
|
|
269
|
+
3. You click Approve / Request changes / Reject (with an optional overall note).
|
|
270
|
+
4. The agent wakes instantly, receives the verdict + comments as <plan-review> XML,
|
|
271
|
+
and proceeds, revises-and-resubmits, or stops accordingly.
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
- **Renders any markdown plan** — the plan body is shown via `@pierre/diffs` with
|
|
275
|
+
full syntax highlighting and line numbers, so it's addressable like a diff.
|
|
276
|
+
- **Line, range & section comments** — hover the gutter `+` or select a line range
|
|
277
|
+
to comment; each comment auto-captures its enclosing markdown heading (section)
|
|
278
|
+
and the exact anchored text for the agent.
|
|
279
|
+
- **General comments** — attach notes scoped to the whole plan.
|
|
280
|
+
- **Three-way verdict** — Approve / Request changes / Reject, GitHub-style, with
|
|
281
|
+
an optional overall comment. Resubmitting a revised plan (same id) bumps the
|
|
282
|
+
version and re-opens it for review.
|
|
283
|
+
- **Live "Plans" badge** — the diff toolbar shows a badge counting plans awaiting
|
|
284
|
+
your review, with a green dot when an agent is connected and waiting.
|
|
285
|
+
- **Same channels everywhere** — drive it from the CLI (`diffing plan …`), the
|
|
286
|
+
MCP tools (`submit_plan`, `await_plan_review`, …), or the HTTP API
|
|
287
|
+
(`POST /api/plans`, `POST /api/plans/:id/decision`, `GET /api/plan-review/await`).
|
|
288
|
+
|
|
289
|
+
### Plan Review XML Specification
|
|
290
|
+
|
|
291
|
+
When a plan verdict is handed to a waiting agent, it is serialized into a
|
|
292
|
+
self-documenting `<plan-review>` envelope:
|
|
293
|
+
|
|
294
|
+
```xml
|
|
295
|
+
<plan-review>
|
|
296
|
+
<instructions>…how to act on the verdict; how to reply/resolve/resubmit…</instructions>
|
|
297
|
+
<plan id="…" title="…" version="2" decision="changes-requested" decided-at="2026-05-29T18:52:56.053Z">
|
|
298
|
+
<decision-summary><![CDATA[The reviewer REQUESTED CHANGES. Revise the plan…]]></decision-summary>
|
|
299
|
+
<decision-comment><![CDATA[Tighten the Phase 2 scope.]]></decision-comment>
|
|
300
|
+
<plan-body><![CDATA[# My Plan
|
|
301
|
+
## Phase 1
|
|
302
|
+
…full markdown of the plan being reviewed…]]></plan-body>
|
|
303
|
+
<comments>
|
|
304
|
+
<comment id="c1" line="4" section="Phase 1" status="open" created-at="2026-05-29T18:52:29.557Z">
|
|
305
|
+
<context><![CDATA[Do the first thing]]></context>
|
|
306
|
+
<body><![CDATA[Clarify what "the first thing" is.]]></body>
|
|
307
|
+
<replies>
|
|
308
|
+
<reply id="r1" created-at="…" role="agent" model="claude-opus-4-8"><![CDATA[Will do — splitting into 1a/1b.]]></reply>
|
|
309
|
+
</replies>
|
|
310
|
+
</comment>
|
|
311
|
+
</comments>
|
|
312
|
+
</plan>
|
|
313
|
+
</plan-review>
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
<img width="1667" height="1087" alt="image" src="https://github.com/user-attachments/assets/00e884b5-4c90-4a9a-9e24-9ae57d80f3f2" />
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
|
|
228
320
|
---
|
|
229
321
|
|
|
230
322
|
## Inline Comment System
|
|
@@ -234,7 +326,7 @@ Rich, real-time comment threads directly on diff lines:
|
|
|
234
326
|
- **Inline Threads** — Hover and click the `+` button on any addition or deletion line to start a thread. Supports markdown (GFM + line breaks) with syntax-highlighted fenced code blocks.
|
|
235
327
|
- **Multi-Line Comments** — Select a line range to comment on an entire block of code.
|
|
236
328
|
- **File-Level Comments** — Add general comments scoped to the entire file without targeting a specific line.
|
|
237
|
-
- **Comment Drafts** —
|
|
329
|
+
- **Comment Drafts** — Draft system (`diffing-draft-*` keys) with 7-day TTL, stored in the global `.diffing` folder so drafts survive page refreshes without any browser storage footprint.
|
|
238
330
|
- **Agent Attribution** — Replies carry `role` (`user`/`agent`) and the agent's `model` name for clear attribution.
|
|
239
331
|
- **Suggestion Application** — Parse `` ```suggestion `` code blocks from comment bodies and apply them to the file in one click via `POST /api/comments/:id/apply-suggestion`.
|
|
240
332
|
- **Full CRUD API** — REST endpoints for creating, reading, updating, and deleting comments and replies.
|
|
@@ -254,6 +346,8 @@ Fine-grained control over how diffs are rendered:
|
|
|
254
346
|
| **Hunk Separators** | `simple`, `metadata`, `line-info`, `line-info-basic` | Style of the separator bar between diff hunks |
|
|
255
347
|
| **Line Hover Highlight** | `both`, `line`, `number`, `disabled` | Which element highlights on hover |
|
|
256
348
|
| **Font Size** | 11px – 16px | Configure globally |
|
|
349
|
+
| **UI Font** | Any system or Google font (default Geist Mono) | Customize typography for the Web UI layout |
|
|
350
|
+
| **Mono Font** | Any system or Google font (default JetBrains Mono) | Customize typography for code/diff block displays |
|
|
257
351
|
| **Tab Size** | 2 / 4 / 8 | Default tab width (overridden per-file by EditorConfig) |
|
|
258
352
|
| **Expandable Context** | `expandContextByDefault`, `collapsedContextThreshold` (default 10 lines), `expansionLineCount` (default 20) | Control how collapsed context regions behave |
|
|
259
353
|
| **Haptic & Sound Feedback** | on / off | Tactile feedback via `web-haptics` and synthesized audio cues (click, toggle, navigate, open, close, resolve, send, error) |
|
|
@@ -408,6 +502,8 @@ For advanced features, internal API endpoints, sequence specifications, and conf
|
|
|
408
502
|
|
|
409
503
|
---
|
|
410
504
|
|
|
505
|
+
👉🏻 The project was intially forked from **wong2/diffx**
|
|
506
|
+
|
|
411
507
|
## License
|
|
412
508
|
|
|
413
509
|
MIT
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { c as formatComments, n as readServerLock, o as formatPlanReview, t as isLockAlive } from "./server-lock-B4crz9-_.mjs";
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
//#region src/cli-agent.ts
|
|
5
|
+
/**
|
|
6
|
+
* Agent-facing `diffing` subcommands. These make the user→agent handoff
|
|
7
|
+
* port-agnostic: each resolves the running server via the per-repo lockfile
|
|
8
|
+
* (`server.json`) so any agent with a shell — or a human — can drive the loop
|
|
9
|
+
* without being told a port.
|
|
10
|
+
*
|
|
11
|
+
* diffing await-review block until the human clicks "Send to agent"
|
|
12
|
+
* diffing reply <id> post an agent reply to a comment
|
|
13
|
+
* diffing resolve <id> mark a comment resolved
|
|
14
|
+
* diffing comments dump the current comments (XML or JSON)
|
|
15
|
+
*/
|
|
16
|
+
const EXIT_OK = 0;
|
|
17
|
+
const EXIT_AWAIT_TIMEOUT = 2;
|
|
18
|
+
const EXIT_NO_SERVER = 3;
|
|
19
|
+
const EXIT_NOT_FOUND = 4;
|
|
20
|
+
const EXIT_USAGE = 5;
|
|
21
|
+
/** Resolve the running server's base URL from the lockfile, or exit cleanly. */
|
|
22
|
+
function baseUrl() {
|
|
23
|
+
const lock = readServerLock();
|
|
24
|
+
if (!lock || !isLockAlive(lock)) {
|
|
25
|
+
console.error("No diffing server running for this repo. Start one with `diffing`.");
|
|
26
|
+
process.exit(EXIT_NO_SERVER);
|
|
27
|
+
}
|
|
28
|
+
return `http://${lock.host === "0.0.0.0" || lock.host === "::" ? "127.0.0.1" : lock.host}:${lock.port}`;
|
|
29
|
+
}
|
|
30
|
+
async function readStdin() {
|
|
31
|
+
const chunks = [];
|
|
32
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
33
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
34
|
+
}
|
|
35
|
+
async function awaitReview(args) {
|
|
36
|
+
const { values } = parseArgs({
|
|
37
|
+
args,
|
|
38
|
+
options: {
|
|
39
|
+
timeout: {
|
|
40
|
+
type: "string",
|
|
41
|
+
short: "t"
|
|
42
|
+
},
|
|
43
|
+
since: { type: "string" }
|
|
44
|
+
},
|
|
45
|
+
allowPositionals: false
|
|
46
|
+
});
|
|
47
|
+
const totalBudgetMs = (values.timeout ? Number(values.timeout) : 570) * 1e3;
|
|
48
|
+
const base = baseUrl();
|
|
49
|
+
let sinceRound = 0;
|
|
50
|
+
try {
|
|
51
|
+
sinceRound = (await fetch(`${base}/api/review/status`).then((r) => r.json())).round ?? 0;
|
|
52
|
+
} catch {}
|
|
53
|
+
const deadline = Date.now() + totalBudgetMs;
|
|
54
|
+
while (Date.now() < deadline) {
|
|
55
|
+
let res;
|
|
56
|
+
try {
|
|
57
|
+
res = await fetch(`${base}/api/review/await?timeoutMs=25000&sinceRound=${sinceRound}`, { signal: AbortSignal.timeout(3e4) });
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (err?.name === "TimeoutError") continue;
|
|
60
|
+
console.error(`Failed to reach diffing server: ${err?.message ?? err}`);
|
|
61
|
+
return EXIT_NO_SERVER;
|
|
62
|
+
}
|
|
63
|
+
const result = await res.json();
|
|
64
|
+
if (result.status === "released") {
|
|
65
|
+
process.stdout.write(result.payload.commentXml + "\n");
|
|
66
|
+
console.error(`DIFFING_REVIEW_ROUND=${result.payload.round}`);
|
|
67
|
+
return EXIT_OK;
|
|
68
|
+
}
|
|
69
|
+
sinceRound = result.round ?? sinceRound;
|
|
70
|
+
}
|
|
71
|
+
console.error("DIFFING_AWAIT_TIMEOUT");
|
|
72
|
+
console.error("No review sent within the timeout. Run `diffing await-review` again to keep waiting.");
|
|
73
|
+
return EXIT_AWAIT_TIMEOUT;
|
|
74
|
+
}
|
|
75
|
+
async function reply(args) {
|
|
76
|
+
const { values, positionals } = parseArgs({
|
|
77
|
+
args,
|
|
78
|
+
options: {
|
|
79
|
+
body: {
|
|
80
|
+
type: "string",
|
|
81
|
+
short: "b"
|
|
82
|
+
},
|
|
83
|
+
model: {
|
|
84
|
+
type: "string",
|
|
85
|
+
short: "m"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
allowPositionals: true
|
|
89
|
+
});
|
|
90
|
+
const commentId = positionals[0];
|
|
91
|
+
if (!commentId) {
|
|
92
|
+
console.error("Usage: diffing reply <commentId> --body <text> [--model <name>]");
|
|
93
|
+
return EXIT_USAGE;
|
|
94
|
+
}
|
|
95
|
+
let body = values.body;
|
|
96
|
+
if (body === "-" || body === void 0) body = (await readStdin()).trim();
|
|
97
|
+
if (!body) {
|
|
98
|
+
console.error("A reply body is required (--body <text> or pipe via stdin).");
|
|
99
|
+
return EXIT_USAGE;
|
|
100
|
+
}
|
|
101
|
+
const res = await fetch(`${baseUrl()}/api/comments/${commentId}/replies`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: { "Content-Type": "application/json" },
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
body,
|
|
106
|
+
role: "agent",
|
|
107
|
+
model: values.model
|
|
108
|
+
})
|
|
109
|
+
});
|
|
110
|
+
if (res.status === 404) {
|
|
111
|
+
console.error(`Comment ${commentId} not found.`);
|
|
112
|
+
return EXIT_NOT_FOUND;
|
|
113
|
+
}
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
console.error(`Failed to reply: HTTP ${res.status}`);
|
|
116
|
+
return 1;
|
|
117
|
+
}
|
|
118
|
+
console.error(`Replied to ${commentId}.`);
|
|
119
|
+
return EXIT_OK;
|
|
120
|
+
}
|
|
121
|
+
async function resolve(args) {
|
|
122
|
+
const { positionals } = parseArgs({
|
|
123
|
+
args,
|
|
124
|
+
allowPositionals: true,
|
|
125
|
+
options: {}
|
|
126
|
+
});
|
|
127
|
+
const commentId = positionals[0];
|
|
128
|
+
if (!commentId) {
|
|
129
|
+
console.error("Usage: diffing resolve <commentId>");
|
|
130
|
+
return EXIT_USAGE;
|
|
131
|
+
}
|
|
132
|
+
const res = await fetch(`${baseUrl()}/api/comments/${commentId}`, {
|
|
133
|
+
method: "PUT",
|
|
134
|
+
headers: { "Content-Type": "application/json" },
|
|
135
|
+
body: JSON.stringify({ status: "resolved" })
|
|
136
|
+
});
|
|
137
|
+
if (res.status === 404) {
|
|
138
|
+
console.error(`Comment ${commentId} not found.`);
|
|
139
|
+
return EXIT_NOT_FOUND;
|
|
140
|
+
}
|
|
141
|
+
if (!res.ok) {
|
|
142
|
+
console.error(`Failed to resolve: HTTP ${res.status}`);
|
|
143
|
+
return 1;
|
|
144
|
+
}
|
|
145
|
+
console.error(`Resolved ${commentId}.`);
|
|
146
|
+
return EXIT_OK;
|
|
147
|
+
}
|
|
148
|
+
async function url() {
|
|
149
|
+
process.stdout.write(baseUrl() + "\n");
|
|
150
|
+
return EXIT_OK;
|
|
151
|
+
}
|
|
152
|
+
async function comments(args) {
|
|
153
|
+
const { values } = parseArgs({
|
|
154
|
+
args,
|
|
155
|
+
options: {
|
|
156
|
+
open: { type: "boolean" },
|
|
157
|
+
json: { type: "boolean" }
|
|
158
|
+
},
|
|
159
|
+
allowPositionals: false
|
|
160
|
+
});
|
|
161
|
+
const all = await fetch(`${baseUrl()}/api/comments`).then((r) => r.json());
|
|
162
|
+
const selected = values.open ? all.filter((c) => c.status === "open") : all;
|
|
163
|
+
if (values.json) process.stdout.write(JSON.stringify(selected, null, 2) + "\n");
|
|
164
|
+
else process.stdout.write(formatComments(selected) + "\n");
|
|
165
|
+
return EXIT_OK;
|
|
166
|
+
}
|
|
167
|
+
/** Derive a human title from a plan's first heading or non-empty line. */
|
|
168
|
+
function deriveTitle(body) {
|
|
169
|
+
for (const raw of body.split("\n")) {
|
|
170
|
+
const line = raw.trim();
|
|
171
|
+
if (!line) continue;
|
|
172
|
+
const heading = /^#{1,6}\s+(.*)$/.exec(line);
|
|
173
|
+
return (heading ? heading[1] : line).slice(0, 120);
|
|
174
|
+
}
|
|
175
|
+
return "Untitled plan";
|
|
176
|
+
}
|
|
177
|
+
/** Long-poll the plan-review handoff until a decision arrives or time runs out. */
|
|
178
|
+
async function pollPlanDecision(base, totalBudgetMs, seedSince) {
|
|
179
|
+
let sinceRound = seedSince ?? 0;
|
|
180
|
+
if (seedSince === void 0) try {
|
|
181
|
+
sinceRound = (await fetch(`${base}/api/plan-review/status`).then((r) => r.json())).round ?? 0;
|
|
182
|
+
} catch {}
|
|
183
|
+
const deadline = Date.now() + totalBudgetMs;
|
|
184
|
+
while (Date.now() < deadline) {
|
|
185
|
+
let res;
|
|
186
|
+
try {
|
|
187
|
+
res = await fetch(`${base}/api/plan-review/await?timeoutMs=25000&sinceRound=${sinceRound}`, { signal: AbortSignal.timeout(3e4) });
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (err?.name === "TimeoutError") continue;
|
|
190
|
+
console.error(`Failed to reach diffing server: ${err?.message ?? err}`);
|
|
191
|
+
return EXIT_NO_SERVER;
|
|
192
|
+
}
|
|
193
|
+
const result = await res.json();
|
|
194
|
+
if (result.status === "released") {
|
|
195
|
+
process.stdout.write(result.payload.reviewXml + "\n");
|
|
196
|
+
console.error(`DIFFING_PLAN_DECISION=${result.payload.decision}`);
|
|
197
|
+
console.error(`DIFFING_PLAN_ROUND=${result.payload.round}`);
|
|
198
|
+
return EXIT_OK;
|
|
199
|
+
}
|
|
200
|
+
sinceRound = result.round ?? sinceRound;
|
|
201
|
+
}
|
|
202
|
+
console.error("DIFFING_PLAN_AWAIT_TIMEOUT");
|
|
203
|
+
console.error("No plan decision within the timeout. Run `diffing plan await` again to keep waiting.");
|
|
204
|
+
return EXIT_AWAIT_TIMEOUT;
|
|
205
|
+
}
|
|
206
|
+
async function planSubmit(args) {
|
|
207
|
+
const { values, positionals } = parseArgs({
|
|
208
|
+
args,
|
|
209
|
+
options: {
|
|
210
|
+
title: { type: "string" },
|
|
211
|
+
source: {
|
|
212
|
+
type: "string",
|
|
213
|
+
short: "s"
|
|
214
|
+
},
|
|
215
|
+
model: {
|
|
216
|
+
type: "string",
|
|
217
|
+
short: "m"
|
|
218
|
+
},
|
|
219
|
+
id: { type: "string" },
|
|
220
|
+
wait: {
|
|
221
|
+
type: "boolean",
|
|
222
|
+
short: "w"
|
|
223
|
+
},
|
|
224
|
+
timeout: {
|
|
225
|
+
type: "string",
|
|
226
|
+
short: "t"
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
allowPositionals: true
|
|
230
|
+
});
|
|
231
|
+
const file = positionals[0];
|
|
232
|
+
let body;
|
|
233
|
+
if (!file || file === "-") body = await readStdin();
|
|
234
|
+
else try {
|
|
235
|
+
body = await readFile(file, "utf-8");
|
|
236
|
+
} catch (err) {
|
|
237
|
+
console.error(`Failed to read plan file ${file}: ${err?.message ?? err}`);
|
|
238
|
+
return EXIT_USAGE;
|
|
239
|
+
}
|
|
240
|
+
body = body.replace(/\r\n/g, "\n");
|
|
241
|
+
if (!body.trim()) {
|
|
242
|
+
console.error("A plan body is required (pass a markdown file path or pipe via stdin).");
|
|
243
|
+
return EXIT_USAGE;
|
|
244
|
+
}
|
|
245
|
+
const title = values.title || deriveTitle(body);
|
|
246
|
+
const base = baseUrl();
|
|
247
|
+
let sinceRound = 0;
|
|
248
|
+
if (values.wait) try {
|
|
249
|
+
sinceRound = (await fetch(`${base}/api/plan-review/status`).then((r) => r.json())).round ?? 0;
|
|
250
|
+
} catch {}
|
|
251
|
+
const res = await fetch(`${base}/api/plans`, {
|
|
252
|
+
method: "POST",
|
|
253
|
+
headers: { "Content-Type": "application/json" },
|
|
254
|
+
body: JSON.stringify({
|
|
255
|
+
id: values.id,
|
|
256
|
+
title,
|
|
257
|
+
body,
|
|
258
|
+
source: values.source,
|
|
259
|
+
model: values.model
|
|
260
|
+
})
|
|
261
|
+
});
|
|
262
|
+
if (!res.ok) {
|
|
263
|
+
console.error(`Failed to submit plan: HTTP ${res.status}`);
|
|
264
|
+
return 1;
|
|
265
|
+
}
|
|
266
|
+
const plan = await res.json();
|
|
267
|
+
console.error(`Submitted plan ${plan.id} (v${plan.version}) — review at ${base}/plan/${plan.id}`);
|
|
268
|
+
if (!values.wait) {
|
|
269
|
+
process.stdout.write(plan.id + "\n");
|
|
270
|
+
return EXIT_OK;
|
|
271
|
+
}
|
|
272
|
+
return pollPlanDecision(base, (values.timeout ? Number(values.timeout) : 570) * 1e3, sinceRound);
|
|
273
|
+
}
|
|
274
|
+
async function planAwait(args) {
|
|
275
|
+
const { values } = parseArgs({
|
|
276
|
+
args,
|
|
277
|
+
options: { timeout: {
|
|
278
|
+
type: "string",
|
|
279
|
+
short: "t"
|
|
280
|
+
} },
|
|
281
|
+
allowPositionals: false
|
|
282
|
+
});
|
|
283
|
+
const totalBudgetMs = (values.timeout ? Number(values.timeout) : 570) * 1e3;
|
|
284
|
+
return pollPlanDecision(baseUrl(), totalBudgetMs);
|
|
285
|
+
}
|
|
286
|
+
async function planList(args) {
|
|
287
|
+
const { values } = parseArgs({
|
|
288
|
+
args,
|
|
289
|
+
options: { json: { type: "boolean" } },
|
|
290
|
+
allowPositionals: false
|
|
291
|
+
});
|
|
292
|
+
const all = await fetch(`${baseUrl()}/api/plans`).then((r) => r.json());
|
|
293
|
+
if (values.json) {
|
|
294
|
+
process.stdout.write(JSON.stringify(all, null, 2) + "\n");
|
|
295
|
+
return EXIT_OK;
|
|
296
|
+
}
|
|
297
|
+
if (all.length === 0) {
|
|
298
|
+
console.error("No plans submitted yet.");
|
|
299
|
+
return EXIT_OK;
|
|
300
|
+
}
|
|
301
|
+
for (const p of all) {
|
|
302
|
+
const open = (p.comments ?? []).filter((c) => c.status === "open").length;
|
|
303
|
+
process.stdout.write(`${p.id}\t[${p.decision}]\tv${p.version}\t${open} open comment(s)\t${p.title}\n`);
|
|
304
|
+
}
|
|
305
|
+
return EXIT_OK;
|
|
306
|
+
}
|
|
307
|
+
async function planShow(args) {
|
|
308
|
+
const { values, positionals } = parseArgs({
|
|
309
|
+
args,
|
|
310
|
+
options: { json: { type: "boolean" } },
|
|
311
|
+
allowPositionals: true
|
|
312
|
+
});
|
|
313
|
+
const base = baseUrl();
|
|
314
|
+
let planId = positionals[0];
|
|
315
|
+
if (!planId) {
|
|
316
|
+
const all = await fetch(`${base}/api/plans`).then((r) => r.json());
|
|
317
|
+
if (all.length === 0) {
|
|
318
|
+
console.error("No plans submitted yet.");
|
|
319
|
+
return EXIT_NOT_FOUND;
|
|
320
|
+
}
|
|
321
|
+
planId = all[all.length - 1].id;
|
|
322
|
+
}
|
|
323
|
+
const res = await fetch(`${base}/api/plans/${planId}`);
|
|
324
|
+
if (res.status === 404) {
|
|
325
|
+
console.error(`Plan ${planId} not found.`);
|
|
326
|
+
return EXIT_NOT_FOUND;
|
|
327
|
+
}
|
|
328
|
+
const plan = await res.json();
|
|
329
|
+
if (values.json) process.stdout.write(JSON.stringify(plan, null, 2) + "\n");
|
|
330
|
+
else process.stdout.write(formatPlanReview(plan) + "\n");
|
|
331
|
+
return EXIT_OK;
|
|
332
|
+
}
|
|
333
|
+
/** Locate which plan owns a given comment id (comment ids are globally unique). */
|
|
334
|
+
async function findCommentPlan(base, commentId) {
|
|
335
|
+
return (await fetch(`${base}/api/plans`).then((r) => r.json())).find((p) => (p.comments ?? []).some((c) => c.id === commentId)) ?? null;
|
|
336
|
+
}
|
|
337
|
+
async function planReply(args) {
|
|
338
|
+
const { values, positionals } = parseArgs({
|
|
339
|
+
args,
|
|
340
|
+
options: {
|
|
341
|
+
body: {
|
|
342
|
+
type: "string",
|
|
343
|
+
short: "b"
|
|
344
|
+
},
|
|
345
|
+
model: {
|
|
346
|
+
type: "string",
|
|
347
|
+
short: "m"
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
allowPositionals: true
|
|
351
|
+
});
|
|
352
|
+
const commentId = positionals[0];
|
|
353
|
+
if (!commentId) {
|
|
354
|
+
console.error("Usage: diffing plan reply <commentId> --body <text> [--model <name>]");
|
|
355
|
+
return EXIT_USAGE;
|
|
356
|
+
}
|
|
357
|
+
let body = values.body;
|
|
358
|
+
if (body === "-" || body === void 0) body = (await readStdin()).trim();
|
|
359
|
+
if (!body) {
|
|
360
|
+
console.error("A reply body is required (--body <text> or pipe via stdin).");
|
|
361
|
+
return EXIT_USAGE;
|
|
362
|
+
}
|
|
363
|
+
const base = baseUrl();
|
|
364
|
+
const plan = await findCommentPlan(base, commentId);
|
|
365
|
+
if (!plan) {
|
|
366
|
+
console.error(`Plan comment ${commentId} not found.`);
|
|
367
|
+
return EXIT_NOT_FOUND;
|
|
368
|
+
}
|
|
369
|
+
const res = await fetch(`${base}/api/plans/${plan.id}/comments/${commentId}/replies`, {
|
|
370
|
+
method: "POST",
|
|
371
|
+
headers: { "Content-Type": "application/json" },
|
|
372
|
+
body: JSON.stringify({
|
|
373
|
+
body,
|
|
374
|
+
role: "agent",
|
|
375
|
+
model: values.model
|
|
376
|
+
})
|
|
377
|
+
});
|
|
378
|
+
if (!res.ok) {
|
|
379
|
+
console.error(`Failed to reply: HTTP ${res.status}`);
|
|
380
|
+
return 1;
|
|
381
|
+
}
|
|
382
|
+
console.error(`Replied to plan comment ${commentId}.`);
|
|
383
|
+
return EXIT_OK;
|
|
384
|
+
}
|
|
385
|
+
async function planResolve(args) {
|
|
386
|
+
const { positionals } = parseArgs({
|
|
387
|
+
args,
|
|
388
|
+
allowPositionals: true,
|
|
389
|
+
options: {}
|
|
390
|
+
});
|
|
391
|
+
const commentId = positionals[0];
|
|
392
|
+
if (!commentId) {
|
|
393
|
+
console.error("Usage: diffing plan resolve <commentId>");
|
|
394
|
+
return EXIT_USAGE;
|
|
395
|
+
}
|
|
396
|
+
const base = baseUrl();
|
|
397
|
+
const plan = await findCommentPlan(base, commentId);
|
|
398
|
+
if (!plan) {
|
|
399
|
+
console.error(`Plan comment ${commentId} not found.`);
|
|
400
|
+
return EXIT_NOT_FOUND;
|
|
401
|
+
}
|
|
402
|
+
const res = await fetch(`${base}/api/plans/${plan.id}/comments/${commentId}`, {
|
|
403
|
+
method: "PUT",
|
|
404
|
+
headers: { "Content-Type": "application/json" },
|
|
405
|
+
body: JSON.stringify({ status: "resolved" })
|
|
406
|
+
});
|
|
407
|
+
if (!res.ok) {
|
|
408
|
+
console.error(`Failed to resolve: HTTP ${res.status}`);
|
|
409
|
+
return 1;
|
|
410
|
+
}
|
|
411
|
+
console.error(`Resolved plan comment ${commentId}.`);
|
|
412
|
+
return EXIT_OK;
|
|
413
|
+
}
|
|
414
|
+
async function plan(args) {
|
|
415
|
+
const action = args[0];
|
|
416
|
+
const rest = args.slice(1);
|
|
417
|
+
switch (action) {
|
|
418
|
+
case "submit": return planSubmit(rest);
|
|
419
|
+
case "await": return planAwait(rest);
|
|
420
|
+
case "list": return planList(rest);
|
|
421
|
+
case "show": return planShow(rest);
|
|
422
|
+
case "reply": return planReply(rest);
|
|
423
|
+
case "resolve": return planResolve(rest);
|
|
424
|
+
default:
|
|
425
|
+
console.error("Usage: diffing plan <submit|await|list|show|reply|resolve> [...]");
|
|
426
|
+
return EXIT_USAGE;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async function runSubcommand(name, args) {
|
|
430
|
+
switch (name) {
|
|
431
|
+
case "await-review": return awaitReview(args);
|
|
432
|
+
case "reply": return reply(args);
|
|
433
|
+
case "resolve": return resolve(args);
|
|
434
|
+
case "comments": return comments(args);
|
|
435
|
+
case "url": return url();
|
|
436
|
+
case "plan": return plan(args);
|
|
437
|
+
default:
|
|
438
|
+
console.error(`Unknown subcommand: ${name}`);
|
|
439
|
+
return EXIT_USAGE;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
//#endregion
|
|
443
|
+
export { runSubcommand };
|