prreviewbuddy 0.12.0 → 0.12.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.
@@ -404,11 +404,7 @@ var GitError = class extends Error {
404
404
  };
405
405
  async function git(cwd, args, options = {}) {
406
406
  try {
407
- const { stdout } = await execFileAsync$1("git", [
408
- "-c",
409
- "core.quotePath=false",
410
- ...args
411
- ], {
407
+ const { stdout } = await execFileAsync$1("git", [...["core.quotePath=false", ...options.config ?? []].flatMap((one) => ["-c", one]), ...args], {
412
408
  cwd,
413
409
  encoding: "utf8",
414
410
  maxBuffer: MAX_BUFFER$1,
@@ -2092,6 +2088,25 @@ function relativeTime(ms) {
2092
2088
  */
2093
2089
  var MANAGED_ROOT = join(STORE_ROOT, "worktrees");
2094
2090
  /**
2091
+ * An empty directory, pointed at so that the repository's own hooks do not run in our checkout.
2092
+ *
2093
+ * `git worktree add` checks the tree out, which fires `post-checkout` in the reviewer's repository.
2094
+ * Found on a 31,644 file monorepo with husky: its hook asks for `HEAD@{1}`, a brand new worktree's
2095
+ * HEAD reflog has exactly one entry, git answers `fatal: log for 'HEAD' only has 1 entries`, and the
2096
+ * add exits 128 *after* writing the whole tree. Every review of that repository failed, in the first
2097
+ * phase, for a reason that was nothing to do with the review.
2098
+ *
2099
+ * Turning them off is the right answer rather than a workaround. These hooks were written for a
2100
+ * person checking out a branch to work on: install dependencies, warn about a stale lockfile, prompt
2101
+ * something. Nobody works in this directory. It is read once by an agent and deleted. It is also
2102
+ * arbitrary code from the repository being reviewed, and a review that runs it has quietly made
2103
+ * "point PR Review Buddy at this repository" mean "execute this repository".
2104
+ *
2105
+ * A directory rather than `/dev/null` because git is being told where hooks live, and a real empty
2106
+ * one is true on every platform.
2107
+ */
2108
+ var NO_HOOKS = join(STORE_ROOT, "no-hooks");
2109
+ /**
2095
2110
  * The proof that we made a directory, kept inside the directory itself.
2096
2111
  *
2097
2112
  * It exists for the removal guard. Git normally answers "is this yours", and when git has
@@ -2101,14 +2116,26 @@ var MANAGED_ROOT = join(STORE_ROOT, "worktrees");
2101
2116
  var MARKER_FILE = ".prreviewbuddy-worktree.json";
2102
2117
  async function createWorktree(input) {
2103
2118
  mkdirSync(MANAGED_ROOT, { recursive: true });
2119
+ mkdirSync(NO_HOOKS, { recursive: true });
2104
2120
  const path = join(MANAGED_ROOT, input.jobId);
2105
- await git(input.originRepoPath, [
2106
- "worktree",
2107
- "add",
2108
- "--detach",
2109
- path,
2110
- input.sha
2111
- ], { timeoutMs: WORKTREE_TIMEOUT_MS });
2121
+ try {
2122
+ await git(input.originRepoPath, [
2123
+ "worktree",
2124
+ "add",
2125
+ "--detach",
2126
+ path,
2127
+ input.sha
2128
+ ], {
2129
+ config: [`core.hooksPath=${NO_HOOKS}`],
2130
+ timeoutMs: WORKTREE_TIMEOUT_MS
2131
+ });
2132
+ } catch (error) {
2133
+ await removeWorktree({
2134
+ originRepoPath: input.originRepoPath,
2135
+ worktreePath: path
2136
+ }).catch(() => {});
2137
+ throw error;
2138
+ }
2112
2139
  const marker = {
2113
2140
  jobId: input.jobId,
2114
2141
  originRepoPath: input.originRepoPath,
package/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as agentById, A as WORK_PHASES, F as loadJob, G as saveWorkspace, H as positionInLineage, K as summarise, L as displayRef, O as relativeTime, P as isTerminal, Q as AGENT_IDS, V as loadWorkspace, W as recentWorkspaces, X as resolveTarget, Z as withBase, d as ensureServer, f as readServerRecord, g as BUILD_VERSION, i as updateReview, k as checkFreshness, l as LANDING_PAGE, m as workspaceUrl, n as record, nt as AgentUnavailableError, p as reviewsUrl, tt as defaultAgent, v as runJob, w as isClaimed, x as forgeResolver, y as startJob, z as groupByLineage } from "./log-DdPaO6Wo.js";
2
+ import { $ as agentById, A as WORK_PHASES, F as loadJob, G as saveWorkspace, H as positionInLineage, K as summarise, L as displayRef, O as relativeTime, P as isTerminal, Q as AGENT_IDS, V as loadWorkspace, W as recentWorkspaces, X as resolveTarget, Z as withBase, d as ensureServer, f as readServerRecord, g as BUILD_VERSION, i as updateReview, k as checkFreshness, l as LANDING_PAGE, m as workspaceUrl, n as record, nt as AgentUnavailableError, p as reviewsUrl, tt as defaultAgent, v as runJob, w as isClaimed, x as forgeResolver, y as startJob, z as groupByLineage } from "./log-PmeEgome.js";
3
3
  import { basename } from "node:path";
4
4
  import { spawn } from "node:child_process";
5
5
  import { platform } from "node:process";
@@ -318,6 +318,23 @@ function restates(note, label) {
318
318
  return bare(note) === bare(label);
319
319
  }
320
320
  /**
321
+ * How many rows this block will actually occupy, which is not how many strings it is.
322
+ *
323
+ * The redraw moves the cursor up by a count and clears from there, so that count has to be in the
324
+ * unit the terminal moves in: rows on screen, after wrapping. Counting strings instead lands the
325
+ * cursor short and clears output the block never drew. The header's second line carries a full
326
+ * repository path and wraps in any normal window, so this was not a corner case.
327
+ *
328
+ * Escape sequences are removed first because they occupy no columns. A resize between drawing and
329
+ * redrawing still misplaces the cursor; nothing short of tracking the resize can fix that, and the
330
+ * next repaint corrects itself.
331
+ */
332
+ function rowsOccupied(lines, columns) {
333
+ if (!columns) return lines.length;
334
+ const visible = (line) => line.replace(/\u001b\[[0-9;]*m/g, "").length;
335
+ return lines.reduce((rows, line) => rows + Math.max(1, Math.ceil(visible(line) / columns)), 0);
336
+ }
337
+ /**
321
338
  * Watch a job and keep one block on the screen describing it.
322
339
  *
323
340
  * Returns the stop function the caller must call: nothing here may outlive the command, and a
@@ -339,12 +356,15 @@ function followJob(jobId, target, options = {}) {
339
356
  if (inPlace && drawn > 0) stream.write(`\u001b[${drawn}A\u001b[0J`);
340
357
  stream.write(`${block}\n`);
341
358
  last = block;
342
- drawn = lines.length;
359
+ drawn = rowsOccupied(lines, stream.columns ?? 0);
343
360
  };
344
361
  const timer = setInterval(paint, POLL_INTERVAL_MS);
345
362
  timer.unref();
346
363
  paint();
364
+ let stopped = false;
347
365
  return () => {
366
+ if (stopped) return;
367
+ stopped = true;
348
368
  clearInterval(timer);
349
369
  paint();
350
370
  };
@@ -675,10 +695,12 @@ async function drive(workspaceId, url) {
675
695
  try {
676
696
  finished = await runJob(workspaceId);
677
697
  if (finished.phase === "failed") {
698
+ stop();
678
699
  err(reviewFailed(finished.failure?.message ?? "The reason was not recorded.", await linkNow(workspace, url)));
679
700
  return 1;
680
701
  }
681
702
  } catch (error) {
703
+ stop();
682
704
  err(reviewFailed(error instanceof Error ? error.message : String(error), await linkNow(workspace, url)));
683
705
  return 1;
684
706
  } finally {
package/dist/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { B as lineagePosition, C as clearClaim, D as removeWorktree, E as readMarker, F as loadJob, G as saveWorkspace, I as saveJob, J as processDiscussion, M as deleteJob, N as fail, O as relativeTime$1, P as isTerminal, R as STORE_ROOT, S as fillFileUrlTemplate, T as MANAGED_ROOT, U as recentReviewGroups, V as loadWorkspace, Y as describeAuthorship, _ as feedbackUrl, a as refreshPrContext, b as EXPLAIN_SIMPLY_PROMPT, c as agentEnvOf, et as agentFor, g as BUILD_VERSION, h as writeServerRecord, i as updateReview, j as allJobs, k as checkFreshness, n as record, nt as AgentUnavailableError, o as isUnchanged, q as touchWorkspace, r as RefreshUnavailableError, s as askCheckout, t as readEvents, u as clearServerRecord, v as runJob, w as isClaimed } from "./log-DdPaO6Wo.js";
1
+ import { B as lineagePosition, C as clearClaim, D as removeWorktree, E as readMarker, F as loadJob, G as saveWorkspace, I as saveJob, J as processDiscussion, M as deleteJob, N as fail, O as relativeTime$1, P as isTerminal, R as STORE_ROOT, S as fillFileUrlTemplate, T as MANAGED_ROOT, U as recentReviewGroups, V as loadWorkspace, Y as describeAuthorship, _ as feedbackUrl, a as refreshPrContext, b as EXPLAIN_SIMPLY_PROMPT, c as agentEnvOf, et as agentFor, g as BUILD_VERSION, h as writeServerRecord, i as updateReview, j as allJobs, k as checkFreshness, n as record, nt as AgentUnavailableError, o as isUnchanged, q as touchWorkspace, r as RefreshUnavailableError, s as askCheckout, t as readEvents, u as clearServerRecord, v as runJob, w as isClaimed } from "./log-PmeEgome.js";
2
2
  import { isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { randomBytes, timingSafeEqual } from "node:crypto";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "prreviewbuddy",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "PR Review Buddy at the command line: review a branch in an isolated checkout and open the workspace.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://prreviewbuddy.com",
7
7
  "type": "module",
8
8
  "bin": {
9
- "prreviewbuddy": "./dist/main.js"
9
+ "prreviewbuddy": "dist/main.js"
10
10
  },
11
11
  "files": [
12
12
  "dist",