@plannotator/pi-extension 0.16.7 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -79,6 +79,20 @@ function isSearchableMarkdownPath(input: string): boolean {
79
79
  return MARKDOWN_PATH_REGEX.test(input.trim());
80
80
  }
81
81
 
82
+ function stripWrappingQuotes(input: string): string {
83
+ if (input.length < 2) {
84
+ return input;
85
+ }
86
+
87
+ const first = input[0];
88
+ const last = input[input.length - 1];
89
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
90
+ return input.slice(1, -1);
91
+ }
92
+
93
+ return input;
94
+ }
95
+
82
96
  /** Check if a path looks like a Windows absolute path (e.g. C:\ or C:/) */
83
97
  function hasWindowsDriveLetter(input: string): boolean {
84
98
  return /^[a-zA-Z]:[/\\]/.test(input);
@@ -156,12 +170,10 @@ export function isAbsoluteMarkdownPath(
156
170
  * @param input - User-provided path (absolute, relative, or bare filename)
157
171
  * @param projectRoot - Project root directory to search within
158
172
  */
159
- export function resolveMarkdownFile(
173
+ function resolveMarkdownFileCore(
160
174
  input: string,
161
175
  projectRoot: string,
162
176
  ): ResolveResult {
163
- // Trim whitespace/CR that may leak from Windows shell pipelines
164
- input = input.trim();
165
177
  const normalizedInput = normalizeMarkdownPathInput(input);
166
178
  const searchInput = normalizeSeparators(normalizedInput);
167
179
  const isBareFilename = !searchInput.includes("/");
@@ -219,6 +231,47 @@ export function resolveMarkdownFile(
219
231
  return { kind: "not_found", input };
220
232
  }
221
233
 
234
+ /**
235
+ * Resolve a markdown file path within a project root.
236
+ *
237
+ * @param input - User-provided path (absolute, relative, or bare filename)
238
+ * @param projectRoot - Project root directory to search within
239
+ */
240
+ export function resolveMarkdownFile(
241
+ input: string,
242
+ projectRoot: string,
243
+ ): ResolveResult {
244
+ const originalInput = input.trim();
245
+ const unquotedInput = stripWrappingQuotes(originalInput);
246
+
247
+ const primary = resolveMarkdownFileCore(unquotedInput, projectRoot);
248
+ if (primary.kind === "found") {
249
+ return primary;
250
+ }
251
+ if (primary.kind === "ambiguous") {
252
+ return { ...primary, input: originalInput };
253
+ }
254
+
255
+ if (!unquotedInput.startsWith("@")) {
256
+ return { kind: "not_found", input: originalInput };
257
+ }
258
+
259
+ const normalizedInput = unquotedInput.replace(/^@+/, "");
260
+ if (!normalizedInput) {
261
+ return { kind: "not_found", input: originalInput };
262
+ }
263
+
264
+ const fallback = resolveMarkdownFileCore(normalizedInput, projectRoot);
265
+ if (fallback.kind === "found") {
266
+ return fallback;
267
+ }
268
+ if (fallback.kind === "ambiguous") {
269
+ return { ...fallback, input: originalInput };
270
+ }
271
+
272
+ return { kind: "not_found", input: originalInput };
273
+ }
274
+
222
275
  /**
223
276
  * Check if a directory contains at least one markdown file.
224
277
  * Used to validate folder annotation targets.
@@ -14,6 +14,7 @@ export type DiffType =
14
14
  | "unstaged"
15
15
  | "last-commit"
16
16
  | "branch"
17
+ | "merge-base"
17
18
  | `worktree:${string}`
18
19
  | "p4-default"
19
20
  | `p4-changelist:${string}`;
@@ -151,6 +152,7 @@ export async function getGitContext(
151
152
 
152
153
  if (currentBranch !== defaultBranch) {
153
154
  diffOptions.push({ id: "branch", label: `vs ${defaultBranch}` });
155
+ diffOptions.push({ id: "merge-base", label: `Current PR Diff` });
154
156
  }
155
157
 
156
158
  const [worktrees, currentTreePathResult] = await Promise.all([
@@ -386,6 +388,28 @@ export async function runGitDiff(
386
388
  break;
387
389
  }
388
390
 
391
+ case "merge-base": {
392
+ const mergeBaseResult = assertGitSuccess(
393
+ await runtime.runGit(["merge-base", defaultBranch, "HEAD"], { cwd }),
394
+ ["merge-base", defaultBranch, "HEAD"],
395
+ );
396
+ const mergeBase = mergeBaseResult.stdout.trim();
397
+ const mergeBaseDiffArgs = [
398
+ "diff",
399
+ "--no-ext-diff",
400
+ `${mergeBase}..HEAD`,
401
+ "--src-prefix=a/",
402
+ "--dst-prefix=b/",
403
+ ];
404
+ const mergeBaseDiff = assertGitSuccess(
405
+ await runtime.runGit(mergeBaseDiffArgs, { cwd }),
406
+ mergeBaseDiffArgs,
407
+ );
408
+ patch = mergeBaseDiff.stdout;
409
+ label = `PR diff vs ${defaultBranch}`;
410
+ break;
411
+ }
412
+
389
413
  default:
390
414
  return { patch: "", label: "Unknown diff type" };
391
415
  }
@@ -475,6 +499,14 @@ export async function getFileContentsForDiff(
475
499
  oldContent: await gitShow(defaultBranch, oldFilePath),
476
500
  newContent: await gitShow("HEAD", filePath),
477
501
  };
502
+ case "merge-base": {
503
+ const mbResult = await runtime.runGit(["merge-base", defaultBranch, "HEAD"], { cwd });
504
+ const mb = mbResult.exitCode === 0 ? mbResult.stdout.trim() : defaultBranch;
505
+ return {
506
+ oldContent: await gitShow(mb, oldFilePath),
507
+ newContent: await gitShow("HEAD", filePath),
508
+ };
509
+ }
478
510
  default:
479
511
  return { oldContent: null, newContent: null };
480
512
  }
@@ -0,0 +1,120 @@
1
+ // @generated — DO NOT EDIT. Source: packages/shared/worktree.ts
2
+ /**
3
+ * Worktree — runtime-agnostic git worktree primitives.
4
+ *
5
+ * Uses ReviewGitRuntime so both Bun and Node runtimes can use the same logic.
6
+ * Lives in packages/shared/ and gets vendored to Pi via vendor.sh.
7
+ *
8
+ * Designed as composable primitives, not tied to any specific use case.
9
+ * PR local checkout, agent sandboxes, parallel sessions — all compose from these.
10
+ */
11
+
12
+ import type { ReviewGitRuntime } from "./review-core";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Types
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export interface CreateWorktreeOptions {
19
+ /** Git ref to check out (branch name, SHA, FETCH_HEAD, etc.) */
20
+ ref: string;
21
+ /** Absolute path where the worktree will be created. */
22
+ path: string;
23
+ /** Create in detached HEAD mode (no branch). Default: false. */
24
+ detach?: boolean;
25
+ /** CWD of the source repository. Defaults to process.cwd(). */
26
+ cwd?: string;
27
+ }
28
+
29
+ export interface RemoveWorktreeOptions {
30
+ /** Force removal even if the worktree has modifications. Default: false. */
31
+ force?: boolean;
32
+ /** CWD of the source repository. Required if the worktree was created from a different cwd. */
33
+ cwd?: string;
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Primitives
38
+ // ---------------------------------------------------------------------------
39
+
40
+ /**
41
+ * Fetch a ref from origin.
42
+ * Runs: `git fetch origin <ref>`
43
+ * Throws on failure.
44
+ */
45
+ export async function fetchRef(
46
+ runtime: ReviewGitRuntime,
47
+ ref: string,
48
+ options?: { cwd?: string },
49
+ ): Promise<void> {
50
+ const result = await runtime.runGit(["fetch", "origin", "--", ref], { cwd: options?.cwd });
51
+ if (result.exitCode !== 0) {
52
+ throw new Error(`git fetch origin ${ref} failed: ${result.stderr.trim() || `exit code ${result.exitCode}`}`);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Ensure a git object (commit SHA) is available locally.
58
+ * Checks with `git cat-file -t`, fetches from origin if missing.
59
+ * Returns true if the object is available after the attempt.
60
+ */
61
+ export async function ensureObjectAvailable(
62
+ runtime: ReviewGitRuntime,
63
+ sha: string,
64
+ options?: { cwd?: string },
65
+ ): Promise<boolean> {
66
+ const check = await runtime.runGit(["cat-file", "-t", sha], { cwd: options?.cwd });
67
+ if (check.exitCode === 0) return true;
68
+
69
+ // Object missing locally — try fetching it
70
+ const fetch = await runtime.runGit(["fetch", "origin", "--", sha], { cwd: options?.cwd });
71
+ if (fetch.exitCode !== 0) return false;
72
+
73
+ // Verify it's now available
74
+ const recheck = await runtime.runGit(["cat-file", "-t", sha], { cwd: options?.cwd });
75
+ return recheck.exitCode === 0;
76
+ }
77
+
78
+ /**
79
+ * Create a git worktree.
80
+ * Runs: `git worktree add [--detach] <path> <ref>`
81
+ * Throws on failure with a descriptive error.
82
+ */
83
+ export async function createWorktree(
84
+ runtime: ReviewGitRuntime,
85
+ options: CreateWorktreeOptions,
86
+ ): Promise<{ worktreePath: string }> {
87
+ const args = ["worktree", "add"];
88
+ if (options.detach) args.push("--detach");
89
+ args.push(options.path, options.ref);
90
+
91
+ const result = await runtime.runGit(args, { cwd: options.cwd });
92
+ if (result.exitCode !== 0) {
93
+ throw new Error(`git worktree add failed: ${result.stderr.trim() || `exit code ${result.exitCode}`}`);
94
+ }
95
+
96
+ return { worktreePath: options.path };
97
+ }
98
+
99
+ /**
100
+ * Remove a git worktree. Best-effort — logs errors but does not throw.
101
+ * Runs: `git worktree remove [--force] <path>`
102
+ */
103
+ export async function removeWorktree(
104
+ runtime: ReviewGitRuntime,
105
+ worktreePath: string,
106
+ options?: RemoveWorktreeOptions,
107
+ ): Promise<void> {
108
+ const args = ["worktree", "remove"];
109
+ if (options?.force) args.push("--force");
110
+ args.push(worktreePath);
111
+
112
+ try {
113
+ const result = await runtime.runGit(args, { cwd: options?.cwd });
114
+ if (result.exitCode !== 0) {
115
+ console.error(`Warning: git worktree remove failed for ${worktreePath}: ${result.stderr.trim()}`);
116
+ }
117
+ } catch (err) {
118
+ console.error(`Warning: worktree cleanup error: ${err instanceof Error ? err.message : String(err)}`);
119
+ }
120
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plannotator/pi-extension",
3
- "version": "0.16.7",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "description": "Plannotator extension for Pi coding agent - interactive plan review with visual annotation",
6
6
  "author": "backnotprop",