-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.jsx
More file actions
241 lines (225 loc) · 10.5 KB
/
Copy pathindex.jsx
File metadata and controls
241 lines (225 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
#!/usr/bin/env bun
import React from "react";
import { spawnSync } from "node:child_process";
import { render } from "ink";
import { loadDiff, parseDiff, defaultSource } from "./src/git.mjs";
import { App } from "./src/App.jsx";
import { createMouseController } from "./src/mouse-select.mjs";
import { VERSION } from "./src/version.mjs";
// Everything after the script name is passed straight to `git diff`.
// bun index.jsx → all outstanding work on the branch
// (branch commits + uncommitted + untracked)
// bun index.jsx --staged → staged changes
// bun index.jsx main..feature → a branch range (PR-style)
const args = process.argv.slice(2);
// Subcommands, checked before the rest is handed to `git diff`.
if (args[0] === "update") {
const { runUpdate } = await import("./src/update.mjs");
await runUpdate();
process.exit(0);
}
if (args[0] === "--version" || args[0] === "-v" || args[0] === "version") {
console.log(VERSION);
process.exit(0);
}
if (args[0] === "prs" || args[0] === "pr") {
const { runPrManager } = await import("./src/pr-manager.jsx");
await runPrManager();
process.exit(0);
}
if (args[0] === "reset") {
const { runResetCommand } = await import("./src/reset.mjs");
process.exit(await runResetCommand(args[1]));
}
// `orbit-diff __watchdog <pid>` — internal. The sidecar process spawned by
// spawnWatchdog() to watch a viewer/prs process from outside and kill it if a
// Bun runtime bug (see src/watchdog.mjs) wedges it at 100% CPU. Never launched
// directly by a user.
if (args[0] === "__watchdog") {
const { runWatchdog } = await import("./src/watchdog.mjs");
await runWatchdog(args[1]);
process.exit(0);
}
// `orbit-diff pr-status` — runs in the review window's status pane, polling
// the worktree's branch/PR/env state on a timer. Never returns on its own;
// the pane goes away when the review window is torn down.
if (args[0] === "pr-status") {
const { runPrStatus } = await import("./src/pr-status.mjs");
await runPrStatus();
process.exit(0);
}
// `orbit-diff env-report [instance] [--instance X] [--url U] [--status S] [--error E]`
// The setup script calls this from inside its worktree once the environment is
// provisioned, so orbit-diff can record the instance (the "EV" number) against
// this worktree's review session and stop the "provisioning" spinner.
if (args[0] === "env-report") {
const { repoRoot } = await import("./src/paths.mjs");
const { sessionKey, readSession, updateSession, reviewTabLabel } = await import("./src/session.mjs");
const { labelReviewTab } = await import("./src/mux.mjs");
const rest = args.slice(1);
const patch = { status: "ready" };
let positional = null;
for (let i = 0; i < rest.length; i++) {
const a = rest[i];
if (a === "--instance") patch.envInstance = rest[++i];
else if (a === "--url") patch.envUrl = rest[++i];
else if (a === "--status") patch.status = rest[++i];
else if (a === "--error") patch.error = rest[++i];
else if (!a.startsWith("--") && positional == null) positional = a;
}
if (positional != null && patch.envInstance == null) patch.envInstance = positional;
const root = repoRoot();
const key = sessionKey(root);
// Upsert so an early/manual call still records the worktree path.
if (!readSession(key)) updateSession(key, { worktreePath: root });
const rec = updateSession(key, { ...patch, worktreePath: root });
// Put the instance on the review tab, so it's readable from every tab of the
// review rather than only from the viewer. Best-effort: silent when this
// worktree has no open review, or when we're not inside herdr at all (setup
// scripts get run by hand from a plain shell often enough).
labelReviewTab(root, reviewTabLabel(rec));
const inst = rec.envInstance != null ? ` → instance ${rec.envInstance}` : "";
console.log(`orbit-diff: recorded env for ${root}${inst} (status: ${rec.status})`);
process.exit(0);
}
if (args[0] === "init") {
const { scaffoldConfig } = await import("./src/ai/config.mjs");
const force = args.includes("--force") || args.includes("-f");
const { path, created } = scaffoldConfig({ force });
console.log(
created
? `orbit-diff: wrote starter config → ${path}`
: `orbit-diff: config already exists at ${path} (pass --force to overwrite)`,
);
process.exit(0);
}
// First-run convenience: materialise the config so there's a file to edit. This
// is best-effort — a missing/broken config already falls back to built-in
// defaults, so we never block the viewer on it.
{
const { ensureConfig } = await import("./src/ai/config.mjs");
const { created, path } = ensureConfig();
if (created) console.error(`\x1b[2morbit-diff: wrote starter config → ${path} (edit to change model/provider)\x1b[0m`);
}
let patch;
try {
patch = loadDiff(args);
} catch (err) {
console.error(`orbit-diff: ${err.message}`);
process.exit(1);
}
const files = parseDiff(patch);
if (files.length === 0) {
const what = args.length ? `git diff ${args.join(" ")}` : "outstanding branch changes";
console.error(`orbit-diff: no ${args.length ? "changes for `" + what + "`" : what}`);
process.exit(0);
}
const source = args.length ? args.join(" ") : defaultSource();
// Ask the terminal for its background so the current-line / selection bands can
// be a subtle off-shade of the actual theme (light or dark). Falls back to
// dark-tuned defaults when the terminal doesn't answer. Runs before render so
// stdin is free for the OSC 11 round-trip; Ink reclaims it afterward.
const { detectLineColors } = await import("./src/theme.mjs");
const { activeBg, selectBg, addBg, delBg } = await detectLineColors();
// Are we the diff pane of a managed review window? If this worktree has a
// review session with a live Claude pane, the viewer routes annotations there
// instead of quitting to hand off to a fresh `claude`.
let claudePane = null;
const { inMux } = await import("./src/mux.mjs");
if (inMux()) {
try {
const { repoRoot } = await import("./src/paths.mjs");
const { sessionForWorktree } = await import("./src/session.mjs");
const { paneAlive } = await import("./src/mux.mjs");
const sess = sessionForWorktree(repoRoot());
const pane = sess?.panes?.claude;
if (pane && paneAlive(pane)) claudePane = pane;
} catch {
/* best-effort — fall back to the quit-and-handoff path */
}
}
// Guard against a known Bun runtime bug that can wedge this process at 100%
// CPU (see src/watchdog.mjs) — spawned once, watches this pid for the whole
// handoff loop below, and exits on its own once we do.
const { spawnWatchdog } = await import("./src/watchdog.mjs");
spawnWatchdog();
// The review loop. render the viewer; when it exits, either the user quit (done)
// or they pressed `r` to apply their annotations, leaving a change-request doc in
// `handoff.doc`. In that case we hand the *bare* terminal to an interactive
// `claude` session so they can watch it work and answer any questions, then
// reload the diff and re-launch the viewer on the result.
// Mouse tracking should only be live while our own Ink view has the
// terminal — turned off around the `claude` handoff below so its session
// isn't fighting the same escape sequences.
const mouse = createMouseController(process.stdout);
const { stdin: lockedStdin, enable: enableScrollLock, disable: disableScrollLock } = mouse;
let current = files;
// The viewer opens on the PR overview, but only the *first* time round this loop.
// Coming back from an editor or a Claude handoff you were working in the diff, so
// that's where you should land rather than back at the front door.
let startMode = "overview";
while (true) {
const handoff = { doc: null };
enableScrollLock();
const app = render(
<App files={current} reloadDiff={() => parseDiff(loadDiff(args))} source={source} handoff={handoff} claudePane={claudePane} initialMode={startMode} activeBg={activeBg} selectBg={selectBg} addBg={addBg} delBg={delBg} mouse={mouse} />,
{ exitOnCtrlC: true, stdout: mouse.stdout, stdin: lockedStdin },
);
await app.waitUntilExit();
disableScrollLock();
// Clear the viewer's final frame so nothing bleeds into what comes next.
process.stdout.write("\x1b[2J\x1b[H");
// `e` in the viewer: open the highlighted file in the user's editor. Ink has
// released the terminal, so stdio "inherit" hands the real TTY to a terminal
// editor (vi/nano) and blocks until it exits; then we re-read the working tree
// (they may have edited the file) and re-launch the viewer on the fresh diff.
// The shell must NOT be interactive (`-i`): an interactive shell enables job
// control, moves the TTY's foreground process group to itself, and leaves it
// pointing at a dead group on exit — the relaunched viewer then dies with
// `setRawMode failed with errno: 5` instead of coming back.
if (handoff.edit) {
const shell = process.env.SHELL || "/bin/sh";
const res = spawnSync(shell, ["-c", handoff.edit.cmd], { stdio: "inherit" });
if (res.error) console.error(`\norbit-diff: couldn't launch editor: ${res.error.message}`);
let next;
try {
next = parseDiff(loadDiff(args));
} catch (err) {
console.error(`orbit-diff: reload failed: ${err.message}`);
process.exit(1);
}
if (next.length === 0) {
console.log("orbit-diff: no changes remain — nothing left to review.");
break;
}
current = next;
startMode = "normal";
continue; // back into the viewer
}
if (!handoff.doc) break; // a plain quit — we're done
// Ink has released the terminal; give it to an interactive claude session.
// stdio "inherit" hands over the real TTY, so its window shows and it can
// prompt. The prompt is seeded as the first message; a copy is also saved under
// ~/.cache/orbit-diff/<repo>/<branch>/. spawnSync blocks until the user exits claude.
console.log("\x1b[2morbit-diff → handing off to Claude Code (exit it to return)…\x1b[0m\n");
const res = spawnSync("claude", [handoff.doc], { stdio: "inherit" });
if (res.error) {
const why = res.error.code === "ENOENT" ? "`claude` not found on PATH" : res.error.message;
console.error(`\norbit-diff: couldn't launch Claude Code: ${why}`);
process.exit(1);
}
// Re-read the working tree Claude just edited.
let next;
try {
next = parseDiff(loadDiff(args));
} catch (err) {
console.error(`orbit-diff: reload failed: ${err.message}`);
process.exit(1);
}
if (next.length === 0) {
console.log("orbit-diff: no changes remain after applying — nothing left to review.");
break;
}
current = next; // loop back into the viewer on the fresh diff
startMode = "normal";
}