@plannotator/pi-extension 0.26.0 → 0.26.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.
@@ -43,6 +43,32 @@ const VERSION_TIMEOUT_MS = 5_000;
43
43
  const OBJECT_ID_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
44
44
  const STATUS_CACHE_MS = 1_000;
45
45
 
46
+ /**
47
+ * GitButler has shipped two spellings of the global JSON output flag:
48
+ * 0.21.x accepts only `--format json` (gitbutlerapp/gitbutler#14061) while
49
+ * 0.22.0+ accepts only `--json` (gitbutlerapp/gitbutler#15026), and each
50
+ * release rejects the other spelling as an unknown argument. The primary
51
+ * spelling matches GITBUTLER_MIN_VERSION; when a status call fails with
52
+ * clap's unexpected-argument rejection for the exact flag we passed, the
53
+ * other spelling is tried once. Any other failure propagates unchanged.
54
+ */
55
+ interface GitButlerStatusSyntax {
56
+ args: readonly string[];
57
+ /** The flag clap names in its unexpected-argument rejection. */
58
+ flag: string;
59
+ label: string;
60
+ }
61
+
62
+ const GITBUTLER_STATUS_SYNTAXES: readonly [GitButlerStatusSyntax, GitButlerStatusSyntax] = [
63
+ { args: ["--format", "json", "status"], flag: "--format", label: "but --format json status" },
64
+ { args: ["--json", "status"], flag: "--json", label: "but --json status" },
65
+ ];
66
+
67
+ /** Narrow sniff for clap rejecting exactly the JSON flag this syntax passed. */
68
+ function isUnexpectedArgumentRejection(result: GitCommandResult, flag: string): boolean {
69
+ return result.exitCode !== 0 && result.stderr.includes(`unexpected argument '${flag}'`);
70
+ }
71
+
46
72
  /** Runtime operations needed by the shared GitButler provider. */
47
73
  export interface ReviewGitButlerRuntime extends ReviewGitRuntime {
48
74
  runBut(args: string[], options?: GitCommandOptions): Promise<GitCommandResult>;
@@ -68,7 +94,7 @@ export interface GitButlerStack {
68
94
  branches: GitButlerBranch[];
69
95
  }
70
96
 
71
- /** Validated subset of `but --format json status`. */
97
+ /** Validated subset of `but --format json status` (0.21.x) / `but --json status` (0.22.0+). */
72
98
  export interface GitButlerStatus {
73
99
  mergeBase: GitButlerCommit;
74
100
  stacks: GitButlerStack[];
@@ -154,12 +180,15 @@ function parseStack(value: unknown, path: string): GitButlerStack {
154
180
  }
155
181
 
156
182
  /** Parse and validate the status fields Plannotator relies on. Unknown fields are allowed. */
157
- export function parseGitButlerStatus(output: string): GitButlerStatus {
183
+ export function parseGitButlerStatus(
184
+ output: string,
185
+ commandLabel = GITBUTLER_STATUS_SYNTAXES[0].label,
186
+ ): GitButlerStatus {
158
187
  let decoded: unknown;
159
188
  try {
160
189
  decoded = JSON.parse(output);
161
190
  } catch {
162
- throw new GitButlerContractError("GitButler returned invalid JSON from `but --format json status`.");
191
+ throw new GitButlerContractError(`GitButler returned invalid JSON from \`${commandLabel}\`.`);
163
192
  }
164
193
 
165
194
  const root = requireRecord(decoded, "root");
@@ -203,6 +232,8 @@ function versionAtLeast(actual: ParsedVersion, minimum: ParsedVersion): boolean
203
232
  }
204
233
 
205
234
  const versionChecks = new WeakMap<ReviewGitButlerRuntime, Promise<void>>();
235
+ /** Index into GITBUTLER_STATUS_SYNTAXES of the spelling this runtime's CLI last accepted. */
236
+ const statusSyntaxPreferences = new WeakMap<ReviewGitButlerRuntime, 0 | 1>();
206
237
  const statusCaches = new WeakMap<
207
238
  ReviewGitButlerRuntime,
208
239
  Map<string, { expiresAt: number; inFlight: boolean; status: Promise<GitButlerStatus> }>
@@ -256,17 +287,42 @@ async function loadStatus(runtime: ReviewGitButlerRuntime, cwd: string): Promise
256
287
  if (existing && (existing.inFlight || existing.expiresAt > now)) return existing.status;
257
288
 
258
289
  const status = (async () => {
259
- const result = await runtime.runBut(["--format", "json", "status"], {
290
+ const preferredIndex = statusSyntaxPreferences.get(runtime) ?? 0;
291
+ let syntaxIndex = preferredIndex;
292
+ let syntax = GITBUTLER_STATUS_SYNTAXES[syntaxIndex];
293
+ let result = await runtime.runBut([...syntax.args], {
260
294
  cwd,
261
295
  timeoutMs: STATUS_TIMEOUT_MS,
262
296
  });
297
+ if (isUnexpectedArgumentRejection(result, syntax.flag)) {
298
+ // This CLI does not know this spelling of the JSON flag; try the other
299
+ // spelling exactly once. Only clap's unexpected-argument rejection for
300
+ // the flag we passed triggers the retry — a real status failure must
301
+ // keep failing loudly below.
302
+ syntaxIndex = preferredIndex === 0 ? 1 : 0;
303
+ const fallback = GITBUTLER_STATUS_SYNTAXES[syntaxIndex];
304
+ const retried = await runtime.runBut([...fallback.args], {
305
+ cwd,
306
+ timeoutMs: STATUS_TIMEOUT_MS,
307
+ });
308
+ if (isUnexpectedArgumentRejection(retried, fallback.flag)) {
309
+ throw new GitButlerContractError(
310
+ `GitButler rejected both \`${syntax.label}\` and \`${fallback.label}\`; ` +
311
+ `Plannotator requires GitButler ${GITBUTLER_MIN_VERSION} or newer.`,
312
+ );
313
+ }
314
+ syntax = fallback;
315
+ result = retried;
316
+ }
263
317
  if (result.exitCode !== 0) {
264
318
  const detail = result.stderr.trim();
265
319
  throw new GitButlerContractError(
266
- `GitButler status failed${detail ? `: ${detail}` : ` with exit code ${result.exitCode}`}`,
320
+ `GitButler status (\`${syntax.label}\`) failed${detail ? `: ${detail}` : ` with exit code ${result.exitCode}`}`,
267
321
  );
268
322
  }
269
- return parseGitButlerStatus(result.stdout);
323
+ const parsed = parseGitButlerStatus(result.stdout, syntax.label);
324
+ statusSyntaxPreferences.set(runtime, syntaxIndex);
325
+ return parsed;
270
326
  })();
271
327
  const entry = { expiresAt: 0, inFlight: true, status };
272
328
  cache.set(cwd, entry);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plannotator/pi-extension",
3
- "version": "0.26.0",
3
+ "version": "0.26.1",
4
4
  "type": "module",
5
5
  "description": "Plannotator Pi extension - interactive plan review with annotations, annotate agent messages, and review code/PRs",
6
6
  "author": "backnotprop",