@d3lm/lint-preset 1.4.6 → 1.5.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.
@@ -0,0 +1,3215 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/comments/cli.ts
4
+ import { existsSync as existsSync4, realpathSync, statSync as statSync3 } from "fs";
5
+ import path5 from "path";
6
+ import { parseArgs } from "util";
7
+
8
+ // src/comments/agent.ts
9
+ import { spawn } from "child_process";
10
+ import { readFileSync } from "fs";
11
+ import path from "path";
12
+ var CHILD_ENV = "COMMENTS_HOOK_CHILD";
13
+ var DEFAULT_RULES = `
14
+ # Comment rules
15
+
16
+ ## Priorities
17
+
18
+ When rules pull in different directions, the earlier one wins.
19
+
20
+ 1. Keep code and exact text byte for byte, including every punctuation character in it. That covers code blocks, \`@example\`, code spans, identifiers, string literals like error messages, regexes, globs and other patterns like \`*test*\`, URLs, quoted text, titles, and notation like \`host:port\`.
21
+ 2. Keep the meaning. An edit may make a comment clearer, never different.
22
+ 3. Follow the style preferences below.
23
+ 4. Change as little as possible. Wording that already reads clearly stays, even when a note points at it.
24
+
25
+ ## Layout
26
+
27
+ - After you finish, the tool rewraps prose paragraphs, list items, and JSDoc tag descriptions. The width is a preferred target. A parenthetical may extend a line up to the configured maximum so it stays together. In longer paragraphs, balanced lines take priority over keeping every parenthetical on one line. The tool also avoids short last lines where possible. Leave these line breaks to the tool, except for the sentence-orphan case below.
28
+ - Don't end a line with the first word of a new sentence, clause, or parenthetical, like \`This\` after a period, \`the\` after a comma, or \`(the\`. Move it to the next line instead.
29
+ - In a paragraph of two lines, the first line ends with a sentence whenever the rest fits on the second line and is no longer than the first. Don't end it with the start of the next sentence, like \`A wrap\` in \`...lay out differently. A wrap\`. Move those words to the second line instead.
30
+ - A comment whose whole text is one paragraph stays on a single line when that line fits a separate, larger single-line width, even past the width. Only a longer comment wraps to the width.
31
+ - Separate paragraphs with an empty comment line.
32
+ - Never split a backtick code span across lines.
33
+ - Keep code blocks, \`@example\`, lists, tables, box drawings, aligned columns, and Markdown hard line breaks (a line that ends with two spaces or a backslash) laid out as they are. List items wrap with their continuation lines indented to the item text, and a JSDoc tag description that doesn't fit continues on the next line without indentation.
34
+ - The tool measures every line, taking the tolerance and the parts it keeps as written into account, and the notes on each comment list what is still wrong. Act on those notes. Don't count columns, and don't apply a stricter limit than the notes show.
35
+ - The maximum is a hard limit for measured prose, including long tokens. If a URL, code span, or other exact text exceeds it, preserve that text and explain the unresolved overflow instead of changing its contents.
36
+ - The tool never rewraps the parts it keeps as written, like a single-line block comment, a paragraph with hard line breaks, or lines that may be laid out by hand. When a note says one of their prose lines is too long, break or shorten it yourself. Exact text and intentional layout win over the width, so keep code, code spans, and the layout in them intact, and if a note can only be fixed by changing them, leave it and say why in your reply.
37
+
38
+ ## Style
39
+
40
+ These are preferences for comment prose. They never apply to code or exact text (see the priorities above), and a punctuation character on its own is never a reason to edit prose that already reads clearly.
41
+
42
+ - Write clear sentences that flow. Descriptions read best as complete sentences with a subject and a verb, so prefer "Returns the bytes the guest sent" over a fragment like \`Bytes from the guest.\` or \`Optional.\` Imperative or third-person verb phrases like "Resolve a hostname..." or "Aborts every channel..." are fine, since the preference is against verb-less fragments, not against concise sentences.
43
+ - Prefer plain sentence punctuation. Where a semicolon, an em-dash (\u2014), or a spaced hyphen, en-dash, or double hyphen standing in for one only joins two thoughts, a period, a comma, parentheses, or a word like "and", "so", or "because" usually reads better. Keep the punctuation where it carries meaning or reads more clearly than the alternatives.
44
+ - Prefer a sentence over a "Label: explanation" fragment, like \`...the socket. Optional: without it...\` or \`Default: refuse\` in the middle of prose. A colon is fine where it introduces a list, code block, or example, in a term's definition, in notation, in a quotation, and in a \`Note:\` or \`Optional:\` that starts a comment. A JSDoc tag like \`@note\` or \`@todo\` can replace a label, and \`@default\` fits a default value, not a description.
45
+ - Avoid asterisk emphasis like \`*this*\`, and use plain wording instead. A name or pattern in asterisks, like \`*test*\` or \`*.spec.ts\`, is a pattern, so keep its asterisks and wrap it in a code span, like \`\` \`*test*\` \`\`.
46
+ - Avoid section banner comments, i.e. dividers or headings made of dashes, equals signs, asterisks, or other repeated characters that split a file into visual sections. Let the code structure carry the grouping, or write one short, plain sentence comment (e.g. \`// handlers for control mode\`). Rules that belong to a diagram, a table, a box drawing, a Markdown document, an example, or a legal header aren't banners.
47
+ - Block and JSDoc comments are capitalized and end with punctuation.
48
+ - Never turn a block or JSDoc comment into \`//\` line comments, and never write a run of consecutive \`//\` lines.
49
+
50
+ ## Editing
51
+
52
+ - Keep the wording unless it goes against a rule above. Then reword as little as needed and keep the meaning.
53
+ - Don't reword anything else unless the instructions below allow it.
54
+ `.trim();
55
+ var DEFAULT_TIMEOUT_SECONDS = 300;
56
+ var DEFAULT_CLAUDE_MODEL = "haiku";
57
+ var KILL_GRACE_MS = 5e3;
58
+ var DRAIN_GRACE_MS = 1e3;
59
+ var RUNNER_COMMANDS = {
60
+ claude: "claude",
61
+ codex: "codex",
62
+ cursor: "cursor-agent"
63
+ };
64
+ var ISSUE_DESCRIPTIONS = {
65
+ underfill: "ends early, and words from the next line fit on it",
66
+ widow: "leaves fewer than three words on the last line of its paragraph",
67
+ unbalanced: "is in a paragraph whose lines are less even than the suggested rewrap",
68
+ orphan: "may end with the start of a new sentence, clause, or parenthetical, which belongs on the next line (leave it if the period or comma before it is part of an abbreviation, a name, or notation like `No. 3`)",
69
+ "em-dash": "may use an em-dash to join two thoughts (leave it if it's in a quotation, a title, or a string literal, or reads more clearly than the alternatives)",
70
+ banner: "may be a section banner (leave it if it's part of a diagram, a table, a box, a Markdown rule, a separator in an example, or legal text)",
71
+ semicolon: "may join clauses with a semicolon (leave it if it's code, a literal, not a clause join, or reads more clearly than a period or a joining word)",
72
+ "label-colon": "may use a colon after a label (leave it if it introduces a list or code, defines a term, is notation like a time or `key: value`, or is a `Note:` or `Optional:` that starts the comment)",
73
+ emphasis: "may use asterisk emphasis (leave the asterisks of a name or pattern like `*test*`, which belongs in a code span with its asterisks)",
74
+ dash: "may use a spaced hyphen, en-dash, or double hyphen as an em-dash (leave it if it's a range, a CLI argument, arithmetic, code, a term's definition, or reads more clearly than the alternatives)"
75
+ };
76
+ var HINT_MARKER = "possible:";
77
+ function describeIssue(issue, width, maxWidth) {
78
+ if (issue.type === "overflow") {
79
+ const limit = maxWidth ?? width;
80
+ const name = maxWidth === void 0 ? "width" : "maximum";
81
+ return `line ${issue.line} is ${issue.length} columns, over the ${name} of ${limit}`;
82
+ }
83
+ const description = `line ${issue.line} ${ISSUE_DESCRIPTIONS[issue.type]}`;
84
+ return issue.tier === "hint" ? `${HINT_MARKER} ${description}` : description;
85
+ }
86
+ function describeIssues(paragraph, width, maxWidth) {
87
+ return paragraph.issues.map((issue) => describeIssue(issue, width, maxWidth));
88
+ }
89
+ function describeComment(comment) {
90
+ const notes = [];
91
+ for (const paragraph of comment.paragraphs) {
92
+ const issues = describeIssues(paragraph, comment.width, comment.maxWidth);
93
+ if (issues.length > 0 && !paragraph.reflowable) {
94
+ const range = `lines ${paragraph.startLine}-${paragraph.endLine}`;
95
+ issues.push(
96
+ `${range} are kept as written (${paragraph.reason ?? "layout"}), so the tool never rewraps them and any fix to them is yours`
97
+ );
98
+ }
99
+ notes.push(...issues);
100
+ }
101
+ const fence = comment.text.includes("```") ? "~~~~" : "```";
102
+ return [
103
+ `### ${comment.file}:${comment.startLine}-${comment.endLine} (width ${comment.width}${comment.maxWidth === void 0 ? "" : `, maximum ${comment.maxWidth}`})`,
104
+ "",
105
+ ...notes.length > 0 ? notes.map((note2) => `- ${note2}`) : ["- no measured issues; review the wording and flow"],
106
+ "",
107
+ `${fence}ts`,
108
+ comment.text,
109
+ fence
110
+ ].join("\n");
111
+ }
112
+ function loadRules(agent, configPath) {
113
+ let rules = DEFAULT_RULES;
114
+ if (agent.rules) {
115
+ const base = configPath ? path.dirname(configPath) : process.cwd();
116
+ rules = readFileSync(path.resolve(base, agent.rules), "utf8");
117
+ }
118
+ return agent.instructions ? `${rules.trimEnd()}
119
+
120
+ ## Instructions
121
+
122
+ ${agent.instructions.trim()}
123
+ ` : rules;
124
+ }
125
+ function buildPrompt(options) {
126
+ return [
127
+ options.rules.trimEnd(),
128
+ "",
129
+ "## Task",
130
+ "",
131
+ `Fix the formatting of the comments below, following the rules above. Paths are relative to ${options.root}. Widths include indentation and the comment prefix. The notes under each comment come from the tool's measurements, so never count columns yourself.`,
132
+ "",
133
+ "- After you finish, the tool rewraps every prose paragraph as described under Layout, so a layout note on a prose paragraph needs no change from you. Change only the wording there, and leave its line breaks to the tool.",
134
+ "- A note that a line may end with the start of a sentence, clause, or parenthetical is the exception. Only you can tell whether it does, so if it does, move those words to the start of the next line.",
135
+ "- Parts kept as written are never rewrapped, and their notes say so. Fix those notes by hand, for example by breaking an overflowing prose line or by turning a long single-line comment into a multi-line one. Keep code, code spans, tables, box drawings, hard line breaks, and other intentional layout in them exactly as they are. If a note can only be fixed by changing those, leave it and say why in your reply.",
136
+ `- Notes marked \`${HINT_MARKER}\` come from heuristics. If the rule doesn't actually apply, don't change anything for that note.`,
137
+ "- Notes only point at places worth a look. Review each comment against the rules, and leave it as it is when it already follows them.",
138
+ "- Edit only these comments. Don't change code, other comments, or other files.",
139
+ "- When done, reply with one line per comment saying what you changed or why you left it.",
140
+ "",
141
+ ...options.comments.map((comment) => describeComment(comment)).flatMap((section) => [section, ""])
142
+ ].join("\n");
143
+ }
144
+ function shellQuote(value) {
145
+ return /^[\w./:@=-]+$/.test(value) ? value : `'${value.replaceAll("'", String.raw`'\''`)}'`;
146
+ }
147
+ function buildInvocation(options) {
148
+ const { agent, prompt, root } = options;
149
+ const extra = agent.args ?? [];
150
+ if (agent.command && agent.command.length > 0) {
151
+ const [command, ...args] = agent.command;
152
+ const hasPlaceholder = args.some((arg) => arg.includes("{prompt}"));
153
+ return {
154
+ command,
155
+ // a function keeps the prompt as it is, while a string would expand `$&` or `$'` in a comment's text
156
+ args: args.map((arg) => arg.replaceAll("{prompt}", () => prompt)),
157
+ stdin: hasPlaceholder ? void 0 : prompt
158
+ };
159
+ }
160
+ const model = agent.model ? ["--model", agent.model] : [];
161
+ if (options.runner === "claude") {
162
+ return {
163
+ command: "claude",
164
+ args: [
165
+ "-p",
166
+ "--no-session-persistence",
167
+ "--model",
168
+ agent.model ?? DEFAULT_CLAUDE_MODEL,
169
+ "--permission-mode",
170
+ "acceptEdits",
171
+ "--allowedTools",
172
+ "Read",
173
+ "Edit",
174
+ "Grep",
175
+ "Glob",
176
+ ...extra
177
+ ],
178
+ stdin: prompt
179
+ };
180
+ }
181
+ if (options.runner === "codex") {
182
+ return {
183
+ command: "codex",
184
+ args: [
185
+ "exec",
186
+ "--ephemeral",
187
+ "--sandbox",
188
+ "workspace-write",
189
+ "--cd",
190
+ root,
191
+ "-c",
192
+ "features.hooks=false",
193
+ ...model,
194
+ ...extra,
195
+ "-"
196
+ ],
197
+ stdin: prompt
198
+ };
199
+ }
200
+ return {
201
+ command: "cursor-agent",
202
+ args: ["-p", "--trust", "--force", "--workspace", root, ...model, ...extra, prompt]
203
+ };
204
+ }
205
+ function describeAgent(agent, runner) {
206
+ if (agent.command && agent.command.length > 0) {
207
+ return path.basename(agent.command[0]);
208
+ }
209
+ const model = runner === "claude" ? agent.model ?? DEFAULT_CLAUDE_MODEL : agent.model;
210
+ return model ? `${RUNNER_COMMANDS[runner]}, model ${model}` : RUNNER_COMMANDS[runner];
211
+ }
212
+ function formatInvocation(invocation) {
213
+ const args = invocation.args.map((arg) => arg.length > 80 ? "<prompt>" : shellQuote(arg));
214
+ return [invocation.command, ...args].join(" ");
215
+ }
216
+ function runAgent(invocation, options) {
217
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT_SECONDS;
218
+ return new Promise((resolve) => {
219
+ const child = spawn(invocation.command, invocation.args, {
220
+ cwd: options.root,
221
+ env: { ...process.env, [CHILD_ENV]: "1" }
222
+ });
223
+ const timers = [];
224
+ let stdout = "";
225
+ let stderr = "";
226
+ let timedOut = false;
227
+ let settled = false;
228
+ const settle = (result) => {
229
+ if (settled) {
230
+ return;
231
+ }
232
+ settled = true;
233
+ for (const timer of timers) {
234
+ clearTimeout(timer);
235
+ }
236
+ child.stdout.destroy();
237
+ child.stderr.destroy();
238
+ child.unref();
239
+ resolve({ ...result, output: `${stdout}${stderr}`.trim() });
240
+ };
241
+ const finish = (code, signal) => {
242
+ if (timedOut) {
243
+ settle({ ok: false, error: `the agent timed out after ${timeout}s` });
244
+ } else if (code === 0) {
245
+ settle({ ok: true });
246
+ } else {
247
+ settle({ ok: false, error: `the agent exited with code ${code ?? signal}` });
248
+ }
249
+ };
250
+ timers.push(
251
+ setTimeout(() => {
252
+ timedOut = true;
253
+ child.kill();
254
+ timers.push(
255
+ setTimeout(
256
+ () => {
257
+ child.kill("SIGKILL");
258
+ finish(null, "SIGKILL");
259
+ },
260
+ Math.min(KILL_GRACE_MS, timeout * 1e3)
261
+ )
262
+ );
263
+ }, timeout * 1e3)
264
+ );
265
+ child.stdout.setEncoding("utf8").on("data", (chunk) => {
266
+ stdout += chunk;
267
+ });
268
+ child.stderr.setEncoding("utf8").on("data", (chunk) => {
269
+ stderr += chunk;
270
+ });
271
+ child.on("error", (error) => {
272
+ settle({
273
+ ok: false,
274
+ error: error.code === "ENOENT" ? `\`${invocation.command}\` is not installed` : error.message
275
+ });
276
+ });
277
+ child.on("close", finish);
278
+ child.on("exit", (code, signal) => {
279
+ timers.push(
280
+ setTimeout(() => {
281
+ finish(code, signal);
282
+ }, DRAIN_GRACE_MS)
283
+ );
284
+ });
285
+ child.stdin.on("error", () => {
286
+ });
287
+ child.stdin.end(invocation.stdin ?? "");
288
+ });
289
+ }
290
+
291
+ // src/comments/analyze.ts
292
+ import { parseSync } from "oxc-parser";
293
+
294
+ // src/comments/style.ts
295
+ var BANNER = /^(?!#{1,6}\s)([-=*#_~+/\\])\1{3,}|([-=*#_~])\2{3,}\s*$/;
296
+ var COLUMN_RULE = /^([-=_~])\1*(?:\s+\1+)+$/;
297
+ var DASH = /(?<!\d)\s[–-]\s(?!\d)|(?<!(?:^|\s)-[\w-]*)\s--\s(?!-)/;
298
+ var SEMICOLON = /;(?:\s|$)/;
299
+ var EMPHASIS = /(?:^|[\s(])(\*\*?)(?=[\p{L}\p{N}])[^*]+?(?<!\s)\1(?=$|[\s)"']|[.,;:!?](?:$|[\s)"']))/u;
300
+ var LIST_MARKER = /^(?:[-*+]|\d+[.)])\s+/;
301
+ var CODE_SPAN = /(?<!`)(`+)(?!`)[\s\S]*?(?<!`)\1(?!`)/g;
302
+ var COLON = /(?<=[\w)\]'"’])(?<!(?:^|[.!?]\s+)(?:[A-Z]{2,}|[Tt]odo|[Ff]ixme|[Hh]ack|[Xx]xx))(?<!\b(?:code|url|entity|[A-Za-z][a-z\d]*[A-Z]\w*|\w*_\w*)):\s+\S/;
303
+ function replaceKeepingLines(text, pattern, replacement) {
304
+ return text.replaceAll(pattern, (match) => `${replacement}${"\n".repeat(match.split("\n").length - 1)}`);
305
+ }
306
+ function stripNonProse(text) {
307
+ let stripped = replaceKeepingLines(text, CODE_SPAN, "code");
308
+ for (let previous = ""; previous !== stripped; ) {
309
+ previous = stripped;
310
+ stripped = replaceKeepingLines(stripped, /\{[^{}]*\}/g, "code");
311
+ }
312
+ return stripped.replaceAll(/\b[a-z][\w+.-]*:\/\/\S+/gi, "url").replaceAll(/&(?:[a-z][a-z\d]*|#\d+|#x[\da-f]+);/gi, "entity");
313
+ }
314
+ function hasExplanatoryColon(text) {
315
+ return COLON.test(text.replace(LIST_MARKER, ""));
316
+ }
317
+ function isBanner(text) {
318
+ const prose = stripNonProse(text.trim());
319
+ return BANNER.test(prose) && !COLUMN_RULE.test(prose);
320
+ }
321
+ function findStyleIssues(lines) {
322
+ const issues = [];
323
+ const prose = stripNonProse(lines.map((line) => line.text.trim()).join("\n"));
324
+ const proseLines = replaceKeepingLines(prose, /^@\w+\s+(?:code\s+)?(?:\S+\s+-\s+)?/gm, "").split("\n");
325
+ for (const [position, line] of lines.entries()) {
326
+ const text = proseLines[position].trim();
327
+ const report = (type) => {
328
+ issues.push({ type, index: line.index });
329
+ };
330
+ if (isBanner(line.text)) {
331
+ report("banner");
332
+ } else {
333
+ if (SEMICOLON.test(text)) {
334
+ report("semicolon");
335
+ }
336
+ if (text.includes("\u2014")) {
337
+ report("em-dash");
338
+ }
339
+ if (DASH.test(text)) {
340
+ report("dash");
341
+ }
342
+ if (hasExplanatoryColon(text)) {
343
+ report("label-colon");
344
+ }
345
+ if (EMPHASIS.test(text)) {
346
+ report("emphasis");
347
+ }
348
+ }
349
+ }
350
+ return issues;
351
+ }
352
+
353
+ // src/comments/wrap.ts
354
+ var CLAUSE_END = /[.!?,;:]["')\]*_]*$/;
355
+ var SENTENCE_END = /[.!?]["')\]*_]*$/;
356
+ var ABBREVIATIONS = /* @__PURE__ */ new Set([
357
+ "e.g.",
358
+ "i.e.",
359
+ "etc.",
360
+ "vs.",
361
+ "cf.",
362
+ "approx.",
363
+ "incl.",
364
+ "resp.",
365
+ "al.",
366
+ "viz.",
367
+ "esp.",
368
+ "ca.",
369
+ "fig.",
370
+ "figs.",
371
+ "eq.",
372
+ "vol.",
373
+ "pp.",
374
+ "mr.",
375
+ "mrs.",
376
+ "ms.",
377
+ "dr.",
378
+ "prof.",
379
+ "st."
380
+ ]);
381
+ function isAbbreviation(word) {
382
+ const bare = word.replace(/^[(["']+/, "").replace(/[)\]"']+$/, "").toLowerCase();
383
+ return ABBREVIATIONS.has(bare) || /^(?:[a-z]\.){2,}$/.test(bare);
384
+ }
385
+ function findClosingBrace(text, start) {
386
+ let depth = 0;
387
+ for (let index = start; index < text.length; index += 1) {
388
+ if (text[index] === "{") {
389
+ depth += 1;
390
+ } else if (text[index] === "}") {
391
+ depth -= 1;
392
+ if (depth === 0) {
393
+ return index;
394
+ }
395
+ }
396
+ }
397
+ return -1;
398
+ }
399
+ function backtickRun(text, start) {
400
+ let end = start;
401
+ while (text[end] === "`") {
402
+ end += 1;
403
+ }
404
+ return end - start;
405
+ }
406
+ function findCodeSpanEnd(text, start) {
407
+ const run = backtickRun(text, start);
408
+ for (let index = text.indexOf("`", start + run); index !== -1; ) {
409
+ const length = backtickRun(text, index);
410
+ if (length === run) {
411
+ return index + length - 1;
412
+ }
413
+ index = text.indexOf("`", index + length);
414
+ }
415
+ return -1;
416
+ }
417
+ function tokenize(text) {
418
+ const tokens = [];
419
+ let current = "";
420
+ let index = 0;
421
+ while (index < text.length) {
422
+ const char = text[index];
423
+ const close = char === "`" ? findCodeSpanEnd(text, index) : char === "{" ? findClosingBrace(text, index) : -1;
424
+ if (char === "`" && close === -1) {
425
+ const run = backtickRun(text, index);
426
+ current += text.slice(index, index + run);
427
+ index += run;
428
+ continue;
429
+ }
430
+ if (close !== -1) {
431
+ current += text.slice(index, close + 1);
432
+ index = close + 1;
433
+ continue;
434
+ }
435
+ if (/\s/.test(char)) {
436
+ if (current) {
437
+ tokens.push(current);
438
+ current = "";
439
+ }
440
+ index += 1;
441
+ continue;
442
+ }
443
+ current += char;
444
+ index += 1;
445
+ }
446
+ if (current) {
447
+ tokens.push(current);
448
+ }
449
+ return tokens;
450
+ }
451
+ function parentheticalEnd(text) {
452
+ let depth = 0;
453
+ for (let index = 0; index < text.length; index += 1) {
454
+ const char = text[index];
455
+ if (char === "\\") {
456
+ index += 1;
457
+ continue;
458
+ }
459
+ const close = char === "`" ? findCodeSpanEnd(text, index) : char === "{" ? findClosingBrace(text, index) : -1;
460
+ if (close !== -1) {
461
+ index = close;
462
+ } else if (char === "(") {
463
+ depth += 1;
464
+ } else if (char === ")" && --depth === 0) {
465
+ return index;
466
+ }
467
+ }
468
+ return -1;
469
+ }
470
+ function isParenthetical(token) {
471
+ return token.startsWith("(") && parentheticalEnd(token) !== -1;
472
+ }
473
+ function parentheticalGroups(tokens, limit) {
474
+ const groups = [];
475
+ for (let index = 0; index < tokens.length; index += 1) {
476
+ if (tokens[index].startsWith("(")) {
477
+ const end = parentheticalEnd(tokens.slice(index).join(" "));
478
+ let last = index;
479
+ let length = tokens[index].length;
480
+ while (length <= end && last + 1 < tokens.length) {
481
+ length += 1 + tokens[++last].length;
482
+ }
483
+ if (end !== -1 && length <= limit) {
484
+ groups.push({ start: index, end: last + 1 });
485
+ index = last;
486
+ continue;
487
+ }
488
+ }
489
+ }
490
+ return groups;
491
+ }
492
+ function bindParentheticals(tokens, limit) {
493
+ const groups = new Map(parentheticalGroups(tokens, limit).map((group) => [group.start, group.end]));
494
+ const units = [];
495
+ for (let index = 0; index < tokens.length; index += 1) {
496
+ const end = groups.get(index) ?? index + 1;
497
+ units.push(tokens.slice(index, end).join(" "));
498
+ index = end - 1;
499
+ }
500
+ return units;
501
+ }
502
+ function endsClause(token) {
503
+ return CLAUSE_END.test(token) && !isAbbreviation(token);
504
+ }
505
+ function endsSentence(token) {
506
+ return SENTENCE_END.test(token) && !isAbbreviation(token);
507
+ }
508
+ function opensAside(token) {
509
+ return /^[([]/.test(token) && !/[)\]]["'`.,;:!?]*$/.test(token);
510
+ }
511
+ function isOrphan(previous, token) {
512
+ return opensAside(token) || endsClause(previous) && !CLAUSE_END.test(token);
513
+ }
514
+ function startsBlock(token) {
515
+ return /^(?:[-*+>]|\d+[.)]|#{1,6})$/.test(token) || /^(?:@[A-Za-z]|`{3}|~{3}|\||<\/?[A-Za-z])/.test(token);
516
+ }
517
+ function bindLineStarts(tokens, startsLine2 = startsBlock) {
518
+ const units = [];
519
+ for (const [index, token] of tokens.entries()) {
520
+ if (units.length > 0 && startsLine2(token, tokens[index + 1])) {
521
+ units[units.length - 1] += ` ${token}`;
522
+ } else {
523
+ units.push(token);
524
+ }
525
+ }
526
+ return units;
527
+ }
528
+ function breaksBefore(previous, last, next, options) {
529
+ return options.avoidSentenceOrphans && isOrphan(previous, last) || startsBlock(next);
530
+ }
531
+ function lineLength(tokens) {
532
+ let length = 0;
533
+ for (const token of tokens) {
534
+ length += token.length;
535
+ }
536
+ return length + Math.max(0, tokens.length - 1);
537
+ }
538
+ function wrapTokens(tokens, firstLimit, restLimit, options) {
539
+ return wrapFrom(tokens, 0, firstLimit, restLimit, options, Infinity);
540
+ }
541
+ function wrapFrom(tokens, from, firstLimit, restLimit, options, maxLines) {
542
+ const lines = [];
543
+ let line = [];
544
+ let length = 0;
545
+ for (let index = from; index < tokens.length; index += 1) {
546
+ const token = tokens[index];
547
+ while (line.length > 0) {
548
+ const limit = lines.length === 0 ? firstLimit : restLimit;
549
+ const nextLength = length + 1 + token.length;
550
+ const extra = isParenthetical(token) ? options.parentheticalOverflow ?? 0 : 0;
551
+ if (nextLength <= limit + extra) {
552
+ break;
553
+ }
554
+ const carry = [];
555
+ while (line.length >= 2 && breaksBefore(line.at(-2) ?? "", line.at(-1) ?? "", carry[0] ?? token, options)) {
556
+ carry.unshift(line.pop() ?? "");
557
+ }
558
+ lines.push(line);
559
+ if (lines.length >= maxLines) {
560
+ return lines;
561
+ }
562
+ line = carry;
563
+ length = lineLength(carry);
564
+ }
565
+ line.push(token);
566
+ length = lineLength(line);
567
+ }
568
+ if (line.length > 0) {
569
+ lines.push(line);
570
+ }
571
+ return lines;
572
+ }
573
+ var MIN_LAST_LINE_WORDS = 3;
574
+ function hasWidow(lines) {
575
+ const last = lines.at(-1) ?? [];
576
+ const words = last.reduce((count, token) => count + (isParenthetical(token) ? tokenize(token).length : 1), 0);
577
+ return lines.length >= 2 && words < MIN_LAST_LINE_WORDS;
578
+ }
579
+ var FULLNESS_WEIGHT = 0.07;
580
+ var PARENTHETICAL_SPLIT_WEIGHT = 2;
581
+ var MAX_STEP = 15;
582
+ var EXTRA_LINES = 2;
583
+ function fewestLines(starts, target) {
584
+ const total = starts.length - 1;
585
+ const fewest = new Int32Array(total + 1);
586
+ let end = total;
587
+ for (let index = total - 1; index >= 0; index -= 1) {
588
+ while (starts[end] - starts[index] - 1 > target) {
589
+ end -= 1;
590
+ }
591
+ fewest[index] = fewest[end] + 1;
592
+ }
593
+ return fewest;
594
+ }
595
+ function firstAtMost(values, limit, from) {
596
+ let low = from;
597
+ let high = values.length - 1;
598
+ while (low < high) {
599
+ const middle = Math.floor((low + high) / 2);
600
+ if (values[middle] <= limit) {
601
+ high = middle;
602
+ } else {
603
+ low = middle + 1;
604
+ }
605
+ }
606
+ return low;
607
+ }
608
+ function costsOfLine(input, target, line, previous, fewest) {
609
+ const { starts, afterOrphan } = input;
610
+ const offset = line === 1 ? input.offset : 0;
611
+ const linesLeft = input.count - line;
612
+ const last = starts.length - 1 - linesLeft;
613
+ const base = firstAtMost(fewest, linesLeft, Math.max(line, previous.low + 1));
614
+ const costs = [];
615
+ const origins = [];
616
+ let low = -1;
617
+ let high = -1;
618
+ for (let end = base; end <= last; end += 1) {
619
+ const nearest = Math.min(end - 1, previous.high);
620
+ if (end > previous.high && starts[end] - starts[nearest] - 1 + offset > target) {
621
+ break;
622
+ }
623
+ let cost = Infinity;
624
+ let origin = -1;
625
+ if (!input.blockStarts[end]) {
626
+ const splitCost = input.splitCosts.get(end) ?? 0;
627
+ for (let start = nearest; start >= previous.low; start -= 1) {
628
+ const length = starts[end] - starts[start] - 1 + offset;
629
+ if (length > target) {
630
+ break;
631
+ }
632
+ const before = previous.costs[start - previous.base];
633
+ if (before === Infinity || afterOrphan[end] && end - start >= 2) {
634
+ continue;
635
+ }
636
+ const candidate = before + (target - length) ** 2 + splitCost;
637
+ if (candidate <= cost) {
638
+ cost = candidate;
639
+ origin = start;
640
+ }
641
+ }
642
+ }
643
+ costs.push(cost);
644
+ origins.push(origin);
645
+ if (cost !== Infinity) {
646
+ low = low === -1 ? end : low;
647
+ high = end;
648
+ }
649
+ }
650
+ return low === -1 ? void 0 : { base, costs, origins, low, high };
651
+ }
652
+ function balanceTokens(tokens, firstLimit, restLimit, count, options, preferredLimit = restLimit, splitCosts = /* @__PURE__ */ new Map()) {
653
+ const total = tokens.length;
654
+ const starts = [0];
655
+ let longest = -Infinity;
656
+ for (const token of tokens) {
657
+ starts.push((starts.at(-1) ?? 0) + token.length + 1);
658
+ longest = Math.max(longest, token.length);
659
+ }
660
+ const input = {
661
+ starts,
662
+ count,
663
+ offset: restLimit - firstLimit,
664
+ blockStarts: tokens.map((token) => startsBlock(token)),
665
+ afterOrphan: tokens.map((_, index) => {
666
+ return options.avoidSentenceOrphans && index >= 2 && isOrphan(tokens[index - 2], tokens[index - 1]);
667
+ }),
668
+ splitCosts
669
+ };
670
+ const targets = [];
671
+ for (let target = restLimit; target >= Math.max(longest, Math.floor(restLimit / 2)); target -= 1) {
672
+ targets.push(target);
673
+ }
674
+ const most = starts[total] - count + input.offset;
675
+ let best;
676
+ for (const target of targets.toReversed()) {
677
+ const fullness = FULLNESS_WEIGHT * (count - 1) * (preferredLimit - target) ** 2;
678
+ const spare = (count - 1) * target - most;
679
+ const least = count > 1 && spare > 0 ? Math.max(spare ** 2 / (count - 1) - 1, 0) : 0;
680
+ if (best !== void 0 && least + fullness > best.cost) {
681
+ continue;
682
+ }
683
+ const fewest = fewestLines(starts, target);
684
+ const rows = [{ base: 0, costs: [0], origins: [-1], low: 0, high: 0 }];
685
+ for (let line = 1; line < count; line += 1) {
686
+ const row2 = costsOfLine(input, target, line, rows[line - 1], fewest);
687
+ if (!row2) {
688
+ break;
689
+ }
690
+ rows.push(row2);
691
+ }
692
+ if (rows.length < count) {
693
+ continue;
694
+ }
695
+ const row = rows[count - 1];
696
+ for (let start = row.low; start <= row.high; start += 1) {
697
+ const score = row.costs[start - row.base] + fullness;
698
+ const improves = score < (best?.cost ?? Infinity) || score === best?.cost && target > best.target;
699
+ if (!improves || starts[total] - starts[start] - 1 > target || hasWidow([[], tokens.slice(start)])) {
700
+ continue;
701
+ }
702
+ const breaks = [start];
703
+ for (let line = count - 1; line > 1; line -= 1) {
704
+ breaks.unshift(rows[line].origins[breaks[0] - rows[line].base]);
705
+ }
706
+ best = { cost: score, target, breaks };
707
+ }
708
+ }
709
+ if (!best) {
710
+ return void 0;
711
+ }
712
+ const bounds = [0, ...best.breaks, total];
713
+ return bounds.slice(0, -1).map((start, index) => tokens.slice(start, bounds[index + 1]));
714
+ }
715
+ function hasStep(lines, offset = 0) {
716
+ return lines.some((line, index) => {
717
+ if (index === 0) {
718
+ return false;
719
+ }
720
+ const before = lineLength(lines[index - 1]) + (index === 1 ? offset : 0);
721
+ return lineLength(line) - before > MAX_STEP;
722
+ });
723
+ }
724
+ function splitCostsOf(tokens, limit) {
725
+ const splitCosts = /* @__PURE__ */ new Map();
726
+ for (const group of parentheticalGroups(tokens, limit)) {
727
+ const length = lineLength(tokens.slice(group.start, group.end));
728
+ for (let at = group.start + 1; at < group.end; at += 1) {
729
+ splitCosts.set(at, length * PARENTHETICAL_SPLIT_WEIGHT);
730
+ }
731
+ }
732
+ return splitCosts;
733
+ }
734
+ function hasParentheticals(tokens, restLimit, options) {
735
+ if (options.parentheticalOverflow === void 0) {
736
+ return false;
737
+ }
738
+ return bindParentheticals(tokens, restLimit + options.parentheticalOverflow).some((token) => isParenthetical(token));
739
+ }
740
+ function layoutTokens(tokens, firstLimit, restLimit, options) {
741
+ const lines = layoutLines(tokens, firstLimit, restLimit, options);
742
+ const offset = restLimit - firstLimit;
743
+ if (options.keepFirstLine || options.keepSteps || !hasStep(lines, offset)) {
744
+ return lines;
745
+ }
746
+ const overflow = hasParentheticals(tokens, restLimit, options) ? options.parentheticalOverflow ?? 0 : 0;
747
+ const splitCosts = overflow > 0 ? splitCostsOf(tokens, restLimit + overflow) : /* @__PURE__ */ new Map();
748
+ for (let count = lines.length + 1; count <= lines.length + EXTRA_LINES; count += 1) {
749
+ const even = balanceTokens(
750
+ tokens,
751
+ firstLimit + overflow,
752
+ restLimit + overflow,
753
+ count,
754
+ options,
755
+ restLimit,
756
+ splitCosts
757
+ );
758
+ if (even && !hasStep(even, offset)) {
759
+ return even;
760
+ }
761
+ }
762
+ return lines;
763
+ }
764
+ function layoutLines(tokens, firstLimit, restLimit, options) {
765
+ if (options.parentheticalOverflow !== void 0) {
766
+ const grouped = bindParentheticals(tokens, restLimit + options.parentheticalOverflow);
767
+ if (grouped.some((token) => isParenthetical(token))) {
768
+ const minimumLines = Math.max(
769
+ 2,
770
+ Math.ceil((lineLength(tokens) + 1 + restLimit - firstLimit) / Math.max(1, restLimit + 1))
771
+ );
772
+ let preserved = wrapTokens(grouped, firstLimit, restLimit, options);
773
+ if (minimumLines === 2 && preserved.length > 2) {
774
+ const wide = wrapTokens(
775
+ grouped,
776
+ firstLimit + options.parentheticalOverflow,
777
+ restLimit + options.parentheticalOverflow,
778
+ { ...options, parentheticalOverflow: void 0 }
779
+ );
780
+ if (wide.length === 2 && !hasWidow(wide)) {
781
+ preserved = wide;
782
+ }
783
+ }
784
+ if (preserved.length <= 2 && !hasWidow(preserved)) {
785
+ let cursor = 0;
786
+ return preserved.map((line) => {
787
+ const expanded = [];
788
+ const length = lineLength(line);
789
+ let used = -1;
790
+ while (cursor < tokens.length && used < length) {
791
+ const token = tokens[cursor++];
792
+ expanded.push(token);
793
+ used += token.length + 1;
794
+ }
795
+ return expanded;
796
+ });
797
+ }
798
+ const splitCosts = splitCostsOf(tokens, restLimit + options.parentheticalOverflow);
799
+ const preferred = wrapTokens(tokens, firstLimit, restLimit, { ...options, parentheticalOverflow: void 0 });
800
+ for (let count = minimumLines; count <= Math.max(preferred.length + 1, preserved.length); count += 1) {
801
+ const balanced2 = balanceTokens(
802
+ tokens,
803
+ firstLimit + options.parentheticalOverflow,
804
+ restLimit + options.parentheticalOverflow,
805
+ count,
806
+ options,
807
+ restLimit,
808
+ splitCosts
809
+ );
810
+ if (balanced2) {
811
+ return balanced2;
812
+ }
813
+ }
814
+ }
815
+ }
816
+ const greedy = wrapTokens(tokens, firstLimit, restLimit, options);
817
+ if (greedy.length < 2) {
818
+ return greedy;
819
+ }
820
+ if (options.keepFirstLine) {
821
+ const [head] = greedy;
822
+ const rest = layoutTokens(tokens.slice(head.length), restLimit, restLimit, { ...options, keepFirstLine: false });
823
+ const lines = [head, ...rest];
824
+ if (!hasWidow(lines)) {
825
+ return lines;
826
+ }
827
+ }
828
+ const balanced = balanceTokens(tokens, firstLimit, restLimit, greedy.length, options);
829
+ if (balanced) {
830
+ return balanced;
831
+ }
832
+ const stretched = wrapTokens(tokens, firstLimit + options.tolerance, restLimit + options.tolerance, options);
833
+ return hasWidow(stretched) ? greedy : stretched;
834
+ }
835
+ function breakAfterSentence(lines, firstLimit, restLimit, options) {
836
+ if (lines.length !== 2 || !options.avoidSentenceOrphans) {
837
+ return lines;
838
+ }
839
+ const tokens = lines.flat();
840
+ const offset = restLimit - firstLimit;
841
+ for (let end = tokens.length - 1; end > 0; end -= 1) {
842
+ const first = tokens.slice(0, end);
843
+ const second = tokens.slice(end);
844
+ if (endsSentence(tokens[end - 1]) && !hasWidow([first, second]) && !breaksBefore(tokens[end - 2] ?? "", tokens[end - 1], tokens[end], options) && lineLength(first) <= firstLimit && lineLength(second) <= lineLength(first) + offset) {
845
+ return [first, second];
846
+ }
847
+ }
848
+ return lines;
849
+ }
850
+ function findLineIssues(lines, firstLimit, restLimit, options) {
851
+ const issues = [];
852
+ const tokens = lines.flat();
853
+ let next = 0;
854
+ for (const [index, line] of lines.entries()) {
855
+ const limit = index === 0 ? firstLimit : restLimit;
856
+ const length = lineLength(line);
857
+ const start = next;
858
+ next += line.length;
859
+ if (line.length > 1 && length > limit + options.tolerance) {
860
+ issues.push({ type: "overflow", line: index });
861
+ }
862
+ if (index === lines.length - 1 || line.length === 0) {
863
+ continue;
864
+ }
865
+ if (options.avoidSentenceOrphans && line.length >= 2 && isOrphan(line.at(-2) ?? "", line.at(-1) ?? "")) {
866
+ issues.push({ type: "orphan", line: index });
867
+ }
868
+ const [expected] = wrapFrom(tokens, start, limit, restLimit, options, 1);
869
+ if (expected.length > line.length) {
870
+ issues.push({ type: "underfill", line: index });
871
+ }
872
+ }
873
+ return issues;
874
+ }
875
+
876
+ // src/comments/analyze.ts
877
+ var ISSUE_TIERS = {
878
+ overflow: "verdict",
879
+ underfill: "verdict",
880
+ widow: "verdict",
881
+ unbalanced: "verdict",
882
+ orphan: "hint",
883
+ banner: "hint",
884
+ "em-dash": "hint",
885
+ semicolon: "hint",
886
+ "label-colon": "hint",
887
+ emphasis: "hint",
888
+ dash: "hint"
889
+ };
890
+ var IGNORE_DIRECTIVE = "comments-ignore";
891
+ var FENCE = /^(?:`{3,}(?!.*`)|~{3,})/;
892
+ var JSDOC_TAG = /^@([A-Za-z]+)/;
893
+ var NESTED_LIST_MARKER = /^\s+(?:[-*+]|\d+[.)])\s+/;
894
+ var PROSE_TAGS = /* @__PURE__ */ new Set([
895
+ "arg",
896
+ "argument",
897
+ "deprecated",
898
+ "description",
899
+ "exception",
900
+ "note",
901
+ "param",
902
+ "prop",
903
+ "property",
904
+ "remarks",
905
+ "return",
906
+ "returns",
907
+ "see",
908
+ "since",
909
+ "summary",
910
+ "template",
911
+ "throws",
912
+ "todo",
913
+ "typeParam",
914
+ "yields"
915
+ ]);
916
+ var CODE_TAGS = /* @__PURE__ */ new Set(["default", "defaultValue", "example"]);
917
+ var NAMED_TAGS = /* @__PURE__ */ new Set(["arg", "argument", "param", "prop", "property", "template", "typeParam"]);
918
+ var LINE_DIRECTIVE = /^\s*(?:(?:eslint|oxlint|stylelint)-(?:disable(?:-next-line|-line)?|enable)\b|eslint-env\b|@ts-(?:ignore|expect-error|nocheck|check)\b|@jsx\w*\b|@vite-ignore\b|[@#]__[A-Z_]+__|@(?:vitest|jest)-environment\b|(?:istanbul|c8|v8)\s+ignore\b|prettier-ignore\b|biome-ignore\b|deno-(?:lint-ignore|fmt-ignore)(?:-file)?\b|tslint:|(?:c[sS]pell|spell-?checker)\s*:|(?:webpack|turbopack)[A-Z][a-zA-Z]*\s*:|vim?:\s|#(?:region|endregion)\b|<reference\s)/;
919
+ var BLOCK_DIRECTIVE = /^\s*(?:eslint\s|globals?\s|exported\s|jshint\s|jslint\s|type\s*:)/;
920
+ var LEGAL_COMMENT = /^!|@license\b|@preserve\b|\bcopyright\s+(?:\(c\)|©|\d{4})/i;
921
+ var DEFINITION = /^(?:``|[\w$.[\]]+(?:[ -][\w$.[\]]+)?)(?: [-–]|:) \S/;
922
+ var BOX_DRAWING = /[\u2500-\u257F]/;
923
+ var TABLE_DELIMITER = /^\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)+\|?$/;
924
+ var ARROWS_OR_RULES = /[▲▼◀▶→←↑↓]|-->|<--|\+-{2,}|-{3,}|={4,}/;
925
+ var HARD_BREAK = /\S {2,}$|(?<!\\)\\(?:\\\\)*$/;
926
+ function isProseLike(statement, source) {
927
+ if (statement.type === "LabeledStatement") {
928
+ return source[statement.end - 1] !== ";";
929
+ }
930
+ if (statement.type === "EmptyStatement") {
931
+ return true;
932
+ }
933
+ if (statement.type !== "ExpressionStatement") {
934
+ return false;
935
+ }
936
+ let { expression } = statement;
937
+ while (expression.type === "ParenthesizedExpression") {
938
+ expression = expression.expression;
939
+ }
940
+ if (expression.type === "TaggedTemplateExpression") {
941
+ return expression.tag.type === "Identifier";
942
+ }
943
+ return expression.type === "Identifier" || expression.type === "Literal" || expression.type === "TemplateLiteral";
944
+ }
945
+ function parsesAsStatements(text) {
946
+ if (!/[()[\]{}=;<>]/.test(text)) {
947
+ return false;
948
+ }
949
+ const source = `async function* comment() {
950
+ ${text}
951
+ }`;
952
+ const { program, errors } = parseSync("comment.tsx", source);
953
+ const wrapper = program.body.at(0);
954
+ if (errors.length > 0 || wrapper?.type !== "FunctionDeclaration" || !wrapper.body) {
955
+ return false;
956
+ }
957
+ const statements = wrapper.body.body;
958
+ return statements.length > 0 && !statements.every((statement) => isProseLike(statement, source));
959
+ }
960
+ var CODE_VALUES = /* @__PURE__ */ new Set([
961
+ "Literal",
962
+ "ArrayExpression",
963
+ "ObjectExpression",
964
+ "CallExpression",
965
+ "NewExpression",
966
+ "ArrowFunctionExpression",
967
+ "FunctionExpression",
968
+ "UnaryExpression",
969
+ "MemberExpression"
970
+ ]);
971
+ function parsesAsProperties(text) {
972
+ if (!text.includes(":")) {
973
+ return false;
974
+ }
975
+ const { program, errors } = parseSync("comment.ts", `({
976
+ ${text}
977
+ })`);
978
+ const statement = program.body.at(0);
979
+ if (errors.length > 0 || program.body.length !== 1 || statement?.type !== "ExpressionStatement") {
980
+ return false;
981
+ }
982
+ let { expression } = statement;
983
+ while (expression.type === "ParenthesizedExpression") {
984
+ expression = expression.expression;
985
+ }
986
+ if (expression.type !== "ObjectExpression" || expression.properties.length === 0) {
987
+ return false;
988
+ }
989
+ const { properties } = expression;
990
+ if (!properties.every((property) => property.type === "Property" && !property.shorthand && !property.method)) {
991
+ return false;
992
+ }
993
+ return properties.length > 1 || text.trimEnd().endsWith(",") || properties.some((property) => property.type === "Property" && CODE_VALUES.has(property.value.type));
994
+ }
995
+ function isCode(texts) {
996
+ const text = texts.map((line) => line.replace(LIST_MARKER, "")).join("\n");
997
+ return parsesAsStatements(text) || parsesAsProperties(text);
998
+ }
999
+ function isCodeLine(text) {
1000
+ return text.endsWith("{") || text.endsWith(";") && (/[()[\]{}=<>]/.test(text) || !/\s/.test(text)) || /^[}\])]/.test(text) || /^(?:const|let|var)\s+[\w$]+\s*[:=]/.test(text) || /^(?:import|export)\s.*(?:\sfrom\s|[{}])/.test(text);
1001
+ }
1002
+ function layoutReason(prose, raw) {
1003
+ if (prose.startsWith("|") || TABLE_DELIMITER.test(prose)) {
1004
+ return "table row";
1005
+ }
1006
+ if (BOX_DRAWING.test(prose)) {
1007
+ return "box drawing";
1008
+ }
1009
+ if (isBanner(raw)) {
1010
+ return "repeated characters";
1011
+ }
1012
+ return void 0;
1013
+ }
1014
+ function isMarkup(prose) {
1015
+ return /^#{1,6}\s/.test(prose) || /^\[[^\]]+\]:/.test(prose) || /^<\/?[a-z]/i.test(prose) || /^>(?:\s|$)/.test(prose);
1016
+ }
1017
+ function mask(text) {
1018
+ return text.replaceAll(/[^\n]/g, "x");
1019
+ }
1020
+ function maskCode(text) {
1021
+ let masked = text.replaceAll(CODE_SPAN, mask);
1022
+ for (let previous = ""; previous !== masked; ) {
1023
+ previous = masked;
1024
+ masked = masked.replaceAll(/\{[^{}]*\}/g, mask);
1025
+ }
1026
+ return masked;
1027
+ }
1028
+ function hasAlignedColumns(lines) {
1029
+ const masked = maskCode(lines.map((body) => body.text).join("\n")).split("\n");
1030
+ const seen = /* @__PURE__ */ new Set();
1031
+ for (const line of masked) {
1032
+ const columns = new Set([...line.matchAll(/[^\s.!?][^\S\n]{2,}(?=\S)/g)].map((gap) => gap.index + gap[0].length));
1033
+ if ([...columns].some((column) => seen.has(column))) {
1034
+ return true;
1035
+ }
1036
+ for (const column of columns) {
1037
+ seen.add(column);
1038
+ }
1039
+ }
1040
+ return false;
1041
+ }
1042
+ function structureOf(lines) {
1043
+ const trimmed = lines.map((line) => line.text.trim()).join("\n");
1044
+ const texts = replaceKeepingLines(trimmed, CODE_SPAN, "``").split("\n");
1045
+ const proseLines = stripNonProse(trimmed).split("\n");
1046
+ if (isCode(lines.map((line) => line.text.trim()))) {
1047
+ return { tier: "code", reason: "code" };
1048
+ }
1049
+ let layout;
1050
+ let codeLike = false;
1051
+ let manual = lines.slice(0, -1).some((line) => line.hardBreak) ? "hard line break" : void 0;
1052
+ for (const [position, line] of texts.entries()) {
1053
+ const prose = proseLines[position].trim();
1054
+ layout ??= layoutReason(prose, lines[position]?.text ?? "");
1055
+ if (isCodeLine(line.trim())) {
1056
+ codeLike = true;
1057
+ } else if (isMarkup(prose)) {
1058
+ manual ??= "markup";
1059
+ } else if ((prose.match(/\|/g)?.length ?? 0) >= 2) {
1060
+ manual ??= "pipes";
1061
+ } else if (ARROWS_OR_RULES.test(prose)) {
1062
+ manual ??= "arrows or rules";
1063
+ }
1064
+ }
1065
+ layout ??= hasAlignedColumns(lines) ? "aligned columns" : void 0;
1066
+ if (layout) {
1067
+ return { tier: "layout", reason: layout };
1068
+ }
1069
+ if (codeLike) {
1070
+ return { tier: "code-like", reason: "code-like line" };
1071
+ }
1072
+ if (texts.filter((text) => DEFINITION.test(text.trim())).length >= 2) {
1073
+ manual ??= "definition-like lines";
1074
+ }
1075
+ return manual ? { tier: "manual", reason: manual } : void 0;
1076
+ }
1077
+ var FenceTracker = class {
1078
+ _open;
1079
+ isInside() {
1080
+ return this._open !== void 0;
1081
+ }
1082
+ next(text) {
1083
+ const trimmed = text.trim();
1084
+ if (this._open === void 0) {
1085
+ this._open = FENCE.exec(trimmed)?.[0];
1086
+ return this._open !== void 0;
1087
+ }
1088
+ if (trimmed.startsWith(this._open) && new RegExp(`^\\${this._open[0]}+$`).test(trimmed)) {
1089
+ this._open = void 0;
1090
+ }
1091
+ return true;
1092
+ }
1093
+ };
1094
+ function segment(lines) {
1095
+ const paragraphs = [];
1096
+ const fence = new FenceTracker();
1097
+ let current;
1098
+ let inCodeTag = false;
1099
+ const flush = () => {
1100
+ if (current) {
1101
+ while (current.lines.length > 1 && !current.lines.at(-1)?.text.trim()) {
1102
+ current.lines.pop();
1103
+ }
1104
+ paragraphs.push(current);
1105
+ current = void 0;
1106
+ }
1107
+ };
1108
+ const start = (kind, line, marker = "", reason) => {
1109
+ flush();
1110
+ current = { kind, lines: [line], marker, reason };
1111
+ };
1112
+ for (const line of lines) {
1113
+ const { text } = line;
1114
+ if (inCodeTag) {
1115
+ if (fence.isInside() || !JSDOC_TAG.test(text)) {
1116
+ fence.next(text);
1117
+ current?.lines.push(line);
1118
+ continue;
1119
+ }
1120
+ inCodeTag = false;
1121
+ }
1122
+ if (fence.isInside()) {
1123
+ current?.lines.push(line);
1124
+ fence.next(text);
1125
+ if (!fence.isInside()) {
1126
+ flush();
1127
+ }
1128
+ continue;
1129
+ }
1130
+ if (line.verbatim) {
1131
+ start("code", line, "", "directive");
1132
+ flush();
1133
+ continue;
1134
+ }
1135
+ if (!text.trim()) {
1136
+ flush();
1137
+ continue;
1138
+ }
1139
+ if (fence.next(text)) {
1140
+ start("code", line, "", "code block");
1141
+ continue;
1142
+ }
1143
+ if (/^\s/.test(text)) {
1144
+ const holdsProse = current?.kind === "list" || current?.kind === "tag" && !current.reason;
1145
+ const nested = holdsProse ? NESTED_LIST_MARKER.exec(text) : null;
1146
+ if (nested) {
1147
+ start("list", line, nested[0]);
1148
+ } else if (current && (current.kind === "list" || current.kind === "tag" || current.reason === "indented")) {
1149
+ current.lines.push(line);
1150
+ } else {
1151
+ start("code", line, "", "indented");
1152
+ }
1153
+ continue;
1154
+ }
1155
+ const tag = JSDOC_TAG.exec(text);
1156
+ if (tag) {
1157
+ start("tag", line, "", PROSE_TAGS.has(tag[1]) ? void 0 : `@${tag[1]} tag`);
1158
+ inCodeTag = CODE_TAGS.has(tag[1]);
1159
+ continue;
1160
+ }
1161
+ const marker = LIST_MARKER.exec(text);
1162
+ if (marker) {
1163
+ start("list", line, marker[0]);
1164
+ continue;
1165
+ }
1166
+ if (current && (current.kind === "prose" || current.kind === "tag" || current.kind === "list")) {
1167
+ current.lines.push(line);
1168
+ continue;
1169
+ }
1170
+ start("prose", line);
1171
+ }
1172
+ flush();
1173
+ return paragraphs;
1174
+ }
1175
+ function startsLine(token, next) {
1176
+ return startsBlock(token) || LINE_DIRECTIVE.test(next === void 0 ? token : `${token} ${next}`);
1177
+ }
1178
+ function visualLength(text, tabWidth) {
1179
+ return text.replaceAll(" ", " ".repeat(tabWidth)).length;
1180
+ }
1181
+ function sourceLineLength(line, tabWidth) {
1182
+ return visualLength(`${line.prefix}${line.text.trimEnd()}`, tabWidth);
1183
+ }
1184
+ function overflowIssues(lines, options) {
1185
+ const issues = [];
1186
+ const limit = options.maxWidth ?? options.width + options.tolerance;
1187
+ for (const line of lines) {
1188
+ const length = sourceLineLength(line, options.tabWidth);
1189
+ if (length <= limit) {
1190
+ continue;
1191
+ }
1192
+ if (options.maxWidth !== void 0) {
1193
+ issues.push(lineIssue("overflow", line, options));
1194
+ continue;
1195
+ }
1196
+ const prefixLength = visualLength(`${line.prefix}${/^\s*/.exec(line.text)?.[0] ?? ""}`, options.tabWidth);
1197
+ const tokens = bindLineStarts(tokenize(line.text), startsLine);
1198
+ if (tokens.length > 1 && tokens.every((token) => prefixLength + token.length <= limit)) {
1199
+ issues.push(lineIssue("overflow", line, options));
1200
+ }
1201
+ }
1202
+ return issues;
1203
+ }
1204
+ function lineIssue(type, line, options) {
1205
+ return { type, tier: ISSUE_TIERS[type], line: line.index + 1, length: sourceLineLength(line, options.tabWidth) };
1206
+ }
1207
+ function stripBlockSyntax(text) {
1208
+ return text.replace(/^\s*\/\*+/, "").replace(/\*+\/\s*$/, "").replace(/^\s*\*(?=\s|$)/, "");
1209
+ }
1210
+ function bannerIssues(paragraph, options) {
1211
+ const verbatim = ["code block", "indented", "legal comment"];
1212
+ if (!options.style || paragraph.reason !== void 0 && verbatim.includes(paragraph.reason)) {
1213
+ return [];
1214
+ }
1215
+ const banners = paragraph.lines.filter((line) => {
1216
+ const text = paragraph.kind === "other" ? stripBlockSyntax(line.text) : line.text;
1217
+ return isBanner(text) || paragraph.kind === "other" && /^\s*\/\*{3,}|\*{3,}\/\s*$/.test(line.text);
1218
+ });
1219
+ return banners.map((line) => lineIssue("banner", line, options));
1220
+ }
1221
+ function styleIssues(paragraph, options) {
1222
+ if (!options.style) {
1223
+ return [];
1224
+ }
1225
+ const byIndex = new Map(paragraph.lines.map((line) => [line.index, line]));
1226
+ const lines = paragraph.lines.map((line) => {
1227
+ return { index: line.index, text: paragraph.kind === "other" ? stripBlockSyntax(line.text) : line.text };
1228
+ });
1229
+ return findStyleIssues(lines).flatMap((issue) => {
1230
+ const line = byIndex.get(issue.index);
1231
+ return line ? [lineIssue(issue.type, line, options)] : [];
1232
+ });
1233
+ }
1234
+ function bindTagHead(tokens) {
1235
+ let end = 1;
1236
+ if (tokens[end]?.startsWith("{")) {
1237
+ end += 1;
1238
+ }
1239
+ if (NAMED_TAGS.has(tokens[0].slice(1)) && end < tokens.length) {
1240
+ end += 1;
1241
+ }
1242
+ if (tokens[end] === "-") {
1243
+ end += 1;
1244
+ }
1245
+ return [tokens.slice(0, end).join(" "), ...tokens.slice(end)];
1246
+ }
1247
+ function keptIssues(paragraph, structure, options) {
1248
+ const tier = structure?.tier ?? "manual";
1249
+ return byLine([
1250
+ ...tier === "manual" || tier === "code-like" ? overflowIssues(paragraph.lines, options) : [],
1251
+ ...tier === "code" || tier === "code-like" ? [] : styleIssues(paragraph, options),
1252
+ ...bannerIssues(paragraph, options)
1253
+ ]);
1254
+ }
1255
+ function byLine(issues) {
1256
+ const unique = new Map(issues.map((issue) => [`${issue.line}:${issue.type}`, issue]));
1257
+ return [...unique.values()].toSorted((a, b) => a.line - b.line);
1258
+ }
1259
+ function sameLayout(a, b) {
1260
+ return a.length === b.length && a.every((words, position) => words.join(" ") === b[position].join(" "));
1261
+ }
1262
+ function analyzeParagraph(paragraph, options) {
1263
+ const first = paragraph.lines[0];
1264
+ const last = paragraph.lines.at(-1) ?? first;
1265
+ const base = { kind: paragraph.kind, startLine: first.index + 1, endLine: last.index + 1 };
1266
+ if (paragraph.kind === "code") {
1267
+ return {
1268
+ ...base,
1269
+ reflowable: false,
1270
+ reason: paragraph.reason,
1271
+ issues: bannerIssues(paragraph, options),
1272
+ suggestion: null
1273
+ };
1274
+ }
1275
+ if (paragraph.kind === "tag" && paragraph.reason) {
1276
+ const codeTag = CODE_TAGS.has(JSDOC_TAG.exec(first.text)?.[1] ?? "");
1277
+ const issues2 = codeTag ? [] : overflowIssues(paragraph.lines, options);
1278
+ return { ...base, reflowable: false, reason: paragraph.reason, issues: issues2, suggestion: null };
1279
+ }
1280
+ if (paragraph.kind === "other") {
1281
+ const fence = new FenceTracker();
1282
+ const prose = {
1283
+ ...paragraph,
1284
+ lines: paragraph.lines.filter((line) => !fence.next(stripBlockSyntax(line.text)))
1285
+ };
1286
+ const structure2 = structureOf(
1287
+ prose.lines.map((line) => {
1288
+ return { ...line, text: stripBlockSyntax(line.text) };
1289
+ })
1290
+ );
1291
+ const issues2 = keptIssues(prose, structure2, options);
1292
+ return { ...base, reflowable: false, reason: paragraph.reason, issues: issues2, suggestion: null };
1293
+ }
1294
+ const keep = (structure2) => {
1295
+ const issues2 = keptIssues(paragraph, structure2, options);
1296
+ return { ...base, reflowable: false, reason: structure2.reason, issues: issues2, suggestion: null };
1297
+ };
1298
+ const structure = structureOf(paragraph.lines);
1299
+ if (structure) {
1300
+ return keep(structure);
1301
+ }
1302
+ const prefix = first.prefix;
1303
+ const prefixLength = visualLength(prefix, options.tabWidth);
1304
+ const hang = " ".repeat(paragraph.marker.length);
1305
+ const firstLimit = options.width - prefixLength - paragraph.marker.length;
1306
+ const restLimit = options.width - prefixLength - hang.length;
1307
+ const currentLines = paragraph.lines.map(
1308
+ (line, position) => bindLineStarts(tokenize(position === 0 ? line.text.slice(paragraph.marker.length) : line.text), startsLine)
1309
+ );
1310
+ const tokens = tokenize(currentLines.flat().join(" "));
1311
+ const layoutOptions = {
1312
+ ...options,
1313
+ tolerance: options.maxWidth === void 0 ? options.tolerance : Math.min(options.tolerance, options.maxWidth - options.width),
1314
+ parentheticalOverflow: options.maxWidth === void 0 ? void 0 : options.maxWidth - options.width,
1315
+ keepFirstLine: paragraph.kind === "tag"
1316
+ };
1317
+ const layOut = (from, settings) => {
1318
+ const even2 = layoutTokens(from, firstLimit, restLimit, settings);
1319
+ return { even: even2, wrapped: breakAfterSentence(even2, firstLimit, restLimit, settings) };
1320
+ };
1321
+ const units = bindLineStarts(paragraph.kind === "tag" ? bindTagHead(tokens) : tokens, startsLine);
1322
+ const hardLimit = restLimit + (options.maxWidth === void 0 ? options.tolerance : options.maxWidth - options.width);
1323
+ const written = currentLines.map((words) => ` ${words.join(" ")} `);
1324
+ const staysInLimit = (unit) => {
1325
+ return unit.length <= hardLimit || written.some((line) => line.includes(` ${unit} `));
1326
+ };
1327
+ if (!units.every((unit) => staysInLimit(unit))) {
1328
+ return keep({ tier: "manual", reason: "unbreakable text across lines" });
1329
+ }
1330
+ const bodyOf = (lines) => {
1331
+ return lines.map((words, position) => {
1332
+ const text = `${position === 0 ? paragraph.marker : hang}${words.join(" ")}`;
1333
+ return { ...first, text, verbatim: false, hardBreak: HARD_BREAK.test(text) };
1334
+ });
1335
+ };
1336
+ const settle = (settings) => {
1337
+ let current = units;
1338
+ let { even: even2, wrapped: wrapped2 } = layOut(current, settings);
1339
+ for (; ; ) {
1340
+ const body = bodyOf(wrapped2);
1341
+ const rewrap = structureOf(body);
1342
+ if (!rewrap) {
1343
+ return { even: even2, wrapped: wrapped2 };
1344
+ }
1345
+ const tipping = body.findIndex((_, position) => structureOf(body.slice(0, position + 1)) !== void 0);
1346
+ const start = wrapped2.slice(0, Math.max(tipping, 1)).flat().length;
1347
+ const pair = `${current[start - 1]} ${current[start]}`;
1348
+ if (body.length < 2 || !staysInLimit(pair)) {
1349
+ const reading = rewrap.tier === "code-like" ? "code" : rewrap.reason;
1350
+ return { tier: "manual", reason: `rewrap would read as ${reading}` };
1351
+ }
1352
+ current = current.toSpliced(start - 1, 2, pair);
1353
+ ({ even: even2, wrapped: wrapped2 } = layOut(current, settings));
1354
+ }
1355
+ };
1356
+ const settled = settle(layoutOptions);
1357
+ const layout = "wrapped" in settled ? settled : settle({ ...layoutOptions, keepSteps: true });
1358
+ if (!("wrapped" in layout)) {
1359
+ return keep(layout);
1360
+ }
1361
+ const { even, wrapped } = layout;
1362
+ const matchesLayout = sameLayout(wrapped, currentLines);
1363
+ const lineIssues = [];
1364
+ const overflow = options.maxWidth === void 0 ? [] : overflowIssues(paragraph.lines, options);
1365
+ if (!matchesLayout) {
1366
+ const hasExtraLines = currentLines.length > wrapped.length;
1367
+ if (options.maxWidth === void 0) {
1368
+ overflow.push(...overflowIssues(paragraph.lines, options));
1369
+ }
1370
+ lineIssues.push(
1371
+ ...findLineIssues(currentLines, firstLimit, restLimit, layoutOptions).filter(
1372
+ (issue) => issue.type !== "overflow" && (issue.type !== "underfill" || hasExtraLines)
1373
+ )
1374
+ );
1375
+ if (hasWidow(currentLines) && !hasWidow(wrapped)) {
1376
+ lineIssues.push({ type: "widow", line: currentLines.length - 1 });
1377
+ }
1378
+ if (lineIssues.length === 0 && overflow.length === 0 && (currentLines.length === wrapped.length || options.maxWidth !== void 0)) {
1379
+ const line = wrapped.findIndex((words, position) => words.join(" ") !== currentLines[position]?.join(" "));
1380
+ lineIssues.push({ type: sameLayout(even, currentLines) ? "orphan" : "unbalanced", line: Math.max(line, 0) });
1381
+ }
1382
+ }
1383
+ const suggestion = wrapped.map(
1384
+ (words, position) => `${prefix}${position === 0 ? paragraph.marker : hang}${words.join(" ")}`
1385
+ );
1386
+ const wrapIssues = lineIssues.map((issue) => lineIssue(issue.type, paragraph.lines[issue.line], options));
1387
+ const issues = byLine([...overflow, ...wrapIssues, ...styleIssues(paragraph, options)]);
1388
+ return { ...base, reflowable: true, issues, suggestion };
1389
+ }
1390
+ function computeLineStarts(source) {
1391
+ const starts = [0];
1392
+ for (let index = 0; index < source.length; index += 1) {
1393
+ if (source.codePointAt(index) === 10) {
1394
+ starts.push(index + 1);
1395
+ }
1396
+ }
1397
+ return starts;
1398
+ }
1399
+ function lineOf(lineStarts, offset) {
1400
+ let low = 0;
1401
+ let high = lineStarts.length - 1;
1402
+ while (low < high) {
1403
+ const middle = Math.ceil((low + high) / 2);
1404
+ if (lineStarts[middle] <= offset) {
1405
+ low = middle;
1406
+ } else {
1407
+ high = middle - 1;
1408
+ }
1409
+ }
1410
+ return low;
1411
+ }
1412
+ function stripCarriageReturn(line) {
1413
+ return line.endsWith("\r") ? line.slice(0, -1) : line;
1414
+ }
1415
+ function shapeBlock(comment, sourceLines, lineStarts) {
1416
+ const startLine = lineOf(lineStarts, comment.start);
1417
+ const endLine = lineOf(lineStarts, comment.end - 1);
1418
+ const kind = comment.value.startsWith("*") ? "jsdoc" : "block";
1419
+ const lines = sourceLines.slice(startLine, endLine + 1);
1420
+ const wholeComment = (paragraphKind, reason) => {
1421
+ const otherLines = lines.map((text, offset) => {
1422
+ return { index: startLine + offset, prefix: "", text, verbatim: false, hardBreak: false };
1423
+ });
1424
+ return { kind, startLine, endLine, paragraphs: [{ kind: paragraphKind, marker: "", reason, lines: otherLines }] };
1425
+ };
1426
+ const value = comment.value.replace(/^\*/, "");
1427
+ if (LEGAL_COMMENT.test(replaceKeepingLines(comment.value, CODE_SPAN, "``"))) {
1428
+ return wholeComment("code", "legal comment");
1429
+ }
1430
+ if (LINE_DIRECTIVE.test(value) || BLOCK_DIRECTIVE.test(value)) {
1431
+ return wholeComment("code", "directive");
1432
+ }
1433
+ const other = () => {
1434
+ return wholeComment(
1435
+ "other",
1436
+ startLine === endLine ? "single-line block comment" : "unsupported block comment layout"
1437
+ );
1438
+ };
1439
+ if (startLine === endLine || !/^\s*\/\*\*?$/.test(lines[0]) || !/^\s*\*\/$/.test(lines.at(-1) ?? "")) {
1440
+ return other();
1441
+ }
1442
+ const body = [];
1443
+ const inner = lines.slice(1, -1);
1444
+ for (const [offset, line] of inner.entries()) {
1445
+ const match = /^(\s*\*)( ?)(.*)$/.exec(line);
1446
+ if (!match) {
1447
+ return other();
1448
+ }
1449
+ const [, star, space, text] = match;
1450
+ body.push({
1451
+ index: startLine + 1 + offset,
1452
+ prefix: `${star} `,
1453
+ text: text.trimEnd(),
1454
+ verbatim: !space && text.length > 0 || LINE_DIRECTIVE.test(text),
1455
+ hardBreak: HARD_BREAK.test(text)
1456
+ });
1457
+ }
1458
+ return { kind, startLine, endLine, paragraphs: segment(body) };
1459
+ }
1460
+ function ownsLines(comment, source, lineStarts) {
1461
+ const startLine = lineOf(lineStarts, comment.start);
1462
+ const before = source.slice(lineStarts[startLine], comment.start);
1463
+ const endLine = lineOf(lineStarts, comment.end - 1);
1464
+ const lineEnd = lineStarts[endLine + 1] ?? source.length;
1465
+ const after = source.slice(comment.end, lineEnd);
1466
+ return /^[\t ]*$/.test(before) && /^[\t ]*\r?\n?$/.test(after);
1467
+ }
1468
+ function singleLineLength(paragraph, options) {
1469
+ const [first] = paragraph.lines;
1470
+ if (paragraph.kind === "other" && paragraph.reason === "single-line block comment") {
1471
+ return sourceLineLength(first, options.tabWidth);
1472
+ }
1473
+ if (paragraph.kind !== "prose" || structureOf(paragraph.lines)) {
1474
+ return Infinity;
1475
+ }
1476
+ const text = tokenize(paragraph.lines.map((line) => line.text).join(" ")).join(" ");
1477
+ return visualLength(first.prefix, options.tabWidth) + text.length;
1478
+ }
1479
+ var BYTE_ORDER_MARK = "\uFEFF";
1480
+ function parseSource(filename, source) {
1481
+ return parseSync(filename, source, filename.endsWith(".js") ? { lang: "jsx" } : void 0);
1482
+ }
1483
+ function describeFailure(errors, lineStarts) {
1484
+ const [first] = errors;
1485
+ const offset = first.labels.at(0)?.start;
1486
+ return { message: first.message, line: offset === void 0 ? void 0 : lineOf(lineStarts, offset) + 1 };
1487
+ }
1488
+ function inspectSource(source, filename, options) {
1489
+ if (options.maxWidth !== void 0 && (!Number.isInteger(options.maxWidth) || options.maxWidth < options.width)) {
1490
+ throw new Error("comments: maxWidth must be an integer at least as large as width");
1491
+ }
1492
+ const text = source.startsWith(BYTE_ORDER_MARK) ? source.slice(1) : source;
1493
+ const { program, comments, errors } = parseSource(filename, text);
1494
+ const lineStarts = computeLineStarts(text);
1495
+ if (errors.length > 0 && program.body.length === 0) {
1496
+ return { comments: [], syntaxError: describeFailure(errors, lineStarts) };
1497
+ }
1498
+ const sourceLines = text.split("\n").map((line) => stripCarriageReturn(line));
1499
+ const parsed = comments.filter((comment) => !(comment.start === 0 && text.startsWith("#!")));
1500
+ const reports = [];
1501
+ const blocks = parsed.filter((comment) => {
1502
+ return comment.type === "Block" && ownsLines(comment, text, lineStarts) && !comment.value.includes(IGNORE_DIRECTIVE);
1503
+ });
1504
+ for (const block of blocks) {
1505
+ const shape = shapeBlock(block, sourceLines, lineStarts);
1506
+ const singleLineWidth = Math.max(
1507
+ options.width,
1508
+ Math.min(options.singleLineWidth ?? options.width, options.maxWidth ?? Infinity)
1509
+ );
1510
+ const paragraphOptions = shape.paragraphs.length === 1 && singleLineLength(shape.paragraphs[0], options) <= singleLineWidth ? { ...options, width: singleLineWidth } : options;
1511
+ const lines = sourceLines.slice(shape.startLine, shape.endLine + 1);
1512
+ reports.push({
1513
+ file: filename,
1514
+ kind: shape.kind,
1515
+ startLine: shape.startLine + 1,
1516
+ endLine: shape.endLine + 1,
1517
+ indent: /^[\t ]*/.exec(lines[0])?.[0] ?? "",
1518
+ width: options.width,
1519
+ maxWidth: options.maxWidth,
1520
+ lengths: lines.map((line) => visualLength(line.trimEnd(), options.tabWidth)),
1521
+ text: lines.join("\n"),
1522
+ paragraphs: shape.paragraphs.map((paragraph) => analyzeParagraph(paragraph, paragraphOptions))
1523
+ });
1524
+ }
1525
+ return { comments: reports };
1526
+ }
1527
+ function isSuspicious(report) {
1528
+ return report.paragraphs.some((paragraph) => paragraph.issues.length > 0);
1529
+ }
1530
+ function hasVerdictIssues(report) {
1531
+ return report.paragraphs.some((paragraph) => paragraph.issues.some((issue) => issue.tier === "verdict"));
1532
+ }
1533
+ function rewrapComment(report) {
1534
+ const lines = report.text.split("\n");
1535
+ const paragraphs = report.paragraphs.filter((paragraph) => paragraph.suggestion !== null).toSorted((a, b) => b.startLine - a.startLine);
1536
+ for (const paragraph of paragraphs) {
1537
+ const start = paragraph.startLine - report.startLine;
1538
+ lines.splice(start, paragraph.endLine - paragraph.startLine + 1, ...paragraph.suggestion ?? []);
1539
+ }
1540
+ return lines.join("\n");
1541
+ }
1542
+ var POSITION_KEYS = /* @__PURE__ */ new Set(["start", "end", "range", "loc"]);
1543
+ function codeFingerprint(source, filename) {
1544
+ const { program, errors } = parseSource(filename, source);
1545
+ if (errors.length > 0) {
1546
+ return JSON.stringify({ source, errors: errors.map((error) => error.message) });
1547
+ }
1548
+ return JSON.stringify(
1549
+ program,
1550
+ (key, value) => POSITION_KEYS.has(key) || typeof value === "bigint" ? void 0 : value
1551
+ );
1552
+ }
1553
+
1554
+ // src/comments/config.ts
1555
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
1556
+ import { createRequire } from "module";
1557
+ import path2 from "path";
1558
+ import { pathToFileURL } from "url";
1559
+ var DEFAULT_EXTENSIONS = Object.freeze([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"]);
1560
+ var CONFIG_FILES = [
1561
+ "comments.config.ts",
1562
+ "comments.config.mts",
1563
+ "comments.config.js",
1564
+ "comments.config.mjs",
1565
+ "comments.config.cjs",
1566
+ ".commentsrc",
1567
+ ".commentsrc.json"
1568
+ ];
1569
+ var PACKAGE_JSON_KEY = "comments";
1570
+ function readJson(filepath) {
1571
+ try {
1572
+ return JSON.parse(readFileSync2(filepath, "utf8"));
1573
+ } catch (error) {
1574
+ throw new Error(`comments: failed to parse ${filepath}: ${error.message}`, { cause: error });
1575
+ }
1576
+ }
1577
+ async function readConfigFile(filepath) {
1578
+ if (filepath.endsWith("rc") || filepath.endsWith(".json")) {
1579
+ return readJson(filepath);
1580
+ }
1581
+ const module = await import(pathToFileURL(filepath).href);
1582
+ return module.default ?? module;
1583
+ }
1584
+ function assertType(value, type, key, filepath) {
1585
+ const actual = Array.isArray(value) ? "array" : typeof value;
1586
+ if (value !== void 0 && actual !== type) {
1587
+ throw new Error(`comments: \`${key}\` in ${filepath} must be a ${type}, got ${actual}`);
1588
+ }
1589
+ }
1590
+ function assertStrings(value, key, filepath) {
1591
+ if (Array.isArray(value) && !value.every((item) => typeof item === "string")) {
1592
+ throw new Error(`comments: \`${key}\` in ${filepath} must only hold strings`);
1593
+ }
1594
+ }
1595
+ function validateConfig(raw, filepath) {
1596
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
1597
+ throw new Error(`comments: ${filepath} must export an object`);
1598
+ }
1599
+ const config = raw;
1600
+ assertType(config.width, "number", "width", filepath);
1601
+ assertType(config.maxWidth, "number", "maxWidth", filepath);
1602
+ assertType(config.singleLineWidth, "number", "singleLineWidth", filepath);
1603
+ assertType(config.tolerance, "number", "tolerance", filepath);
1604
+ assertType(config.avoidSentenceOrphans, "boolean", "avoidSentenceOrphans", filepath);
1605
+ assertType(config.style, "boolean", "style", filepath);
1606
+ assertType(config.extensions, "array", "extensions", filepath);
1607
+ assertType(config.ignore, "array", "ignore", filepath);
1608
+ assertType(config.agent, "object", "agent", filepath);
1609
+ assertType(config.hook, "object", "hook", filepath);
1610
+ assertStrings(config.extensions, "extensions", filepath);
1611
+ assertStrings(config.ignore, "ignore", filepath);
1612
+ for (const key of ["width", "maxWidth", "singleLineWidth", "tolerance"]) {
1613
+ const value = config[key];
1614
+ const minimum = key === "tolerance" ? 0 : 1;
1615
+ if (value !== void 0 && (!Number.isInteger(value) || value < minimum)) {
1616
+ throw new Error(`comments: \`${key}\` in ${filepath} must be an integer of at least ${minimum}`);
1617
+ }
1618
+ }
1619
+ if (typeof config.width === "number" && typeof config.maxWidth === "number" && config.maxWidth < config.width) {
1620
+ throw new Error(`comments: \`maxWidth\` in ${filepath} must be at least \`width\``);
1621
+ }
1622
+ const agent = config.agent ?? {};
1623
+ if (agent.runner !== void 0 && !["claude", "codex", "cursor"].includes(agent.runner)) {
1624
+ throw new Error(`comments: \`agent.runner\` in ${filepath} must be one of claude, codex, cursor`);
1625
+ }
1626
+ assertType(agent.model, "string", "agent.model", filepath);
1627
+ assertType(agent.args, "array", "agent.args", filepath);
1628
+ assertType(agent.command, "array", "agent.command", filepath);
1629
+ assertType(agent.timeout, "number", "agent.timeout", filepath);
1630
+ assertStrings(agent.args, "agent.args", filepath);
1631
+ assertStrings(agent.command, "agent.command", filepath);
1632
+ if (agent.timeout !== void 0 && !(agent.timeout > 0)) {
1633
+ throw new Error(`comments: \`agent.timeout\` in ${filepath} must be a positive number`);
1634
+ }
1635
+ for (const key of ["batchSize", "maxFiles", "maxAgents"]) {
1636
+ const value = agent[key];
1637
+ if (value !== void 0 && (!Number.isInteger(value) || value < 1)) {
1638
+ throw new Error(`comments: \`agent.${key}\` in ${filepath} must be a positive integer`);
1639
+ }
1640
+ }
1641
+ assertType(agent.rules, "string", "agent.rules", filepath);
1642
+ assertType(agent.instructions, "string", "agent.instructions", filepath);
1643
+ if (agent.scope !== void 0 && agent.scope !== "suspicious" && agent.scope !== "all") {
1644
+ throw new Error(`comments: \`agent.scope\` in ${filepath} must be "suspicious" or "all"`);
1645
+ }
1646
+ const hook = config.hook ?? {};
1647
+ if (hook.mode !== void 0 && !["agent", "report", "off"].includes(hook.mode)) {
1648
+ throw new Error(`comments: \`hook.mode\` in ${filepath} must be one of agent, report, off`);
1649
+ }
1650
+ return config;
1651
+ }
1652
+ async function loadConfig(cwd, options = {}) {
1653
+ if (options.explicitPath) {
1654
+ const filepath = path2.resolve(cwd, options.explicitPath);
1655
+ return { config: validateConfig(await readConfigFile(filepath), filepath), filepath };
1656
+ }
1657
+ let directory = path2.resolve(cwd);
1658
+ for (; ; ) {
1659
+ for (const name of CONFIG_FILES) {
1660
+ const filepath = path2.join(directory, name);
1661
+ if (existsSync(filepath)) {
1662
+ return { config: validateConfig(await readConfigFile(filepath), filepath), filepath };
1663
+ }
1664
+ }
1665
+ const packageJsonPath = path2.join(directory, "package.json");
1666
+ if (existsSync(packageJsonPath)) {
1667
+ const value = readJson(packageJsonPath)[PACKAGE_JSON_KEY];
1668
+ if (value !== void 0) {
1669
+ return { config: validateConfig(value, packageJsonPath), filepath: packageJsonPath };
1670
+ }
1671
+ }
1672
+ const parent = path2.dirname(directory);
1673
+ if (directory === options.stopDir || parent === directory) {
1674
+ return { config: {} };
1675
+ }
1676
+ directory = parent;
1677
+ }
1678
+ }
1679
+ var PRETTIER_FILES = [".prettierrc", ".prettierrc.json", ".prettierrc.json5"];
1680
+ function readPrettierFile(directory, stopDir) {
1681
+ let current = directory;
1682
+ for (; ; ) {
1683
+ for (const name of PRETTIER_FILES) {
1684
+ const filepath = path2.join(current, name);
1685
+ if (!existsSync(filepath)) {
1686
+ continue;
1687
+ }
1688
+ const text = readFileSync2(filepath, "utf8");
1689
+ const printWidth = /["']?printWidth["']?\s*:\s*(\d+)/.exec(text)?.[1];
1690
+ const tabWidth = /["']?tabWidth["']?\s*:\s*(\d+)/.exec(text)?.[1];
1691
+ return {
1692
+ printWidth: printWidth === void 0 ? void 0 : Number(printWidth),
1693
+ tabWidth: tabWidth === void 0 ? void 0 : Number(tabWidth)
1694
+ };
1695
+ }
1696
+ const packageJsonPath = path2.join(current, "package.json");
1697
+ if (existsSync(packageJsonPath)) {
1698
+ const prettier = readJson(packageJsonPath).prettier;
1699
+ if (prettier && typeof prettier === "object") {
1700
+ return prettier;
1701
+ }
1702
+ }
1703
+ const parent = path2.dirname(current);
1704
+ if (current === stopDir || parent === current) {
1705
+ return void 0;
1706
+ }
1707
+ current = parent;
1708
+ }
1709
+ }
1710
+ var PrettierOptionsResolver = class {
1711
+ constructor(_root) {
1712
+ this._root = _root;
1713
+ }
1714
+ _root;
1715
+ _prettier;
1716
+ _cache = /* @__PURE__ */ new Map();
1717
+ async _loadPrettier() {
1718
+ if (this._prettier !== void 0) {
1719
+ return this._prettier;
1720
+ }
1721
+ try {
1722
+ const require2 = createRequire(path2.join(this._root, "package.json"));
1723
+ const resolved = require2.resolve("prettier");
1724
+ this._prettier = await import(pathToFileURL(resolved).href);
1725
+ } catch {
1726
+ this._prettier = null;
1727
+ }
1728
+ return this._prettier;
1729
+ }
1730
+ async resolve(file) {
1731
+ const prettier = await this._loadPrettier();
1732
+ if (prettier) {
1733
+ try {
1734
+ const options2 = await prettier.resolveConfig(file);
1735
+ if (options2) {
1736
+ return options2;
1737
+ }
1738
+ } catch {
1739
+ }
1740
+ }
1741
+ const directory = path2.dirname(file);
1742
+ const cached = this._cache.get(directory);
1743
+ if (cached) {
1744
+ return cached;
1745
+ }
1746
+ const options = readPrettierFile(directory, this._root) ?? {};
1747
+ this._cache.set(directory, options);
1748
+ return options;
1749
+ }
1750
+ };
1751
+
1752
+ // src/comments/flow.ts
1753
+ import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
1754
+ var DEFAULT_BATCH_SIZE = 20;
1755
+ var DEFAULT_MAX_FILES = 10;
1756
+ var DEFAULT_MAX_AGENTS = 6;
1757
+ var PROGRESS_INTERVAL_MS = 3e4;
1758
+ var LISTED_FILES = 3;
1759
+ function withVerdictIssues(analyses) {
1760
+ return analyses.flatMap((analysis) => analysis.comments).filter((comment) => hasVerdictIssues(comment));
1761
+ }
1762
+ function skippedFiles(analyses) {
1763
+ return analyses.flatMap(({ file, syntaxError }) => syntaxError ? [{ file, syntaxError }] : []);
1764
+ }
1765
+ function describeSkipped({ file, syntaxError }) {
1766
+ const line = syntaxError.line === void 0 ? "" : ` on line ${syntaxError.line}`;
1767
+ return `skipped ${file} because of a syntax error${line} (${syntaxError.message})`;
1768
+ }
1769
+ function describeChangedElsewhere(files) {
1770
+ return `${files.length} file(s) that no agent was given changed during the run: ${listFiles(files)}`;
1771
+ }
1772
+ function changedBetween(before, after) {
1773
+ const files = /* @__PURE__ */ new Set([...before.keys(), ...after.keys()]);
1774
+ return [...files].filter((file) => before.get(file) !== after.get(file)).toSorted();
1775
+ }
1776
+ function needsAgent(comment) {
1777
+ return comment.paragraphs.some(
1778
+ (paragraph) => paragraph.issues.some((issue) => issue.tier === "hint" || paragraph.suggestion === null)
1779
+ );
1780
+ }
1781
+ function seconds(milliseconds) {
1782
+ return `${Math.round(milliseconds / 1e3)}s`;
1783
+ }
1784
+ function listFiles(files) {
1785
+ const more = files.length > LISTED_FILES ? `, and ${files.length - LISTED_FILES} more` : "";
1786
+ return `${files.slice(0, LISTED_FILES).join(", ")}${more}`;
1787
+ }
1788
+ var SILENT_REPORTER = {
1789
+ start: () => {
1790
+ },
1791
+ agentStarted: () => {
1792
+ },
1793
+ agentFinished: () => {
1794
+ },
1795
+ stop: () => {
1796
+ }
1797
+ };
1798
+ function createLogReporter(log3, options = {}) {
1799
+ let plan;
1800
+ let heartbeat;
1801
+ let started = 0;
1802
+ let running = 0;
1803
+ let finished = 0;
1804
+ return {
1805
+ start(next) {
1806
+ plan = next;
1807
+ started = Date.now();
1808
+ const count = next.agents.length;
1809
+ const agents = `${count} agent${count > 1 ? "s" : ""} (${next.agent})`;
1810
+ const queueing = count > next.maxAgents ? `, ${next.maxAgents} at a time,` : "";
1811
+ log3(`spawning ${agents}${queueing} for ${next.comments} comment(s) in ${next.files} file(s)`);
1812
+ log3(` command: ${next.command}`);
1813
+ if (count > 1) {
1814
+ for (const agent of next.agents) {
1815
+ log3(` ${agent.name}: ${agent.comments} comment(s) in ${listFiles(agent.files)}`);
1816
+ }
1817
+ }
1818
+ heartbeat = setInterval(() => {
1819
+ const queued = count - finished - running;
1820
+ const waiting = count > 1 ? `${count - finished} of ${count} agents` : "the agent";
1821
+ log3(`still waiting for ${waiting}${queued > 0 ? `, ${queued} queued` : ""} (${seconds(Date.now() - started)})`);
1822
+ }, PROGRESS_INTERVAL_MS);
1823
+ },
1824
+ agentStarted(index) {
1825
+ running += 1;
1826
+ if (plan && index >= plan.maxAgents) {
1827
+ log3(`${plan.agents[index].name} started`);
1828
+ }
1829
+ },
1830
+ agentFinished(_index, report) {
1831
+ running -= 1;
1832
+ finished += 1;
1833
+ const count = plan?.agents.length ?? 1;
1834
+ const took = seconds(report.durationMs);
1835
+ const status = report.ok ? `finished in ${took}` : `failed after ${took}`;
1836
+ const progress = count > 1 ? ` (${finished} of ${count} done)` : "";
1837
+ log3(`${report.name} ${status}${progress}${report.ok ? "" : `: ${report.error}`}`);
1838
+ if (report.output && (options.verbose || !report.ok)) {
1839
+ log3(report.output);
1840
+ }
1841
+ },
1842
+ stop() {
1843
+ clearInterval(heartbeat);
1844
+ }
1845
+ };
1846
+ }
1847
+ function batchFiles(files, batchSize, maxFiles = Infinity) {
1848
+ const total = files.reduce((sum, file) => sum + file.comments.length, 0);
1849
+ const order = files.map((_, index) => index).toSorted((a, b) => files[b].comments.length - files[a].comments.length);
1850
+ const minimum = Math.max(Math.ceil(total / batchSize), Math.ceil(files.length / maxFiles));
1851
+ for (let count = Math.min(files.length, minimum); ; count += 1) {
1852
+ const sizes = Array.from({ length: count }).fill(0);
1853
+ const indices = Array.from({ length: count }, () => []);
1854
+ for (const index of order) {
1855
+ const open = sizes.map((size, batch) => indices[batch].length < maxFiles ? size : Infinity);
1856
+ const smallest = open.indexOf(Math.min(...open));
1857
+ indices[smallest].push(index);
1858
+ sizes[smallest] += files[index].comments.length;
1859
+ }
1860
+ const fits = indices.every((batch, index) => batch.length === 1 || sizes[index] <= batchSize);
1861
+ if (fits || count >= files.length) {
1862
+ return indices.map((batch) => batch.toSorted((a, b) => a - b).map((index) => files[index]));
1863
+ }
1864
+ }
1865
+ }
1866
+ async function runPool(items, limit, worker) {
1867
+ const results = Array.from({ length: items.length });
1868
+ let next = 0;
1869
+ const lane = async () => {
1870
+ while (next < items.length) {
1871
+ const index = next;
1872
+ next += 1;
1873
+ results[index] = await worker(items[index], index);
1874
+ }
1875
+ };
1876
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => lane()));
1877
+ return results;
1878
+ }
1879
+ function applyRewraps(analyses, skip) {
1880
+ let count = 0;
1881
+ for (const analysis of analyses) {
1882
+ const comments = analysis.comments.filter((comment) => hasVerdictIssues(comment));
1883
+ if (comments.length === 0 || skip.has(analysis.file)) {
1884
+ continue;
1885
+ }
1886
+ const lines = analysis.source.split("\n");
1887
+ let rewrapped = 0;
1888
+ for (const comment of comments.toReversed()) {
1889
+ const text = rewrapComment(comment);
1890
+ if (text === comment.text) {
1891
+ continue;
1892
+ }
1893
+ const first = lines[comment.startLine - 1];
1894
+ const mark = first.startsWith(BYTE_ORDER_MARK) ? BYTE_ORDER_MARK : "";
1895
+ const ending = first.endsWith("\r") ? "\r" : "";
1896
+ const replacement = text.split("\n").map((line, index) => `${index === 0 ? mark : ""}${line}${ending}`);
1897
+ lines.splice(comment.startLine - 1, comment.endLine - comment.startLine + 1, ...replacement);
1898
+ rewrapped += 1;
1899
+ }
1900
+ const output = lines.join("\n");
1901
+ if (rewrapped === 0 || codeFingerprint(output, analysis.file) !== codeFingerprint(analysis.source, analysis.file)) {
1902
+ continue;
1903
+ }
1904
+ writeFileSync(analysis.absolute, output);
1905
+ count += rewrapped;
1906
+ }
1907
+ return count;
1908
+ }
1909
+ async function runFlow(project, scope, options) {
1910
+ const reporter = options.reporter ?? SILENT_REPORTER;
1911
+ const analyses = await project.analyze(scope);
1912
+ const measure = async () => {
1913
+ const current = await project.analyze(scope);
1914
+ return { remaining: withVerdictIssues(current), skipped: skippedFiles(current) };
1915
+ };
1916
+ if (options.mode === "report") {
1917
+ return {
1918
+ sentToAgent: 0,
1919
+ codeChanged: [],
1920
+ changedElsewhere: [],
1921
+ remaining: withVerdictIssues(analyses),
1922
+ skipped: skippedFiles(analyses)
1923
+ };
1924
+ }
1925
+ const pending = analyses.map((analysis) => {
1926
+ const selected = options.select === "all" ? analysis.comments : analysis.comments.filter((comment) => needsAgent(comment));
1927
+ return { ...analysis, comments: selected };
1928
+ }).filter((analysis) => analysis.comments.length > 0);
1929
+ const comments = pending.flatMap((analysis) => analysis.comments);
1930
+ if (comments.length === 0) {
1931
+ const rewrapped2 = applyRewraps(analyses, /* @__PURE__ */ new Set());
1932
+ return { sentToAgent: 0, codeChanged: [], changedElsewhere: [], rewrapped: rewrapped2, ...await measure() };
1933
+ }
1934
+ const agentConfig = project.config.agent ?? {};
1935
+ const rules = loadRules(agentConfig, options.configPath);
1936
+ const batches = batchFiles(
1937
+ pending,
1938
+ agentConfig.batchSize ?? DEFAULT_BATCH_SIZE,
1939
+ agentConfig.maxFiles ?? DEFAULT_MAX_FILES
1940
+ );
1941
+ const maxAgents = agentConfig.maxAgents ?? DEFAULT_MAX_AGENTS;
1942
+ const runs = batches.map((batch, index) => {
1943
+ const batchComments = batch.flatMap((analysis) => analysis.comments);
1944
+ const prompt = buildPrompt({ rules, root: project.root, comments: batchComments });
1945
+ const invocation = buildInvocation({ agent: agentConfig, runner: options.runner, prompt, root: project.root });
1946
+ return {
1947
+ name: batches.length > 1 ? `agent ${index + 1}` : "the agent",
1948
+ files: batch.map((analysis) => analysis.file),
1949
+ comments: batchComments.length,
1950
+ invocation
1951
+ };
1952
+ });
1953
+ reporter.start({
1954
+ agent: describeAgent(agentConfig, options.runner),
1955
+ command: formatInvocation(runs[0].invocation),
1956
+ comments: comments.length,
1957
+ files: pending.length,
1958
+ maxAgents,
1959
+ agents: runs.map(({ name, files, comments: comments2 }) => {
1960
+ return { name, files, comments: comments2 };
1961
+ })
1962
+ });
1963
+ const before = project.snapshot();
1964
+ let reports;
1965
+ try {
1966
+ reports = await runPool(runs, maxAgents, async (run, index) => {
1967
+ const start = Date.now();
1968
+ reporter.agentStarted(index);
1969
+ const result = await runAgent(run.invocation, { root: project.root, timeout: agentConfig.timeout });
1970
+ const report = {
1971
+ name: run.name,
1972
+ files: run.files,
1973
+ comments: run.comments,
1974
+ ok: result.ok,
1975
+ error: result.error,
1976
+ output: result.output,
1977
+ durationMs: Date.now() - start
1978
+ };
1979
+ reporter.agentFinished(index, report);
1980
+ return report;
1981
+ });
1982
+ } finally {
1983
+ reporter.stop();
1984
+ }
1985
+ const agentErrors = reports.flatMap((report, index) => {
1986
+ if (report.ok) {
1987
+ return [];
1988
+ }
1989
+ return [reports.length > 1 ? `agent ${index + 1} of ${reports.length}: ${report.error}` : report.error ?? ""];
1990
+ });
1991
+ const after = project.snapshot();
1992
+ const handedOver = new Set(pending.map((analysis) => analysis.file));
1993
+ const changedElsewhere = before && after ? changedBetween(before, after).filter((file) => !handedOver.has(file)) : [];
1994
+ const codeChanged = pending.filter((analysis) => {
1995
+ if (!existsSync2(analysis.absolute)) {
1996
+ return true;
1997
+ }
1998
+ const current = readFileSync3(analysis.absolute, "utf8");
1999
+ return codeFingerprint(current, analysis.file) !== codeFingerprint(analysis.source, analysis.file);
2000
+ }).map((analysis) => analysis.file);
2001
+ const rewrapped = applyRewraps(await project.analyze(scope), new Set(codeChanged));
2002
+ return {
2003
+ sentToAgent: comments.length,
2004
+ agents: runs.length,
2005
+ runs: reports,
2006
+ codeChanged,
2007
+ changedElsewhere,
2008
+ rewrapped,
2009
+ ...await measure(),
2010
+ agentError: agentErrors.length > 0 ? agentErrors.join(", ") : void 0
2011
+ };
2012
+ }
2013
+ var LISTED_COMMENTS = 5;
2014
+ function describeRemaining(remaining) {
2015
+ const listed = remaining.slice(0, LISTED_COMMENTS).map((comment) => {
2016
+ const issues = comment.paragraphs.flatMap((paragraph) => paragraph.issues);
2017
+ const types = new Set(issues.filter((issue) => issue.tier === "verdict").map((issue) => issue.type));
2018
+ return `${comment.file}:${comment.startLine} (${[...types].join(", ")})`;
2019
+ });
2020
+ const more = remaining.length > LISTED_COMMENTS ? `, and ${remaining.length - LISTED_COMMENTS} more` : "";
2021
+ return `${remaining.length} comment(s) still have issues: ${listed.join(", ")}${more}`;
2022
+ }
2023
+ function describeSummary(summary) {
2024
+ const parts = [];
2025
+ if (summary.sentToAgent > 0) {
2026
+ const agents = (summary.agents ?? 1) > 1 ? `${summary.agents} agents` : "the agent";
2027
+ parts.push(`sent ${summary.sentToAgent} comment(s) to ${agents}`);
2028
+ }
2029
+ if (summary.rewrapped) {
2030
+ parts.push(`rewrapped ${summary.rewrapped} comment(s)`);
2031
+ }
2032
+ if (summary.agentError) {
2033
+ parts.push(`agent failed: ${summary.agentError}`);
2034
+ }
2035
+ if (summary.codeChanged.length > 0) {
2036
+ parts.push(`the agent changed code, not just comments, in ${summary.codeChanged.join(", ")}`);
2037
+ }
2038
+ if (summary.changedElsewhere.length > 0) {
2039
+ parts.push(describeChangedElsewhere(summary.changedElsewhere));
2040
+ }
2041
+ if (summary.remaining.length > 0) {
2042
+ parts.push(describeRemaining(summary.remaining));
2043
+ }
2044
+ parts.push(...summary.skipped.map((skipped) => describeSkipped(skipped)));
2045
+ return parts.length > 0 ? `comments: ${parts.join("; ")}` : void 0;
2046
+ }
2047
+
2048
+ // src/comments/git.ts
2049
+ import { spawnSync } from "child_process";
2050
+ import { createHash } from "crypto";
2051
+ import { readFileSync as readFileSync4, statSync } from "fs";
2052
+ import path3 from "path";
2053
+ function git(cwd, args) {
2054
+ const result = spawnSync("git", ["-c", "core.quotePath=false", ...args], {
2055
+ cwd,
2056
+ encoding: "utf8",
2057
+ maxBuffer: 256 * 1024 * 1024
2058
+ });
2059
+ const stdout = result.stdout ?? "";
2060
+ const stderr = result.stderr ?? "";
2061
+ return { ok: result.status === 0, stdout, stderr };
2062
+ }
2063
+ function gitOrThrow(cwd, args) {
2064
+ const result = git(cwd, args);
2065
+ if (!result.ok) {
2066
+ throw new Error(`comments: \`git ${args.join(" ")}\` failed: ${result.stderr.trim()}`);
2067
+ }
2068
+ return result.stdout;
2069
+ }
2070
+ function splitPaths(output) {
2071
+ return output.split("\0").filter(Boolean);
2072
+ }
2073
+ function unquotePath(value) {
2074
+ if (!value.startsWith('"')) {
2075
+ return value;
2076
+ }
2077
+ try {
2078
+ return JSON.parse(value);
2079
+ } catch {
2080
+ return value;
2081
+ }
2082
+ }
2083
+ function findRepoRoot(cwd) {
2084
+ const result = git(cwd, ["rev-parse", "--show-toplevel"]);
2085
+ return result.ok ? result.stdout.trim() : void 0;
2086
+ }
2087
+ function parseUnifiedDiff(diff) {
2088
+ const changed = /* @__PURE__ */ new Map();
2089
+ let lines;
2090
+ let oldPending = 0;
2091
+ let newPending = 0;
2092
+ for (const line of diff.split("\n")) {
2093
+ if (oldPending > 0 || newPending > 0) {
2094
+ if (line.startsWith("+")) {
2095
+ newPending -= 1;
2096
+ } else if (line.startsWith("-")) {
2097
+ oldPending -= 1;
2098
+ } else if (line.startsWith(" ")) {
2099
+ oldPending -= 1;
2100
+ newPending -= 1;
2101
+ }
2102
+ continue;
2103
+ }
2104
+ if (line.startsWith("+++ ")) {
2105
+ const target = unquotePath(line.slice(4).replace(/\t$/, ""));
2106
+ if (target === "/dev/null") {
2107
+ lines = void 0;
2108
+ continue;
2109
+ }
2110
+ const file = target.startsWith("b/") ? target.slice(2) : target;
2111
+ lines = /* @__PURE__ */ new Set();
2112
+ changed.set(file, lines);
2113
+ continue;
2114
+ }
2115
+ const hunk = /^@@ -\d+(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
2116
+ if (!hunk) {
2117
+ continue;
2118
+ }
2119
+ const start = Number(hunk[2]);
2120
+ const count = hunk[3] ? Number(hunk[3]) : 1;
2121
+ oldPending = hunk[1] ? Number(hunk[1]) : 1;
2122
+ newPending = count;
2123
+ if (!lines) {
2124
+ continue;
2125
+ }
2126
+ if (count === 0) {
2127
+ lines.add(start);
2128
+ lines.add(start + 1);
2129
+ continue;
2130
+ }
2131
+ for (let offset = 0; offset < count; offset += 1) {
2132
+ lines.add(start + offset);
2133
+ }
2134
+ }
2135
+ return changed;
2136
+ }
2137
+ function hasHead(root) {
2138
+ return git(root, ["rev-parse", "--verify", "--quiet", "HEAD"]).ok;
2139
+ }
2140
+ function listUntrackedFiles(root) {
2141
+ return splitPaths(gitOrThrow(root, ["ls-files", "-z", "--others", "--exclude-standard"]));
2142
+ }
2143
+ function listFiles2(root, paths = []) {
2144
+ return splitPaths(gitOrThrow(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...paths]));
2145
+ }
2146
+ var MAX_HASHED_BYTES = 1024 * 1024;
2147
+ function fileState(absolute) {
2148
+ try {
2149
+ const stats = statSync(absolute);
2150
+ if (!stats.isFile()) {
2151
+ return "no file";
2152
+ }
2153
+ return stats.size > MAX_HASHED_BYTES ? `${stats.size} bytes, changed at ${stats.mtimeMs}` : createHash("sha1").update(readFileSync4(absolute)).digest("hex");
2154
+ } catch {
2155
+ return "missing";
2156
+ }
2157
+ }
2158
+ function snapshotChanges(root) {
2159
+ try {
2160
+ const tracked = hasHead(root) ? splitPaths(gitOrThrow(root, ["diff", "-z", "--name-only", "--no-renames", "HEAD"])) : splitPaths(gitOrThrow(root, ["ls-files", "-z", "--cached"]));
2161
+ const files = /* @__PURE__ */ new Set([...tracked, ...listUntrackedFiles(root)]);
2162
+ return new Map([...files].map((file) => [file, fileState(path3.join(root, file))]));
2163
+ } catch {
2164
+ return void 0;
2165
+ }
2166
+ }
2167
+ var DIFF_FLAGS = ["diff", "-U0", "--no-color", "--no-ext-diff", "--no-renames", "--src-prefix=a/", "--dst-prefix=b/"];
2168
+ function getChangedLines(root, scope = {}) {
2169
+ if (!hasHead(root)) {
2170
+ const files = scope.staged ? splitPaths(gitOrThrow(root, ["diff", "-z", "--cached", "--name-only", "--diff-filter=d"])) : listFiles2(root);
2171
+ return new Map(files.map((file) => [file, "all"]));
2172
+ }
2173
+ let args;
2174
+ if (scope.staged) {
2175
+ args = [...DIFF_FLAGS, "--cached", "HEAD"];
2176
+ } else if (scope.base) {
2177
+ const mergeBase = gitOrThrow(root, ["merge-base", scope.base, "HEAD"]).trim();
2178
+ args = [...DIFF_FLAGS, mergeBase];
2179
+ } else {
2180
+ args = [...DIFF_FLAGS, "HEAD"];
2181
+ }
2182
+ const changed = parseUnifiedDiff(gitOrThrow(root, args));
2183
+ if (!scope.staged) {
2184
+ for (const file of listUntrackedFiles(root)) {
2185
+ changed.set(file, "all");
2186
+ }
2187
+ }
2188
+ return changed;
2189
+ }
2190
+
2191
+ // src/comments/project.ts
2192
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
2193
+ import path4 from "path";
2194
+ var DEFAULT_WIDTH = 80;
2195
+ var DEFAULT_MAX_WIDTH = 120;
2196
+ var DEFAULT_SINGLE_LINE_WIDTH = 120;
2197
+ var DEFAULT_TAB_WIDTH = 2;
2198
+ var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage"]);
2199
+ function toPosix(file) {
2200
+ return file.split(path4.sep).join("/");
2201
+ }
2202
+ function mergeFilter(existing, next) {
2203
+ if (existing === "all" || next === "all") {
2204
+ return "all";
2205
+ }
2206
+ return existing ? /* @__PURE__ */ new Set([...existing, ...next]) : next;
2207
+ }
2208
+ function walkDirectory(directory, files = []) {
2209
+ const entries = readdirSync(directory, { withFileTypes: true });
2210
+ for (const entry of entries) {
2211
+ if (entry.isDirectory()) {
2212
+ if (!SKIPPED_DIRECTORIES.has(entry.name)) {
2213
+ walkDirectory(path4.join(directory, entry.name), files);
2214
+ }
2215
+ } else if (entry.isFile()) {
2216
+ files.push(path4.join(directory, entry.name));
2217
+ }
2218
+ }
2219
+ return files;
2220
+ }
2221
+ function overlaps(comment, filter) {
2222
+ if (filter === "all") {
2223
+ return true;
2224
+ }
2225
+ for (let line = comment.startLine; line <= comment.endLine; line += 1) {
2226
+ if (filter.has(line)) {
2227
+ return true;
2228
+ }
2229
+ }
2230
+ return false;
2231
+ }
2232
+ var Project = class {
2233
+ constructor(root, config, _options) {
2234
+ this.root = root;
2235
+ this.config = config;
2236
+ this._options = _options;
2237
+ this._prettier = new PrettierOptionsResolver(root);
2238
+ this._extensions = new Set(config.extensions ?? DEFAULT_EXTENSIONS);
2239
+ }
2240
+ root;
2241
+ config;
2242
+ _options;
2243
+ _prettier;
2244
+ _extensions;
2245
+ relative(absolute) {
2246
+ return toPosix(path4.relative(this.root, absolute));
2247
+ }
2248
+ _isIncluded(absolute) {
2249
+ if (!this._extensions.has(path4.extname(absolute))) {
2250
+ return false;
2251
+ }
2252
+ const relative = this.relative(absolute);
2253
+ return !(this.config.ignore ?? []).some((pattern) => path4.matchesGlob(relative, pattern));
2254
+ }
2255
+ _filesIn(directory) {
2256
+ if (!this._options.isGitRepo) {
2257
+ return walkDirectory(directory);
2258
+ }
2259
+ return listFiles2(this.root, [path4.relative(this.root, directory) || "."]).map((file) => path4.join(this.root, file));
2260
+ }
2261
+ collect(scope) {
2262
+ const files = /* @__PURE__ */ new Map();
2263
+ const add = (absolute, filter) => {
2264
+ if (this._isIncluded(absolute) && existsSync3(absolute)) {
2265
+ files.set(absolute, mergeFilter(files.get(absolute), filter));
2266
+ }
2267
+ };
2268
+ if (scope.targets && scope.targets.length > 0) {
2269
+ for (const target of scope.targets) {
2270
+ const range = /^(.+):(\d+)(?:-(\d+))?$/.exec(target);
2271
+ const absolute = path4.resolve(this._options.cwd, range ? range[1] : target);
2272
+ if (!existsSync3(absolute)) {
2273
+ throw new Error(`comments: ${target} does not exist`);
2274
+ }
2275
+ if (statSync2(absolute).isDirectory()) {
2276
+ for (const file of this._filesIn(absolute)) {
2277
+ add(file, "all");
2278
+ }
2279
+ continue;
2280
+ }
2281
+ if (!range) {
2282
+ add(absolute, "all");
2283
+ continue;
2284
+ }
2285
+ const start = Number(range[2]);
2286
+ const end = range[3] ? Number(range[3]) : start;
2287
+ add(absolute, new Set(Array.from({ length: Math.max(0, end - start + 1) }, (_, offset) => start + offset)));
2288
+ }
2289
+ return files;
2290
+ }
2291
+ if (scope.repo) {
2292
+ for (const file of this._filesIn(this.root)) {
2293
+ add(file, "all");
2294
+ }
2295
+ return files;
2296
+ }
2297
+ if (!this._options.isGitRepo) {
2298
+ throw new Error("comments: not a git repository, so pass files or directories explicitly");
2299
+ }
2300
+ for (const [file, lines] of getChangedLines(this.root, scope)) {
2301
+ add(path4.join(this.root, file), lines);
2302
+ }
2303
+ return files;
2304
+ }
2305
+ async analyzeFile(absolute, filter = "all") {
2306
+ const source = readFileSync5(absolute, "utf8");
2307
+ const prettier = await this._prettier.resolve(absolute);
2308
+ const file = this.relative(absolute);
2309
+ const width = this._options.width ?? this.config.width ?? prettier.printWidth ?? DEFAULT_WIDTH;
2310
+ const maxWidth = this.config.maxWidth ?? Math.max(width, this.config.tolerance === void 0 ? DEFAULT_MAX_WIDTH : width + this.config.tolerance);
2311
+ const { comments, syntaxError } = inspectSource(source, file, {
2312
+ width,
2313
+ maxWidth,
2314
+ singleLineWidth: this.config.singleLineWidth ?? DEFAULT_SINGLE_LINE_WIDTH,
2315
+ tabWidth: prettier.tabWidth ?? DEFAULT_TAB_WIDTH,
2316
+ tolerance: Math.min(this.config.tolerance ?? 0, maxWidth - width),
2317
+ avoidSentenceOrphans: this.config.avoidSentenceOrphans ?? true,
2318
+ style: this.config.style ?? true
2319
+ });
2320
+ return { file, absolute, source, comments: comments.filter((comment) => overlaps(comment, filter)), syntaxError };
2321
+ }
2322
+ /**
2323
+ * Records the state of every file with uncommitted changes, to compare it with a later
2324
+ * snapshot. Returns `undefined` outside of a git repository, where only reading every
2325
+ * file would show which ones changed.
2326
+ */
2327
+ snapshot() {
2328
+ return this._options.isGitRepo ? snapshotChanges(this.root) : void 0;
2329
+ }
2330
+ async analyze(scope) {
2331
+ const results = [];
2332
+ for (const [absolute, filter] of this.collect(scope)) {
2333
+ results.push(await this.analyzeFile(absolute, filter));
2334
+ }
2335
+ return results.toSorted((a, b) => a.file.localeCompare(b.file));
2336
+ }
2337
+ };
2338
+
2339
+ // src/comments/terminal.ts
2340
+ import {
2341
+ intro,
2342
+ isCancel,
2343
+ isCI,
2344
+ log,
2345
+ note,
2346
+ S_BAR,
2347
+ S_BAR_END,
2348
+ S_RADIO_INACTIVE,
2349
+ S_STEP_ERROR,
2350
+ S_STEP_SUBMIT,
2351
+ select,
2352
+ spinner,
2353
+ unicode,
2354
+ unicodeOr
2355
+ } from "@clack/prompts";
2356
+ import { spawnSync as spawnSync2 } from "child_process";
2357
+ import { Writable } from "stream";
2358
+ import { styleText } from "util";
2359
+ var FRAMES = unicode ? ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"] : ["|", "/", "-", "\\"];
2360
+ var FRAME_MS = 80;
2361
+ var S_DONE = unicodeOr("\u2714", "\u221A");
2362
+ var S_FAILED = unicodeOr("\u2716", "x");
2363
+ var HIDE_CURSOR = "\x1B[?25l";
2364
+ var SHOW_CURSOR = "\x1B[?25h";
2365
+ var LISTED_REMAINING = 10;
2366
+ var CTRL_C = 3;
2367
+ var MIN_FILES_WIDTH = 12;
2368
+ var ICONS = {
2369
+ queued: ["dim", S_RADIO_INACTIVE],
2370
+ done: ["green", S_DONE],
2371
+ failed: ["red", S_FAILED]
2372
+ };
2373
+ function isLiveTerminal(output = process.stderr) {
2374
+ return output.isTTY && !isCI() && process.env.TERM !== "dumb";
2375
+ }
2376
+ function plural(count, noun) {
2377
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
2378
+ }
2379
+ function formatDuration(milliseconds) {
2380
+ const total = Math.round(milliseconds / 1e3);
2381
+ const minutes = Math.floor(total / 60);
2382
+ return minutes > 0 ? `${minutes}m ${String(total % 60).padStart(2, "0")}s` : `${total}s`;
2383
+ }
2384
+ function fitFiles(files, width) {
2385
+ for (let shown = files.length; shown > 0; shown -= 1) {
2386
+ const more = files.length - shown;
2387
+ const text = `${files.slice(0, shown).join(", ")}${more > 0 ? `, +${more} more` : ""}`;
2388
+ if (text.length <= width) {
2389
+ return text;
2390
+ }
2391
+ }
2392
+ return width > 1 ? `\u2026${files[0].slice(-(width - 1))}` : "";
2393
+ }
2394
+ function truncate(text, width) {
2395
+ return text.length > width ? `${text.slice(0, Math.max(width - 1, 0))}\u2026` : text;
2396
+ }
2397
+ function agentLabel(index) {
2398
+ return `agent ${index + 1}`;
2399
+ }
2400
+ function formatReply(text, paint) {
2401
+ return text.replaceAll(/\*\*([^*\n]+)\*\*/g, (_, inner) => paint("bold", inner)).replaceAll(/`([^`\n]+)`/g, (_, inner) => paint("cyan", inner));
2402
+ }
2403
+ var AgentTable = class {
2404
+ constructor(_output, _paint) {
2405
+ this._output = _output;
2406
+ this._paint = _paint;
2407
+ }
2408
+ _output;
2409
+ _paint;
2410
+ _rows = [];
2411
+ _title = "";
2412
+ _drawn = 0;
2413
+ _frame = 0;
2414
+ _startedAt = 0;
2415
+ _timer;
2416
+ _restoreCursor = () => {
2417
+ this._output.write(SHOW_CURSOR);
2418
+ };
2419
+ _interrupt = () => {
2420
+ this._restoreCursor();
2421
+ process.off("exit", this._restoreCursor);
2422
+ process.kill(process.pid, "SIGINT");
2423
+ };
2424
+ start(plan, title) {
2425
+ this._rows = plan.agents.map((agent) => {
2426
+ return { plan: agent, state: "queued" };
2427
+ });
2428
+ this._title = title;
2429
+ this._startedAt = Date.now();
2430
+ process.once("SIGINT", this._interrupt);
2431
+ process.once("exit", this._restoreCursor);
2432
+ this._output.write(`${this._paint("gray", S_BAR)}
2433
+ ${HIDE_CURSOR}`);
2434
+ this._draw(false);
2435
+ this._timer = setInterval(() => {
2436
+ this._frame = (this._frame + 1) % FRAMES.length;
2437
+ this._draw(false);
2438
+ }, FRAME_MS);
2439
+ }
2440
+ started(index) {
2441
+ this._rows[index].state = "running";
2442
+ this._rows[index].startedAt = Date.now();
2443
+ }
2444
+ finished(index, report) {
2445
+ this._rows[index].state = report.ok ? "done" : "failed";
2446
+ this._rows[index].durationMs = report.durationMs;
2447
+ }
2448
+ stop() {
2449
+ if (this._timer === void 0) {
2450
+ return;
2451
+ }
2452
+ clearInterval(this._timer);
2453
+ this._timer = void 0;
2454
+ this._draw(true);
2455
+ this._restoreCursor();
2456
+ process.off("SIGINT", this._interrupt);
2457
+ process.off("exit", this._restoreCursor);
2458
+ }
2459
+ _draw(final) {
2460
+ const lines = this._render(final);
2461
+ const clear = this._drawn > 0 ? `\x1B[${this._drawn}F\x1B[J` : "";
2462
+ this._output.write(`${clear}${lines.join("\n")}
2463
+ `);
2464
+ this._drawn = lines.length;
2465
+ }
2466
+ _count(state) {
2467
+ return this._rows.filter((row) => row.state === state).length;
2468
+ }
2469
+ _render(final) {
2470
+ const columns = Math.max((this._output.columns || 80) - 1, 20);
2471
+ const elapsed = formatDuration(Date.now() - this._startedAt);
2472
+ const failed = this._count("failed");
2473
+ const finished = this._count("done") + failed;
2474
+ let header;
2475
+ if (final) {
2476
+ const outcome = `${plural(this._rows.length, "agent")} finished in ${elapsed}${failed > 0 ? `, ${failed} failed` : ""}`;
2477
+ const symbol = failed > 0 ? this._paint("yellow", S_STEP_ERROR) : this._paint("green", S_STEP_SUBMIT);
2478
+ header = `${symbol} ${truncate(outcome, columns - 3)}`;
2479
+ } else {
2480
+ const progress = `${finished} of ${this._rows.length} done, ${elapsed}`;
2481
+ const text = truncate(`${this._title} ${progress}`, columns - 3);
2482
+ header = `${this._paint("magenta", FRAMES[this._frame])} ${text.replace(progress, this._paint("dim", progress))}`;
2483
+ }
2484
+ return [header, ...this._renderRows(columns, final)];
2485
+ }
2486
+ _renderRows(columns, final) {
2487
+ const bar = this._paint("gray", S_BAR);
2488
+ const nameWidth = agentLabel(this._rows.length - 1).length;
2489
+ const commentsWidth = Math.max(...this._rows.map((row) => plural(row.plan.comments, "comment").length));
2490
+ const statusWidth = Math.max(...this._rows.map((row) => this._status(row).length));
2491
+ const filesWidth = columns - (3 + 2 + nameWidth + 2 + commentsWidth + 2 + statusWidth + 2);
2492
+ const height = (this._output.rows || 24) - 3;
2493
+ const compact = !final && this._rows.length + 1 > height;
2494
+ const visible = this._rows.map((row, index) => {
2495
+ return { row, index };
2496
+ }).filter(({ row }) => !compact || row.state === "running" || row.state === "failed").slice(0, Math.max(height - 1, 1));
2497
+ const lines = visible.map(({ row, index }) => {
2498
+ const status = this._status(row).padEnd(statusWidth);
2499
+ const files = filesWidth >= MIN_FILES_WIDTH ? ` ${this._paint("dim", fitFiles(row.plan.files, filesWidth))}` : "";
2500
+ return [
2501
+ `${bar} ${this._icon(row)} ${agentLabel(index).padEnd(nameWidth)}`,
2502
+ this._paint("dim", plural(row.plan.comments, "comment").padEnd(commentsWidth)),
2503
+ this._paint(this._statusStyle(row), status)
2504
+ ].join(" ") + files;
2505
+ });
2506
+ if (compact) {
2507
+ const done = this._count("done");
2508
+ const queued = this._count("queued");
2509
+ lines.push(`${bar} ${this._paint("dim", `${S_DONE} ${done} done, ${S_RADIO_INACTIVE} ${queued} queued`)}`);
2510
+ }
2511
+ return lines;
2512
+ }
2513
+ _icon(row) {
2514
+ if (row.state === "running") {
2515
+ return this._paint("magenta", FRAMES[this._frame]);
2516
+ }
2517
+ const [style, symbol] = ICONS[row.state];
2518
+ return this._paint(style, symbol);
2519
+ }
2520
+ _status(row) {
2521
+ if (row.state === "queued") {
2522
+ return "queued";
2523
+ }
2524
+ if (row.state === "running") {
2525
+ return formatDuration(Date.now() - (row.startedAt ?? this._startedAt));
2526
+ }
2527
+ return formatDuration(row.durationMs ?? 0);
2528
+ }
2529
+ _statusStyle(row) {
2530
+ switch (row.state) {
2531
+ case "running": {
2532
+ return "cyan";
2533
+ }
2534
+ case "failed": {
2535
+ return "red";
2536
+ }
2537
+ default: {
2538
+ return "dim";
2539
+ }
2540
+ }
2541
+ }
2542
+ };
2543
+ var TerminalUI = class {
2544
+ constructor(_output = process.stderr) {
2545
+ this._output = _output;
2546
+ this._table = new AgentTable(_output, this._paint);
2547
+ this.reporter = {
2548
+ start: (plan) => {
2549
+ const queueing = plan.agents.length > plan.maxAgents ? `, ${plan.maxAgents} at a time` : "";
2550
+ this._stopSpinner(`Found ${plural(plan.comments, "comment")} to review in ${plural(plan.files, "file")}`);
2551
+ this._muteInput();
2552
+ this._table.start(plan, `Running ${plural(plan.agents.length, "agent")} (${plan.agent})${queueing}`);
2553
+ },
2554
+ agentStarted: (index) => {
2555
+ this._table.started(index);
2556
+ },
2557
+ agentFinished: (index, report) => {
2558
+ this._table.finished(index, report);
2559
+ },
2560
+ stop: () => {
2561
+ this._table.stop();
2562
+ this._unmuteInput();
2563
+ }
2564
+ };
2565
+ }
2566
+ _output;
2567
+ reporter;
2568
+ _table;
2569
+ _busy;
2570
+ _muted = false;
2571
+ _paint = (style, text) => {
2572
+ return styleText(style, text, { stream: this._output });
2573
+ };
2574
+ begin(title) {
2575
+ intro(this._paint("inverse", ` ${title} `), { output: this._output });
2576
+ this._startSpinner("Finding comments");
2577
+ }
2578
+ dispose() {
2579
+ this._unmuteInput();
2580
+ this._table.stop();
2581
+ this._stopSpinner();
2582
+ }
2583
+ async finish(summary, options) {
2584
+ const output = this._output;
2585
+ const runs = summary.runs ?? [];
2586
+ if (summary.sentToAgent === 0) {
2587
+ this._stopSpinner("No comment in scope needs an agent");
2588
+ if (summary.rewrapped) {
2589
+ log.info(`Rewrapped ${plural(summary.rewrapped, "comment")} to fix their layout`, { output });
2590
+ }
2591
+ if (summary.remaining.length > 0) {
2592
+ log.warn(this._describeRemaining(summary.remaining), { output });
2593
+ }
2594
+ this._warnSkipped(summary);
2595
+ this._outro(summary.rewrapped ? "Done" : "Nothing to do");
2596
+ return;
2597
+ }
2598
+ for (const [index, run] of runs.entries()) {
2599
+ if (!run.ok) {
2600
+ log.error(`${agentLabel(index)} failed: ${run.error}`, { output });
2601
+ }
2602
+ }
2603
+ if (summary.codeChanged.length > 0) {
2604
+ log.warn(`The agents changed code, not just comments, in ${summary.codeChanged.join(", ")}`, { output });
2605
+ }
2606
+ if (summary.changedElsewhere.length > 0) {
2607
+ const files = plural(summary.changedElsewhere.length, "file");
2608
+ log.warn(`${files} that no agent was given changed during the run: ${summary.changedElsewhere.join(", ")}`, {
2609
+ output
2610
+ });
2611
+ }
2612
+ if (summary.rewrapped) {
2613
+ log.info(`Rewrapped ${plural(summary.rewrapped, "comment")} to fix their layout`, { output });
2614
+ }
2615
+ if (summary.remaining.length > 0) {
2616
+ log.warn(this._describeRemaining(summary.remaining), { output });
2617
+ } else {
2618
+ log.success("Every comment passes the check now", { output });
2619
+ }
2620
+ this._warnSkipped(summary);
2621
+ if (options.verbose) {
2622
+ for (const [index, run] of runs.entries()) {
2623
+ this._showReply(run, index, summary.remaining);
2624
+ }
2625
+ } else if (options.interactive) {
2626
+ await this._browse(runs, summary.remaining);
2627
+ }
2628
+ const clean = summary.remaining.length === 0 && summary.codeChanged.length === 0 && !summary.agentError;
2629
+ this._outro(clean ? "Done" : "Done, but some comments need a look");
2630
+ }
2631
+ _warnSkipped(summary) {
2632
+ for (const skipped of summary.skipped) {
2633
+ const text = describeSkipped(skipped);
2634
+ log.warn(`${text.charAt(0).toUpperCase()}${text.slice(1)}`, { output: this._output });
2635
+ }
2636
+ }
2637
+ /**
2638
+ * Closes the guide like clack's `outro`, which always ends with an empty line.
2639
+ */
2640
+ _outro(message) {
2641
+ this._output.write(`${this._paint("gray", S_BAR)}
2642
+ ${this._paint("gray", S_BAR_END)} ${message}
2643
+ `);
2644
+ }
2645
+ /**
2646
+ * Keeps the terminal from echoing keys, like `^[[B` for the down arrow, into the output while
2647
+ * it redraws. Raw mode also stops the terminal from turning Ctrl-C into a signal, so the input
2648
+ * handler sends it instead.
2649
+ */
2650
+ _muteInput() {
2651
+ const { stdin } = process;
2652
+ if (!stdin.isTTY || this._muted) {
2653
+ return;
2654
+ }
2655
+ this._muted = true;
2656
+ stdin.setRawMode(true);
2657
+ stdin.on("data", this._discardInput);
2658
+ stdin.resume();
2659
+ }
2660
+ _unmuteInput() {
2661
+ const { stdin } = process;
2662
+ if (!this._muted) {
2663
+ return;
2664
+ }
2665
+ this._muted = false;
2666
+ stdin.off("data", this._discardInput);
2667
+ stdin.setRawMode(false);
2668
+ stdin.pause();
2669
+ }
2670
+ _discardInput = (data) => {
2671
+ if (!data.includes(CTRL_C)) {
2672
+ return;
2673
+ }
2674
+ this._unmuteInput();
2675
+ try {
2676
+ process.kill(0, "SIGINT");
2677
+ } catch {
2678
+ process.kill(process.pid, "SIGINT");
2679
+ }
2680
+ };
2681
+ _startSpinner(message) {
2682
+ this._busy = spinner({ output: this._output, frames: FRAMES, delay: FRAME_MS });
2683
+ this._busy.start(message);
2684
+ }
2685
+ _stopSpinner(message) {
2686
+ if (message === void 0) {
2687
+ this._busy?.clear();
2688
+ } else {
2689
+ this._busy?.stop(message);
2690
+ }
2691
+ this._busy = void 0;
2692
+ }
2693
+ _describeRemaining(remaining) {
2694
+ const locations = remaining.map((comment) => `${comment.file}:${comment.startLine}`);
2695
+ const width = Math.max(...locations.slice(0, LISTED_REMAINING).map((location) => location.length));
2696
+ const listed = remaining.slice(0, LISTED_REMAINING).map((comment, index) => {
2697
+ const issues = comment.paragraphs.flatMap((paragraph) => paragraph.issues);
2698
+ const types = new Set(issues.filter((issue) => issue.tier === "verdict").map((issue) => issue.type));
2699
+ return ` ${locations[index].padEnd(width)} ${this._paint("dim", [...types].join(", "))}`;
2700
+ });
2701
+ const more = remaining.length > LISTED_REMAINING ? [this._paint("dim", ` and ${remaining.length - LISTED_REMAINING} more`)] : [];
2702
+ return [
2703
+ `${plural(remaining.length, "comment")} still ${remaining.length === 1 ? "has" : "have"} issues`,
2704
+ ...listed,
2705
+ ...more
2706
+ ].join("\n");
2707
+ }
2708
+ _remainingIn(run, remaining) {
2709
+ return remaining.filter((comment) => run.files.includes(comment.file)).length;
2710
+ }
2711
+ _describeRun(run, remaining) {
2712
+ if (!run.ok) {
2713
+ return `failed after ${formatDuration(run.durationMs)}`;
2714
+ }
2715
+ const left = this._remainingIn(run, remaining);
2716
+ return [
2717
+ plural(run.comments, "comment"),
2718
+ formatDuration(run.durationMs),
2719
+ ...left > 0 ? [`${left} still ${left === 1 ? "has" : "have"} issues`] : []
2720
+ ].join(", ");
2721
+ }
2722
+ _showReply(run, index, remaining, output = this._output) {
2723
+ const reply = run.output ? formatReply(run.output, this._paint) : this._paint("dim", "The agent printed nothing.");
2724
+ const error = run.ok ? [] : [this._paint("red", run.error ?? "The agent failed."), ""];
2725
+ const files = this._paint("dim", run.files.join(", "));
2726
+ const title = `${agentLabel(index)}, ${this._describeRun(run, remaining)}`;
2727
+ note([files, "", ...error, reply].join("\n"), title, { output, withGuide: output === this._output });
2728
+ }
2729
+ /**
2730
+ * Opens an agent's reply in `less`, which shows it on the alternate screen and restores the
2731
+ * terminal when it quits, so that opening replies doesn't pile them up in the scrollback.
2732
+ */
2733
+ _openReply(run, index, remaining) {
2734
+ let text = "";
2735
+ const capture = Object.assign(
2736
+ new Writable({
2737
+ write(chunk, _encoding, callback) {
2738
+ text += String(chunk);
2739
+ callback();
2740
+ }
2741
+ }),
2742
+ { columns: this._output.columns || 80 }
2743
+ );
2744
+ this._showReply(run, index, remaining, capture);
2745
+ const prompt = `${agentLabel(index)}, press q to go back`.replaceAll(/[?:.%\\]/g, String.raw`\$&`);
2746
+ const result = spawnSync2("less", ["-R", "-c", "-~", "-+F", "-+E", "-+X", `-Ps${prompt}`], {
2747
+ input: text,
2748
+ stdio: ["pipe", this._output.fd, "inherit"]
2749
+ });
2750
+ if (result.error) {
2751
+ this._showReply(run, index, remaining);
2752
+ }
2753
+ }
2754
+ /**
2755
+ * Erases what a closed `select` prompt left behind, its guide line, message, and choice, plus a
2756
+ * closing guide line when it was cancelled.
2757
+ */
2758
+ _erasePrompt(cancelled) {
2759
+ const lines = cancelled ? 4 : 3;
2760
+ this._output.write(`\x1B[${lines}F\x1B[J`);
2761
+ }
2762
+ async _browse(runs, remaining) {
2763
+ const done = -1;
2764
+ const columns = (this._output.columns || 80) - 1;
2765
+ const nameWidth = agentLabel(runs.length - 1).length;
2766
+ const options = runs.map((run, index) => {
2767
+ const icon = run.ok ? this._paint("green", S_DONE) : this._paint("red", S_FAILED);
2768
+ const hint = this._describeRun(run, remaining);
2769
+ const filesWidth = Math.max(columns - 9 - nameWidth - 2 - (hint.length + 3), MIN_FILES_WIDTH);
2770
+ return {
2771
+ value: index,
2772
+ label: `${icon} ${agentLabel(index).padEnd(nameWidth)} ${fitFiles(run.files, filesWidth)}`,
2773
+ hint
2774
+ };
2775
+ });
2776
+ let initial = done;
2777
+ for (; ; ) {
2778
+ const choice = await select({
2779
+ message: "Open an agent's reply",
2780
+ options: [...options, { value: done, label: "Done" }],
2781
+ initialValue: initial,
2782
+ output: this._output
2783
+ });
2784
+ this._erasePrompt(isCancel(choice));
2785
+ if (isCancel(choice) || choice === done) {
2786
+ return;
2787
+ }
2788
+ this._openReply(runs[choice], choice, remaining);
2789
+ initial = choice;
2790
+ }
2791
+ }
2792
+ };
2793
+
2794
+ // src/comments/cli.ts
2795
+ var HELP = `
2796
+ Usage: comments <command> [targets...] [options]
2797
+
2798
+ Finds comments in JavaScript and TypeScript files and measures how their prose fits the configured width.
2799
+ Only agent and hook change files, by applying the exact rewrap to comments that still have layout issues.
2800
+
2801
+ Commands:
2802
+ list Print comments in scope with their line lengths, issues, and rewrap suggestions
2803
+ check Print comments with layout violations and exit with code 1 if any remain
2804
+ wrap Print the exact rewrap of the comment at each file:line target
2805
+ agent Hand comments in scope to an agent, then report any that still have issues
2806
+ hook Stop-hook entry point for agents (reads the hook payload from stdin)
2807
+
2808
+ Targets are files, directories, file:line, or file:start-end. Without targets, only comments
2809
+ overlapping uncommitted changes (and untracked files) are considered.
2810
+
2811
+ Options:
2812
+ --staged Only consider staged changes
2813
+ --base <ref> Consider changes since the merge base with <ref>
2814
+ --repo Consider every file in the repository
2815
+ --suspicious Only comments with layout issues (overflow, early line ends, last lines of one or two words)
2816
+ or hints (possible orphans, em-dashes, banners, ...). For agent and hook (the default), only
2817
+ comments with hints or with issues in parts the tool never rewraps, a triage, not a full review
2818
+ --all agent, hook: hand every comment in scope to the agent for a full review of its wording
2819
+ --verbose agent, hook: print every agent's reply (otherwise a terminal offers to open them)
2820
+ --no-interactive agent: show the agents while they run, then exit without offering to open their replies
2821
+ --json Print JSON
2822
+ --config <path> Use this config file instead of searching for one
2823
+ --cwd <dir> Run as if started in <dir> (targets and --config resolve against it)
2824
+ --host <host> hook: the agent running the hook (cursor, claude, codex)
2825
+ --runner <name> agent, hook: the agent CLI to spawn (claude, codex, cursor)
2826
+ --mode <mode> hook: agent, report, or off
2827
+ -h, --help Show this help
2828
+
2829
+ Config overrides (take precedence over the config file):
2830
+ --width <n> Preferred comment line length
2831
+ --max-width <n> Hard limit for prose (default: 120, never below width)
2832
+ --single-line-width <n> Longest line a one-paragraph comment may stay on (default: 120)
2833
+ --tolerance <n> Legacy hard limit of width + n, unless max-width is set
2834
+ --[no-]orphans Keep a sentence's first word off the end of a line, and end the first of two lines
2835
+ with a sentence when the second is no longer than the first (default: on)
2836
+ --[no-]style Look for possible semicolons, em-dashes, dashes, label colons, emphasis, and banners
2837
+ as hints (default: on). The agent's rules apply either way
2838
+ --ignore <glob> Skip matching files, repeatable
2839
+ --model <name> agent, hook: model for the agent
2840
+ --timeout <seconds> agent, hook: how long to wait for each agent (default: 300)
2841
+ --batch-size <n> agent, hook: most comments per agent (default: 20)
2842
+ --max-files <n> agent, hook: most files per agent (default: 10)
2843
+ --max-agents <n> agent, hook: most agents running at once (default: 6)
2844
+ --instructions <text> agent, hook: extra instructions appended to the rules
2845
+ `.trim();
2846
+ var RUNNERS = /* @__PURE__ */ new Set(["claude", "codex", "cursor"]);
2847
+ var MODES = /* @__PURE__ */ new Set(["agent", "report", "off"]);
2848
+ var OPTIONS = {
2849
+ tolerance: { type: "string" },
2850
+ orphans: { type: "boolean" },
2851
+ style: { type: "boolean" },
2852
+ ignore: { type: "string", multiple: true },
2853
+ model: { type: "string" },
2854
+ timeout: { type: "string" },
2855
+ "batch-size": { type: "string" },
2856
+ "max-files": { type: "string" },
2857
+ "max-agents": { type: "string" },
2858
+ instructions: { type: "string" },
2859
+ staged: { type: "boolean" },
2860
+ base: { type: "string" },
2861
+ repo: { type: "boolean" },
2862
+ suspicious: { type: "boolean" },
2863
+ all: { type: "boolean" },
2864
+ verbose: { type: "boolean" },
2865
+ interactive: { type: "boolean" },
2866
+ json: { type: "boolean" },
2867
+ width: { type: "string" },
2868
+ "max-width": { type: "string" },
2869
+ "single-line-width": { type: "string" },
2870
+ config: { type: "string" },
2871
+ cwd: { type: "string" },
2872
+ host: { type: "string" },
2873
+ runner: { type: "string" },
2874
+ mode: { type: "string" },
2875
+ help: { type: "boolean", short: "h" }
2876
+ };
2877
+ function parse(argv) {
2878
+ return parseArgs({ args: [...argv], allowPositionals: true, allowNegative: true, options: OPTIONS });
2879
+ }
2880
+ function peek(argv) {
2881
+ const { values, positionals } = parseArgs({
2882
+ args: [...argv],
2883
+ allowPositionals: true,
2884
+ allowNegative: true,
2885
+ options: OPTIONS,
2886
+ strict: false
2887
+ });
2888
+ return {
2889
+ command: positionals.at(0),
2890
+ host: typeof values.host === "string" ? values.host : void 0,
2891
+ help: values.help === true
2892
+ };
2893
+ }
2894
+ function log2(message) {
2895
+ process.stderr.write(`${message}
2896
+ `);
2897
+ }
2898
+ function resolveCwd(directory, source = "--cwd") {
2899
+ const resolved = path5.resolve(directory);
2900
+ if (!existsSync4(resolved) || !statSync3(resolved).isDirectory()) {
2901
+ throw new Error(`comments: ${source} ${directory} is not a directory`);
2902
+ }
2903
+ return realpathSync(resolved);
2904
+ }
2905
+ function parseInteger(name, value, minimum) {
2906
+ if (value === void 0) {
2907
+ return void 0;
2908
+ }
2909
+ const number = Number(value);
2910
+ if (!Number.isInteger(number) || number < minimum) {
2911
+ throw new Error(`comments: --${name} must be an integer of at least ${minimum}, got ${value}`);
2912
+ }
2913
+ return number;
2914
+ }
2915
+ function defined(object) {
2916
+ return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== void 0));
2917
+ }
2918
+ function applyOverrides(config, values) {
2919
+ const overrides = defined({
2920
+ width: parseInteger("width", values.width, 1),
2921
+ maxWidth: parseInteger("max-width", values["max-width"], 1),
2922
+ singleLineWidth: parseInteger("single-line-width", values["single-line-width"], 1),
2923
+ tolerance: parseInteger("tolerance", values.tolerance, 0),
2924
+ avoidSentenceOrphans: values.orphans,
2925
+ style: values.style,
2926
+ ignore: values.ignore ? [...config.ignore ?? [], ...values.ignore] : void 0
2927
+ });
2928
+ const agent = defined({
2929
+ model: values.model,
2930
+ timeout: parseInteger("timeout", values.timeout, 1),
2931
+ batchSize: parseInteger("batch-size", values["batch-size"], 1),
2932
+ maxFiles: parseInteger("max-files", values["max-files"], 1),
2933
+ maxAgents: parseInteger("max-agents", values["max-agents"], 1),
2934
+ instructions: values.instructions
2935
+ });
2936
+ return { ...config, ...overrides, agent: { ...config.agent, ...agent } };
2937
+ }
2938
+ async function createProject(cwd, values) {
2939
+ const repoRoot = findRepoRoot(cwd);
2940
+ const root = repoRoot ?? cwd;
2941
+ const loaded = await loadConfig(cwd, { explicitPath: values.config, stopDir: root });
2942
+ const config = applyOverrides(loaded.config, values);
2943
+ const project = new Project(root, config, { isGitRepo: repoRoot !== void 0, cwd });
2944
+ return { project, configPath: loaded.filepath };
2945
+ }
2946
+ function scopeFrom(positionals, values) {
2947
+ return { targets: positionals, staged: values.staged, base: values.base, repo: values.repo };
2948
+ }
2949
+ function selectFrom(values, project) {
2950
+ if (values.all) {
2951
+ return "all";
2952
+ }
2953
+ return values.suspicious ? "suspicious" : project.config.agent?.scope ?? "suspicious";
2954
+ }
2955
+ function formatIssue(issue) {
2956
+ return issue.tier === "hint" ? `${issue.type} (hint)` : issue.type;
2957
+ }
2958
+ function formatComment(comment) {
2959
+ const issueList = comment.paragraphs.flatMap((paragraph) => paragraph.issues);
2960
+ const issues = /* @__PURE__ */ new Map();
2961
+ for (const issue of issueList) {
2962
+ issues.set(issue.line, [...issues.get(issue.line) ?? [], formatIssue(issue)]);
2963
+ }
2964
+ const typesOf = (tier) => {
2965
+ return [...new Set(issueList.filter((issue) => issue.tier === tier).map((issue) => issue.type))];
2966
+ };
2967
+ const verdicts = typesOf("verdict");
2968
+ const hints = typesOf("hint");
2969
+ const labels = [
2970
+ ...verdicts.length > 0 ? [`suspicious: ${verdicts.join(", ")}`] : [],
2971
+ ...hints.length > 0 ? [`hints: ${hints.join(", ")}`] : []
2972
+ ];
2973
+ const status = labels.length > 0 ? labels.join(", ") : "ok";
2974
+ const range = `${comment.startLine}-${comment.endLine}`;
2975
+ const location = comment.startLine === comment.endLine ? comment.startLine : range;
2976
+ const maximum = comment.maxWidth === void 0 ? "" : `, maximum ${comment.maxWidth}`;
2977
+ const output = [`${comment.file}:${location} ${comment.kind}, width ${comment.width}, ${status}${maximum}`];
2978
+ const lines = comment.text.split("\n");
2979
+ const numberWidth = String(comment.endLine).length;
2980
+ for (const [offset, text] of lines.entries()) {
2981
+ const line = comment.startLine + offset;
2982
+ const marks = issues.get(line);
2983
+ const length = String(comment.lengths[offset]).padStart(3);
2984
+ output.push(
2985
+ ` ${String(line).padStart(numberWidth)} ${length} | ${text}${marks ? ` <- ${marks.join(", ")}` : ""}`
2986
+ );
2987
+ }
2988
+ return output.join("\n");
2989
+ }
2990
+ function printComments(comments, values) {
2991
+ if (values.json) {
2992
+ process.stdout.write(`${JSON.stringify(comments, null, 2)}
2993
+ `);
2994
+ return;
2995
+ }
2996
+ for (const comment of comments) {
2997
+ process.stdout.write(`${formatComment(comment)}
2998
+
2999
+ `);
3000
+ }
3001
+ }
3002
+ function commentsOf(analyses) {
3003
+ return analyses.flatMap((analysis) => analysis.comments);
3004
+ }
3005
+ function hintEmptyScope(analyses, scope) {
3006
+ const isDefaultScope = (scope.targets ?? []).length === 0 && !scope.repo && !scope.base;
3007
+ if (analyses.length === 0 && isDefaultScope) {
3008
+ const changes = scope.staged ? "staged changes" : "uncommitted changes";
3009
+ log2(
3010
+ `comments: no ${changes} in JavaScript or TypeScript files. Pass --repo, --base <ref>, or files to widen the scope.`
3011
+ );
3012
+ }
3013
+ }
3014
+ function warnSkipped(analyses) {
3015
+ for (const skipped of skippedFiles(analyses)) {
3016
+ log2(`comments: ${describeSkipped(skipped)}`);
3017
+ }
3018
+ }
3019
+ function parseRunner(value) {
3020
+ if (value !== void 0 && !RUNNERS.has(value)) {
3021
+ throw new Error(`comments: unknown runner ${value}, expected claude, codex, or cursor`);
3022
+ }
3023
+ return value;
3024
+ }
3025
+ async function runWrap(project, targets, values) {
3026
+ if (targets.length === 0 || targets.some((target) => !/:\d+(?:-\d+)?$/.test(target))) {
3027
+ throw new Error("comments: wrap expects file:line targets, e.g. `comments wrap src/a.ts:12`");
3028
+ }
3029
+ const results = [];
3030
+ for (const target of targets) {
3031
+ const analyses = await project.analyze({ targets: [target] });
3032
+ const comments = commentsOf(analyses);
3033
+ const skipped = skippedFiles(analyses).at(0);
3034
+ if (skipped) {
3035
+ throw new Error(`comments: ${describeSkipped(skipped)}`);
3036
+ }
3037
+ if (comments.length === 0) {
3038
+ throw new Error(`comments: no comment at ${target}`);
3039
+ }
3040
+ for (const comment of comments) {
3041
+ results.push({
3042
+ file: comment.file,
3043
+ startLine: comment.startLine,
3044
+ endLine: comment.endLine,
3045
+ text: rewrapComment(comment)
3046
+ });
3047
+ }
3048
+ }
3049
+ if (values.json) {
3050
+ process.stdout.write(`${JSON.stringify(results, null, 2)}
3051
+ `);
3052
+ } else {
3053
+ process.stdout.write(`${results.map((result) => result.text).join("\n\n")}
3054
+ `);
3055
+ }
3056
+ return 0;
3057
+ }
3058
+ async function readStdin() {
3059
+ if (process.stdin.isTTY) {
3060
+ return "";
3061
+ }
3062
+ const chunks = [];
3063
+ for await (const chunk of process.stdin) {
3064
+ chunks.push(chunk);
3065
+ }
3066
+ return Buffer.concat(chunks).toString("utf8");
3067
+ }
3068
+ function hookOutput(host, message) {
3069
+ if (host === "cursor" || !message) {
3070
+ return "{}";
3071
+ }
3072
+ return JSON.stringify({ systemMessage: message });
3073
+ }
3074
+ function parsePayload(raw) {
3075
+ const payload = raw.trim() ? JSON.parse(raw) : {};
3076
+ return payload !== null && typeof payload === "object" ? payload : {};
3077
+ }
3078
+ function payloadDirectory(payload) {
3079
+ const roots = payload.workspace_roots;
3080
+ const candidates = [Array.isArray(roots) ? roots.at(0) : void 0, payload.cwd];
3081
+ return candidates.find((candidate) => typeof candidate === "string" && candidate !== "");
3082
+ }
3083
+ async function runHook(argv) {
3084
+ const host = peek(argv).host ?? "claude";
3085
+ let message;
3086
+ try {
3087
+ const { values, positionals } = parse(argv);
3088
+ if (!RUNNERS.has(host)) {
3089
+ throw new Error(`comments: unknown host ${host}, expected cursor, claude, or codex`);
3090
+ }
3091
+ const payload = parsePayload(await readStdin());
3092
+ if (process.env[CHILD_ENV] || payload.status !== void 0 && payload.status !== "completed") {
3093
+ return;
3094
+ }
3095
+ const directory = values.cwd ?? payloadDirectory(payload);
3096
+ const source = values.cwd === void 0 ? "the hook directory" : "--cwd";
3097
+ const cwd = directory === void 0 ? process.cwd() : resolveCwd(directory, source);
3098
+ const { project, configPath } = await createProject(cwd, values);
3099
+ const mode = values.mode ?? project.config.hook?.mode ?? "agent";
3100
+ if (!MODES.has(mode)) {
3101
+ throw new Error(`comments: unknown mode ${mode}`);
3102
+ }
3103
+ if (mode === "off") {
3104
+ return;
3105
+ }
3106
+ const summary = await runFlow(project, scopeFrom(positionals.slice(1), values), {
3107
+ mode,
3108
+ select: selectFrom(values, project),
3109
+ runner: parseRunner(values.runner) ?? project.config.agent?.runner ?? host,
3110
+ configPath,
3111
+ reporter: createLogReporter(log2, { verbose: values.verbose })
3112
+ });
3113
+ message = describeSummary(summary);
3114
+ } catch (error) {
3115
+ message = `comments: ${error.message.replace(/^comments: /, "")}`;
3116
+ } finally {
3117
+ if (message) {
3118
+ log2(message);
3119
+ }
3120
+ process.stdout.write(`${hookOutput(host, message)}
3121
+ `);
3122
+ }
3123
+ }
3124
+ async function main(argv) {
3125
+ const peeked = peek(argv);
3126
+ if (peeked.command === "hook" && !peeked.help) {
3127
+ await runHook(argv);
3128
+ return 0;
3129
+ }
3130
+ const { values, positionals } = parse(argv);
3131
+ const [command, ...targets] = positionals;
3132
+ if (values.help || !command || command === "help") {
3133
+ process.stdout.write(HELP);
3134
+ return 0;
3135
+ }
3136
+ const cwd = values.cwd ? resolveCwd(values.cwd) : process.cwd();
3137
+ const { project, configPath } = await createProject(cwd, values);
3138
+ const scope = scopeFrom(targets, values);
3139
+ switch (command) {
3140
+ case "list": {
3141
+ const analyses = await project.analyze(scope);
3142
+ const comments = commentsOf(analyses);
3143
+ printComments(values.suspicious ? comments.filter((comment) => isSuspicious(comment)) : comments, values);
3144
+ warnSkipped(analyses);
3145
+ hintEmptyScope(analyses, scope);
3146
+ return 0;
3147
+ }
3148
+ case "check": {
3149
+ const analyses = await project.analyze(scope);
3150
+ const comments = commentsOf(analyses).filter((comment) => hasVerdictIssues(comment));
3151
+ printComments(comments, values);
3152
+ warnSkipped(analyses);
3153
+ hintEmptyScope(analyses, scope);
3154
+ if (!values.json) {
3155
+ log2(comments.length > 0 ? `${comments.length} comment(s) with layout issues` : "all comments fit the width");
3156
+ }
3157
+ return comments.length > 0 ? 1 : 0;
3158
+ }
3159
+ case "wrap": {
3160
+ return runWrap(project, targets, values);
3161
+ }
3162
+ case "agent": {
3163
+ const options = {
3164
+ mode: "agent",
3165
+ select: selectFrom(values, project),
3166
+ runner: parseRunner(values.runner) ?? project.config.agent?.runner ?? "claude",
3167
+ configPath
3168
+ };
3169
+ let summary;
3170
+ if (isLiveTerminal()) {
3171
+ const ui = new TerminalUI();
3172
+ ui.begin("comments agent");
3173
+ try {
3174
+ summary = await runFlow(project, scope, { ...options, reporter: ui.reporter });
3175
+ } catch (error) {
3176
+ ui.dispose();
3177
+ throw error;
3178
+ }
3179
+ await ui.finish(summary, {
3180
+ interactive: process.stdin.isTTY && values.interactive !== false,
3181
+ verbose: values.verbose
3182
+ });
3183
+ } else {
3184
+ summary = await runFlow(project, scope, {
3185
+ ...options,
3186
+ reporter: createLogReporter(log2, { verbose: values.verbose })
3187
+ });
3188
+ log2(describeSummary(summary) ?? "comments: nothing to do");
3189
+ }
3190
+ const failed = summary.remaining.length > 0 || summary.codeChanged.length > 0 || summary.agentError;
3191
+ return failed ? 1 : 0;
3192
+ }
3193
+ default: {
3194
+ log2(`comments: unknown command ${command}
3195
+ `);
3196
+ process.stderr.write(HELP);
3197
+ return 2;
3198
+ }
3199
+ }
3200
+ }
3201
+
3202
+ // src/comments/bin.ts
3203
+ try {
3204
+ process.exitCode = await main(process.argv.slice(2));
3205
+ } catch (error) {
3206
+ log2(error.message);
3207
+ process.exitCode = 2;
3208
+ }
3209
+ /**
3210
+ * Legal comments are kept as they are. A comment is legal when it uses the same markers
3211
+ * as bundlers like esbuild (`/*!`, `//!`, `@license`, and `@preserve`), or when it is a
3212
+ * copyright notice like `Copyright (c) 2020 Foo`.
3213
+ */
3214
+ // a code span only names a marker, like the `@license` in a comment that explains legal comments
3215
+ //# sourceMappingURL=comments.js.map