@sealant/mend 0.22.0 → 0.23.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.
Files changed (80) hide show
  1. package/README.md +133 -6
  2. package/dist/chunk-YPM6TYQR.js +27888 -0
  3. package/dist/dashboard-LR5LHV7O.js +2951 -0
  4. package/dist/main.js +6917 -2580
  5. package/man/mend-accounts.1 +1 -1
  6. package/man/mend-adopt.1 +4 -4
  7. package/man/mend-attach.1 +1 -1
  8. package/man/mend-codex.1 +1 -1
  9. package/man/mend-completions.1 +1 -1
  10. package/man/mend-connect.1 +1 -1
  11. package/man/mend-continue.1 +1 -1
  12. package/man/mend-doctor.1 +1 -1
  13. package/man/mend-dotfiles-sync.1 +1 -1
  14. package/man/mend-dotfiles.1 +1 -1
  15. package/man/mend-env-cluster.1 +1 -1
  16. package/man/mend-env-load.1 +1 -1
  17. package/man/mend-env-show.1 +1 -1
  18. package/man/mend-help.1 +1 -1
  19. package/man/mend-keys-init.1 +1 -1
  20. package/man/mend-keys-mode.1 +1 -1
  21. package/man/mend-keys-share.1 +1 -1
  22. package/man/mend-keys-show.1 +1 -1
  23. package/man/mend-login.1 +1 -1
  24. package/man/mend-logout.1 +1 -1
  25. package/man/mend-man.1 +1 -1
  26. package/man/mend-pair.1 +1 -1
  27. package/man/mend-projects.1 +1 -1
  28. package/man/mend-refresh.1 +1 -1
  29. package/man/mend-rejoin.1 +1 -1
  30. package/man/mend-resume.1 +1 -1
  31. package/man/mend-run.1 +1 -1
  32. package/man/mend-server-logs.1 +15 -0
  33. package/man/mend-server-restart.1 +15 -0
  34. package/man/mend-server-setup.1 +64 -0
  35. package/man/mend-server-start.1 +15 -0
  36. package/man/mend-server-status.1 +11 -0
  37. package/man/mend-server-stop.1 +11 -0
  38. package/man/mend-server-upgrade.1 +30 -0
  39. package/man/mend-server.1 +11 -0
  40. package/man/mend-service-add.1 +1 -1
  41. package/man/mend-service-connect.1 +1 -1
  42. package/man/mend-service-init.1 +1 -1
  43. package/man/mend-service-list.1 +1 -1
  44. package/man/mend-service-logs.1 +1 -1
  45. package/man/mend-service-restart.1 +1 -1
  46. package/man/mend-service-run.1 +1 -1
  47. package/man/mend-service-stop.1 +1 -1
  48. package/man/mend-sessions.1 +1 -1
  49. package/man/mend-shell.1 +1 -1
  50. package/man/mend-skills-push.1 +1 -1
  51. package/man/mend-skills.1 +1 -1
  52. package/man/mend-ssh-setup.1 +6 -3
  53. package/man/mend-ssh.1 +1 -1
  54. package/man/mend-stop.1 +1 -1
  55. package/man/mend-ui.1 +1 -1
  56. package/man/mend-version.1 +1 -1
  57. package/man/mend-worktrees.1 +1 -1
  58. package/man/mend.1 +26 -2
  59. package/package.json +6 -2
  60. package/dist/agent-share.js +0 -150
  61. package/dist/clipboard.js +0 -73
  62. package/dist/dashboard-model.js +0 -308
  63. package/dist/dashboard.js +0 -1200
  64. package/dist/doctor.js +0 -233
  65. package/dist/dotfiles.js +0 -90
  66. package/dist/env.js +0 -17
  67. package/dist/help.js +0 -839
  68. package/dist/login.js +0 -240
  69. package/dist/pair.js +0 -83
  70. package/dist/review-model.js +0 -212
  71. package/dist/review-workflow.js +0 -8
  72. package/dist/review.js +0 -690
  73. package/dist/service-init.js +0 -227
  74. package/dist/shared.js +0 -338
  75. package/dist/skills.js +0 -134
  76. package/dist/ssh-setup.js +0 -181
  77. package/dist/terminal.js +0 -8
  78. package/dist/tui-theme.js +0 -41
  79. package/dist/version.js +0 -48
  80. package/dist/workbench-events.js +0 -72
package/dist/login.js DELETED
@@ -1,240 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import * as os from "node:os";
3
- import { groupCode } from "./pair.js";
4
- /**
5
- * The opened request as the server's JSON, checked field by field before the
6
- * CLI acts on it — a captive portal or a non-Mend server answering 200 must
7
- * read as "not a Mend server", never as a crash. Null follows the CLI's
8
- * absence idiom (chooseUrl, minutesUntil); the one caller turns it into the
9
- * exit message.
10
- */
11
- const parseCliAuthStart = (json) => {
12
- if (typeof json !== "object" || json === null)
13
- return null;
14
- if (!("deviceCode" in json &&
15
- "code" in json &&
16
- "verifyPath" in json &&
17
- "expiresAt" in json &&
18
- "intervalSeconds" in json)) {
19
- return null;
20
- }
21
- const { deviceCode, code, verifyPath, expiresAt, intervalSeconds } = json;
22
- if (typeof deviceCode !== "string" ||
23
- typeof code !== "string" ||
24
- typeof verifyPath !== "string" ||
25
- typeof expiresAt !== "string" ||
26
- typeof intervalSeconds !== "number") {
27
- return null;
28
- }
29
- return { deviceCode, code, verifyPath, expiresAt, intervalSeconds };
30
- };
31
- /** A poll answer's JSON, checked the same way; anything off-shape answers null. */
32
- const parseCliAuthPoll = (json) => {
33
- if (typeof json !== "object" || json === null || !("status" in json))
34
- return null;
35
- if (json.status === "pending")
36
- return { status: "pending" };
37
- if (json.status !== "approved")
38
- return null;
39
- if (!("token" in json && "user" in json && "device" in json))
40
- return null;
41
- const { token, user, device } = json;
42
- if (typeof token !== "string")
43
- return null;
44
- if (typeof user !== "object" || user === null)
45
- return null;
46
- if (!("id" in user && "name" in user && "email" in user))
47
- return null;
48
- if (typeof user.id !== "string" ||
49
- typeof user.name !== "string" ||
50
- typeof user.email !== "string") {
51
- return null;
52
- }
53
- if (typeof device !== "object" || device === null)
54
- return null;
55
- if (!("id" in device && "name" in device))
56
- return null;
57
- if (typeof device.id !== "string" || typeof device.name !== "string")
58
- return null;
59
- return {
60
- status: "approved",
61
- token,
62
- user: { id: user.id, name: user.name, email: user.email },
63
- device: { id: device.id, name: device.name },
64
- };
65
- };
66
- const paint = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text}` : text;
67
- const dim = paint("2");
68
- const green = paint("32");
69
- const cobalt = paint("34");
70
- const say = (line) => process.stdout.write(`${line}\n`);
71
- const fail = (message) => {
72
- process.stderr.write(`mend: ${message}\n`);
73
- process.exit(1);
74
- };
75
- const takeFlagValue = (args, flag) => {
76
- const at = args.indexOf(flag);
77
- return at !== -1 && args[at + 1] !== undefined ? String(args[at + 1]) : null;
78
- };
79
- /**
80
- * The server URL as a human types it: a bare `host:port` gets http://, a
81
- * trailing slash goes, anything the URL parser rejects answers null so the
82
- * caller can say so. The path is kept — an instance can live under one.
83
- */
84
- export const normalizeServerUrl = (input) => {
85
- const trimmed = input.trim();
86
- if (trimmed === "")
87
- return null;
88
- const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
89
- let url;
90
- try {
91
- url = new URL(withScheme);
92
- }
93
- catch {
94
- return null;
95
- }
96
- if (url.protocol !== "http:" && url.protocol !== "https:")
97
- return null;
98
- return `${url.origin}${url.pathname === "/" ? "" : url.pathname.replace(/\/$/, "")}`;
99
- };
100
- /** The page a human approves on — the verify path resolved against the URL the CLI dialed. */
101
- export const authorizeUrl = (base, verifyPath) => `${base}${verifyPath}`;
102
- /** How the browser opens on this platform; null means print the URL and let the human click. */
103
- export const browserCommand = (platform) => {
104
- if (platform === "darwin")
105
- return { command: "open", args: [] };
106
- if (platform === "linux")
107
- return { command: "xdg-open", args: [] };
108
- return null;
109
- };
110
- /** Polling cadence: what the server asked for, held to a sane band. */
111
- export const pollDelayMs = (intervalSeconds) => Math.min(Math.max(Math.round(intervalSeconds), 1), 10) * 1000;
112
- /** When to stop polling: the request's own expiry, or ten minutes for an unreadable date. */
113
- export const pollDeadline = (expiresAt, now = Date.now()) => {
114
- const at = Date.parse(expiresAt);
115
- return Number.isNaN(at) ? now + 10 * 60_000 : at;
116
- };
117
- /** Fire-and-forget: a browser that fails to open must never take the login down. */
118
- const openBrowser = (url) => {
119
- const opener = browserCommand(process.platform);
120
- if (opener === null || process.stdout.isTTY !== true)
121
- return;
122
- try {
123
- const child = spawn(opener.command, [...opener.args, url], {
124
- stdio: "ignore",
125
- detached: true,
126
- });
127
- child.on("error", () => undefined);
128
- child.unref();
129
- }
130
- catch {
131
- // The URL is already printed; clicking it is the fallback.
132
- }
133
- };
134
- /** One unauthenticated POST to the authorize surface; anything but 2xx surfaces as a status. */
135
- const post = async (url, body) => {
136
- let response;
137
- try {
138
- response = await fetch(url, {
139
- method: "POST",
140
- headers: { "content-type": "application/json" },
141
- body: JSON.stringify(body),
142
- });
143
- }
144
- catch {
145
- return fail(`cannot reach the Mend server at ${new URL(url).origin} — is it running?`);
146
- }
147
- const text = await response.text();
148
- let json = null;
149
- try {
150
- json = JSON.parse(text);
151
- }
152
- catch {
153
- // A non-JSON body only matters for the happy path, which always is JSON.
154
- }
155
- return { status: response.status, json };
156
- };
157
- const retryAfterSecondsOf = (json) => {
158
- if (typeof json === "object" && json !== null && "retryAfterSeconds" in json) {
159
- const seconds = Number(json.retryAfterSeconds);
160
- if (Number.isFinite(seconds) && seconds > 0)
161
- return seconds;
162
- }
163
- return 5;
164
- };
165
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
166
- /**
167
- * Where this login points: `--url` wins, then whatever is already configured
168
- * (MEND_URL or the config file) — a set URL is never asked about again;
169
- * `--url` is how you point somewhere else. Only a fresh machine prompts, and
170
- * only on a terminal; a piped stdin takes the default silently.
171
- */
172
- const resolveServerUrl = async (args, deps) => {
173
- const given = takeFlagValue(args, "--url") ?? deps.configuredUrl;
174
- if (given !== null) {
175
- return normalizeServerUrl(given) ?? fail(`"${given}" is not a URL the CLI can dial`);
176
- }
177
- if (process.stdin.isTTY !== true) {
178
- return (normalizeServerUrl(deps.defaultUrl) ??
179
- fail(`"${deps.defaultUrl}" is not a URL the CLI can dial`));
180
- }
181
- const readline = await import("node:readline/promises");
182
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
183
- const typed = (await rl.question(`mend server url ${dim(`[${deps.defaultUrl}]`)}: `)).trim();
184
- rl.close();
185
- const chosen = typed === "" ? deps.defaultUrl : typed;
186
- return normalizeServerUrl(chosen) ?? fail(`"${chosen}" is not a URL the CLI can dial`);
187
- };
188
- /**
189
- * `mend login [--url <server>]` — open, show, wait, save. The token lands in
190
- * the CLI config (0600) together with the device id, so `mend logout` can
191
- * revoke the token server-side instead of merely forgetting it.
192
- */
193
- export const loginCommand = async (args, deps) => {
194
- const base = await resolveServerUrl(args, deps);
195
- const started = await post(`${base}/api/cli/auth`, { name: os.hostname() });
196
- if (started.status === 429) {
197
- return fail(`the server asked for a pause — try again in ${retryAfterSecondsOf(started.json)}s`);
198
- }
199
- if (started.status < 200 || started.status >= 300) {
200
- return fail(`the server at ${base} refused to open an authorize request (${started.status})`);
201
- }
202
- const opened = parseCliAuthStart(started.json) ??
203
- fail(`the server at ${base} did not answer like a Mend server — is that the right URL?`);
204
- const url = authorizeUrl(base, opened.verifyPath);
205
- say(`${green("✓")} authorize request open at ${base}`);
206
- say(` ${dim("code")} ${groupCode(opened.code)} ${dim("· approve only if the browser shows the same code")}`);
207
- say(` ${dim("browser")} ${cobalt(url)}`);
208
- say("");
209
- say(dim(" waiting for approval… Ctrl-C stops; nothing is granted until someone approves"));
210
- openBrowser(url);
211
- const deadline = pollDeadline(opened.expiresAt);
212
- const delay = pollDelayMs(opened.intervalSeconds);
213
- while (Date.now() < deadline) {
214
- await sleep(delay);
215
- const poll = await post(`${base}/api/cli/auth/token`, { deviceCode: opened.deviceCode });
216
- if (poll.status === 429) {
217
- await sleep(retryAfterSecondsOf(poll.json) * 1000);
218
- continue;
219
- }
220
- if (poll.status === 403)
221
- return fail("denied in the browser; nothing was granted");
222
- if (poll.status === 404 || poll.status === 410) {
223
- return fail("the authorize request is no longer open — run mend login again");
224
- }
225
- if (poll.status < 200 || poll.status >= 300) {
226
- return fail(`the server answered ${poll.status} while waiting — run mend login again`);
227
- }
228
- const result = parseCliAuthPoll(poll.json);
229
- if (result === null) {
230
- return fail("the server's answer stopped making sense while waiting — run mend login again");
231
- }
232
- if (result.status === "pending")
233
- continue;
234
- deps.save({ url: base, token: result.token, deviceId: result.device.id });
235
- say(`${green("✓")} signed in as ${result.user.email}`);
236
- say(` ${dim("this terminal is the device")} ${result.device.name} ${dim("· revoke it any time under Settings → Devices")}`);
237
- return;
238
- }
239
- return fail("the authorize request expired before anyone approved — run mend login again");
240
- };
package/dist/pair.js DELETED
@@ -1,83 +0,0 @@
1
- const paint = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text}` : text;
2
- const dim = paint("2");
3
- const green = paint("32");
4
- const say = (line) => process.stdout.write(`${line}\n`);
5
- const fail = (message) => {
6
- process.stderr.write(`mend: ${message}\n`);
7
- process.exit(1);
8
- };
9
- const takeFlagValue = (args, flag) => {
10
- const at = args.indexOf(flag);
11
- return at !== -1 && args[at + 1] !== undefined ? String(args[at + 1]) : null;
12
- };
13
- /**
14
- * The code as a human reads it aloud: 8 Crockford characters in two groups. The
15
- * server compares case-insensitively with dashes stripped, so the dash is purely
16
- * for the eye and either form can be typed back.
17
- */
18
- export const groupCode = (code) => {
19
- const bare = code.replace(/[^0-9a-z]/gi, "").toUpperCase();
20
- return bare.length <= 4 ? bare : `${bare.slice(0, 4)}-${bare.slice(4)}`;
21
- };
22
- /** The scanned string. The device reads the base URL and the code out of it, nothing else. */
23
- export const pairingLink = (url, code) => `mend://pair?u=${encodeURIComponent(url)}&c=${code}`;
24
- /**
25
- * Tailscale hands every node an IPv4 in the CGNAT range 100.64.0.0/10 — the same
26
- * observation packages/api/src/machine.ts makes. A tailnet URL is preferred because
27
- * it keeps working when the phone leaves the LAN.
28
- */
29
- const isTailnetUrl = (value) => {
30
- let host;
31
- try {
32
- host = new URL(value).hostname;
33
- }
34
- catch {
35
- return false;
36
- }
37
- const parts = host.split(".").map(Number);
38
- const [first, second] = parts;
39
- if (parts.length !== 4 || first === undefined || second === undefined)
40
- return false;
41
- return first === 100 && second >= 64 && second <= 127;
42
- };
43
- /** `--url` wins; otherwise the tailnet address, otherwise the first candidate the server offered. */
44
- export const chooseUrl = (urls, override) => override ?? urls.find(isTailnetUrl) ?? urls[0] ?? null;
45
- /** Whole minutes left, floored at 0; null when the server sent a date this CLI cannot read. */
46
- export const minutesUntil = (expiresAt, now = Date.now()) => {
47
- const at = Date.parse(expiresAt);
48
- if (Number.isNaN(at))
49
- return null;
50
- return Math.max(0, Math.round((at - now) / 60_000));
51
- };
52
- /** The QR as block characters. Deliberately not gated on isTTY — a piped QR still scans. */
53
- export const renderQr = async (text) => {
54
- const { toString: toQrString } = await import("qrcode");
55
- const options = { type: "terminal", small: true };
56
- return (await toQrString(text, options)).trimEnd();
57
- };
58
- /**
59
- * `mend pair [--url <base url>]`: mint a pairing code and show it. The device names
60
- * itself when it claims the code, so this side sends nothing but the request.
61
- */
62
- export const pairCommand = async (args, api) => {
63
- const pairing = await api("POST", "/me/devices/pairings");
64
- const url = chooseUrl(pairing.urls, takeFlagValue(args, "--url"));
65
- if (url === null) {
66
- return fail("no LAN or tailnet address to hand the device — pass one: mend pair --url http://<host>:<port>");
67
- }
68
- const minutes = minutesUntil(pairing.expiresAt);
69
- say(await renderQr(pairingLink(url, pairing.code)));
70
- say(`${green("✓")} pairing code ${groupCode(pairing.code)}`);
71
- say(` ${dim("url")} ${url}`);
72
- say(` ${dim("expires")} ${minutes === null ? pairing.expiresAt : `in ${minutes} min`} ${dim("· one device, once")}`);
73
- say("");
74
- say(dim(" scan it in the Mend app, or enter the url and the code there by hand"));
75
- say(dim(" the app is apps/mobile — build it yourself; it is not published yet"));
76
- };
77
- /** `mend qr <text>`: print a terminal QR of anything. Hidden from help; the installer uses it. */
78
- export const qrCommand = async (args) => {
79
- const text = args.join(" ").trim();
80
- if (text === "")
81
- return fail("usage: mend qr <text>");
82
- say(await renderQr(text));
83
- };
@@ -1,212 +0,0 @@
1
- import * as path from "node:path";
2
- const FILETYPE_BY_EXTENSION = {
3
- c: "c",
4
- cc: "cpp",
5
- cpp: "cpp",
6
- css: "css",
7
- go: "go",
8
- h: "c",
9
- hpp: "cpp",
10
- html: "html",
11
- java: "java",
12
- js: "javascript",
13
- json: "json",
14
- jsx: "javascript",
15
- md: "markdown",
16
- mjs: "javascript",
17
- py: "python",
18
- rb: "ruby",
19
- rs: "rust",
20
- sh: "bash",
21
- sql: "sql",
22
- swift: "swift",
23
- toml: "toml",
24
- ts: "typescript",
25
- tsx: "tsx",
26
- yaml: "yaml",
27
- yml: "yaml",
28
- zsh: "bash",
29
- };
30
- const filetypeOf = (name) => {
31
- const extension = path.extname(name).slice(1).toLowerCase();
32
- return extension === "" ? undefined : (FILETYPE_BY_EXTENSION[extension] ?? extension);
33
- };
34
- const statusOf = (patch) => {
35
- if (/^new file mode /m.test(patch))
36
- return "added";
37
- if (/^deleted file mode /m.test(patch))
38
- return "deleted";
39
- if (/^rename (?:from|to) /m.test(patch))
40
- return "renamed";
41
- return "modified";
42
- };
43
- const isLikelyGenerated = (name) => /(^|\/)(?:dist|build|generated)(?:\/|$)/i.test(name) ||
44
- /(?:\.generated\.|\.min\.(?:js|css)$|(?:^|\/)(?:pnpm-lock\.yaml|package-lock\.json|yarn\.lock)$)/i.test(name);
45
- /** A legible terminal whitespace mode that preserves git's control prefixes. */
46
- export const visibleWhitespace = (patch) => patch
47
- .split("\n")
48
- .map((line) => {
49
- const hunkContent = (line.startsWith("+") && !line.startsWith("+++")) ||
50
- (line.startsWith("-") && !line.startsWith("---")) ||
51
- line.startsWith(" ");
52
- if (!hunkContent)
53
- return line;
54
- const prefix = line.slice(0, 1);
55
- const content = line.slice(1).replaceAll("\t", "→····").replaceAll(" ", "·");
56
- return `${prefix}${content}`;
57
- })
58
- .join("\n");
59
- /**
60
- * Git is the source of this wire value, so a file begins only at a column-zero
61
- * `diff --git` header. A changed source line is prefixed with +/− and cannot
62
- * accidentally split the patch.
63
- */
64
- const splitFilePatches = (diff) => {
65
- const starts = [...diff.matchAll(/^diff --git /gm)].map((match) => match.index);
66
- return starts.map((start, index) => {
67
- const end = starts[index + 1] ?? diff.length;
68
- const patch = diff.slice(start, end);
69
- return patch.endsWith("\n") ? patch : `${patch}\n`;
70
- });
71
- };
72
- const parseRows = (patch) => {
73
- const patchLines = patch.split("\n");
74
- const lines = [];
75
- const hunks = [];
76
- let unifiedRow = 0;
77
- let splitRow = 0;
78
- let hunkIndex = -1;
79
- let oldLine = 0;
80
- let newLine = 0;
81
- for (let index = 0; index < patchLines.length; index += 1) {
82
- const header = patchLines[index] ?? "";
83
- const hunkMatch = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(header);
84
- if (hunkMatch === null)
85
- continue;
86
- hunkIndex += 1;
87
- oldLine = Number(hunkMatch[1]);
88
- newLine = Number(hunkMatch[2]);
89
- hunks.push({ index: hunkIndex, header, unifiedRow, splitRow });
90
- const body = [];
91
- let bodyIndex = index + 1;
92
- while (bodyIndex < patchLines.length) {
93
- const bodyLine = patchLines[bodyIndex] ?? "";
94
- if (bodyLine.startsWith("@@ "))
95
- break;
96
- if (bodyLine.startsWith("+") && !bodyLine.startsWith("+++")) {
97
- body.push({ kind: "addition" });
98
- }
99
- else if (bodyLine.startsWith("-") && !bodyLine.startsWith("---")) {
100
- body.push({ kind: "deletion" });
101
- }
102
- else if (bodyLine.startsWith(" ")) {
103
- body.push({ kind: "context" });
104
- }
105
- bodyIndex += 1;
106
- }
107
- let cursor = 0;
108
- while (cursor < body.length) {
109
- const current = body[cursor];
110
- if (current?.kind === "context") {
111
- lines.push({
112
- kind: "context",
113
- oldLine,
114
- newLine,
115
- unifiedRow,
116
- splitRow,
117
- hunk: hunkIndex,
118
- commentable: true,
119
- });
120
- oldLine += 1;
121
- newLine += 1;
122
- unifiedRow += 1;
123
- splitRow += 1;
124
- cursor += 1;
125
- continue;
126
- }
127
- // Git groups a replacement as a run of deletions/additions. Unified
128
- // stacks every row; split aligns the Nth deletion with the Nth addition.
129
- const groupStart = cursor;
130
- while (cursor < body.length && body[cursor]?.kind !== "context")
131
- cursor += 1;
132
- const group = body.slice(groupStart, cursor);
133
- let deletionOffset = 0;
134
- let additionOffset = 0;
135
- for (const entry of group) {
136
- if (entry.kind === "deletion") {
137
- lines.push({
138
- kind: entry.kind,
139
- oldLine,
140
- newLine: null,
141
- unifiedRow,
142
- splitRow: splitRow + deletionOffset,
143
- hunk: hunkIndex,
144
- // ReviewComment persists a line number but no old/new side. Like
145
- // the web review, deletions therefore abstain instead of storing
146
- // a confidently wrong new-file anchor.
147
- commentable: false,
148
- });
149
- oldLine += 1;
150
- deletionOffset += 1;
151
- }
152
- else {
153
- lines.push({
154
- kind: entry.kind,
155
- oldLine: null,
156
- newLine,
157
- unifiedRow,
158
- splitRow: splitRow + additionOffset,
159
- hunk: hunkIndex,
160
- commentable: true,
161
- });
162
- newLine += 1;
163
- additionOffset += 1;
164
- }
165
- unifiedRow += 1;
166
- }
167
- splitRow += Math.max(deletionOffset, additionOffset);
168
- }
169
- index = bodyIndex - 1;
170
- }
171
- return { lines, hunks, unifiedRows: unifiedRow, splitRows: splitRow };
172
- };
173
- /** Public review seam: the API's live git diff becomes the terminal's files, hunks, and anchors. */
174
- export const buildReviewFiles = (diff, stats) => {
175
- const patches = splitFilePatches(diff);
176
- return stats.map((stat, index) => {
177
- const patch = patches[index] ?? "";
178
- const rows = parseRows(patch);
179
- return {
180
- ...stat,
181
- patch,
182
- filetype: filetypeOf(stat.path),
183
- status: statusOf(patch),
184
- binary: /^Binary files |^GIT binary patch$/m.test(patch),
185
- likelyGenerated: isLikelyGenerated(stat.path),
186
- ...rows,
187
- };
188
- });
189
- };
190
- /** The mechanically assembled draft remains editable before the API receives it. */
191
- export const assembleReviewInstruction = (change, comments) => {
192
- const points = comments
193
- .map((comment, index) => {
194
- const anchor = comment.file === null
195
- ? "the change as a whole"
196
- : `${comment.file}${comment.line === null ? "" : `:${comment.line}${comment.endLine === null || comment.endLine === comment.line ? "" : `-${comment.endLine}`}`}`;
197
- const proposal = comment.suggestion === null
198
- ? ""
199
- : `\n Proposed replacement:\n${comment.suggestion
200
- .split("\n")
201
- .map((line) => ` ${line}`)
202
- .join("\n")}`;
203
- return `${index + 1}. ${anchor} — ${comment.body}${proposal}`;
204
- })
205
- .join("\n");
206
- return `This is a follow-up on your earlier work in this worktree, responding to review feedback. The branch ${change.branch} is already checked out with your work on it: keep working there, and do not start a new branch or reset it.
207
-
208
- Review comments:
209
- ${points}
210
-
211
- Address each point using your own judgment about how. Check your work with the repository's own build, tests, or typecheck; a check that still fails is something to report, not to force green. Report honestly what you changed and what you did not — if a point cannot or should not be done, say so plainly.`;
212
- };
@@ -1,8 +0,0 @@
1
- export const reviewTargetForSession = (session, annotation, projectName) => annotation?.changeId === null || annotation?.changeId === undefined
2
- ? null
3
- : { session, changeId: annotation.changeId, projectName };
4
- export const commentRange = (start, current) => start === null
5
- ? { line: current, endLine: null }
6
- : { line: Math.min(start, current), endLine: Math.max(start, current) };
7
- /** One server-owned operation: persist intent, launch, correlate, finalize. */
8
- export const deliverReview = (api, sessionId, input) => api("POST", `/sessions/${sessionId}/follow-up/deliver`, input);