@blogic-cz/agent-tools 0.14.41 → 0.14.42

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "0.14.41",
3
+ "version": "0.14.42",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, sessions, and audit",
5
5
  "keywords": [
6
6
  "agent",
@@ -40,6 +40,7 @@ import {
40
40
  prReplyAndResolveCommand,
41
41
  prReviewTriageBatchCommand,
42
42
  prReviewTriageCommand,
43
+ prWaitMergeableCommand,
43
44
  } from "./pr/index";
44
45
  import { branchRenameCommand } from "./branch";
45
46
  import {
@@ -74,6 +75,7 @@ const prCommand = Command.make("pr", {}).pipe(
74
75
  prCloseCommand,
75
76
  prEditCommand,
76
77
  prMergeCommand,
78
+ prWaitMergeableCommand,
77
79
  prThreadsCommand,
78
80
  prCommentsCommand,
79
81
  prIssueCommentsCommand,
@@ -1,10 +1,20 @@
1
1
  import { Command, Flag } from "effect/unstable/cli";
2
- import { Effect, Option } from "effect";
2
+ import { Console, Effect, Option } from "effect";
3
3
 
4
4
  import type { CheckResult, PRStatusResult } from "#gh/types";
5
5
 
6
6
  import { formatOption, logFormatted } from "#shared";
7
7
  import { GitHubService } from "#gh/service";
8
+ import { GitHubCommandError } from "#gh/errors";
9
+
10
+ const emptyBatchError = (batch: string) =>
11
+ new GitHubCommandError({
12
+ message: `--prs received no valid PR numbers: ${JSON.stringify(batch)}`,
13
+ command: "gh pr --prs",
14
+ exitCode: 1,
15
+ stderr: "",
16
+ hint: "Pass comma-separated positive integers, e.g. --prs 1,2,3.",
17
+ });
8
18
  import {
9
19
  resolveDefaultTextInput,
10
20
  resolveOptionalTextInput,
@@ -29,6 +39,7 @@ import {
29
39
  mergePR,
30
40
  rerunChecks,
31
41
  viewPR,
42
+ waitForMergeable,
32
43
  } from "./core";
33
44
  import {
34
45
  fetchComments,
@@ -127,18 +138,34 @@ export const prViewCommand = Command.make(
127
138
  Flag.withDescription("PR number (default: current branch PR)"),
128
139
  Flag.optional,
129
140
  ),
141
+ prs: Flag.string("prs").pipe(
142
+ Flag.withDescription("Comma-separated PR numbers to view in one call (overrides --pr)"),
143
+ Flag.optional,
144
+ ),
130
145
  repo: repoOption,
131
146
  },
132
- ({ format, pr, repo }) =>
147
+ ({ format, pr, prs, repo }) =>
133
148
  withRepo(
134
149
  repo,
135
150
  Effect.gen(function* () {
136
- const prNumber = Option.getOrNull(pr);
137
- const info = yield* viewPR(prNumber);
151
+ const batch = Option.getOrNull(prs);
152
+ if (batch !== null) {
153
+ const numbers = parsePrNumbers(batch);
154
+ if (numbers.length === 0) return yield* emptyBatchError(batch);
155
+ const results = yield* Effect.all(
156
+ numbers.map((n) => viewPR(n).pipe(Effect.map((info) => ({ pr: n, info })))),
157
+ { concurrency: 5 },
158
+ );
159
+ yield* logFormatted({ count: results.length, prs: results }, format);
160
+ return;
161
+ }
162
+ const info = yield* viewPR(Option.getOrNull(pr));
138
163
  yield* logFormatted(info, format);
139
164
  }),
140
165
  ),
141
- ).pipe(Command.withDescription("View PR information"));
166
+ ).pipe(
167
+ Command.withDescription("View PR information (use --prs 1,2,3 to view several in one call)"),
168
+ );
142
169
 
143
170
  export const prStatusCommand = Command.make(
144
171
  "status",
@@ -200,6 +227,36 @@ export const prListCommand = Command.make(
200
227
  ),
201
228
  );
202
229
 
230
+ export const prWaitMergeableCommand = Command.make(
231
+ "wait-mergeable",
232
+ {
233
+ format: formatOption,
234
+ pr: Flag.integer("pr").pipe(
235
+ Flag.withDescription("PR number (default: current branch PR)"),
236
+ Flag.optional,
237
+ ),
238
+ timeout: Flag.integer("timeout").pipe(
239
+ Flag.withDescription(
240
+ "Max seconds to wait for a definitive mergeable verdict (capped at 180)",
241
+ ),
242
+ Flag.withDefault(60),
243
+ ),
244
+ repo: repoOption,
245
+ },
246
+ ({ format, pr, timeout, repo }) =>
247
+ withRepo(
248
+ repo,
249
+ Effect.gen(function* () {
250
+ const info = yield* waitForMergeable(Option.getOrNull(pr), timeout);
251
+ yield* logFormatted(info, format);
252
+ }),
253
+ ),
254
+ ).pipe(
255
+ Command.withDescription(
256
+ "Poll until GitHub reports a definitive mergeable verdict (MERGEABLE/CONFLICTING) or timeout",
257
+ ),
258
+ );
259
+
203
260
  export const prCreateCommand = Command.make(
204
261
  "create",
205
262
  {
@@ -388,6 +445,10 @@ export const prChecksCommand = Command.make(
388
445
  Flag.withDescription("PR number (default: current branch PR)"),
389
446
  Flag.optional,
390
447
  ),
448
+ prs: Flag.string("prs").pipe(
449
+ Flag.withDescription("Comma-separated PR numbers for a one-shot batch snapshot (no --watch)"),
450
+ Flag.optional,
451
+ ),
391
452
  repo: repoOption,
392
453
  timeout: Flag.integer("timeout").pipe(
393
454
  Flag.withDefault(CI_CHECK_WATCH_TIMEOUT_MS / 1000),
@@ -398,16 +459,39 @@ export const prChecksCommand = Command.make(
398
459
  Flag.withDescription("Watch until checks complete or timeout"),
399
460
  ),
400
461
  },
401
- ({ failFast, format, pr, repo, timeout, watch }) =>
462
+ ({ failFast, format, pr, prs, repo, timeout, watch }) =>
402
463
  withRepo(
403
464
  repo,
404
465
  Effect.gen(function* () {
405
- const prNumber = Option.getOrNull(pr);
406
- const checks = yield* fetchChecksForCommand(prNumber, watch, failFast, timeout);
466
+ const batch = Option.getOrNull(prs);
467
+ if (batch !== null) {
468
+ if (watch) {
469
+ yield* Console.warn(
470
+ "ℹ️ --watch is ignored with --prs; batch mode returns a one-shot snapshot per PR.",
471
+ );
472
+ }
473
+ const numbers = parsePrNumbers(batch);
474
+ if (numbers.length === 0) return yield* emptyBatchError(batch);
475
+ const results = yield* Effect.all(
476
+ numbers.map((n) =>
477
+ fetchChecks(n, false, failFast, timeout).pipe(
478
+ Effect.map((checks) => ({ pr: n, checks })),
479
+ ),
480
+ ),
481
+ { concurrency: 5 },
482
+ );
483
+ yield* logFormatted({ count: results.length, prs: results }, format);
484
+ return;
485
+ }
486
+ const checks = yield* fetchChecksForCommand(Option.getOrNull(pr), watch, failFast, timeout);
407
487
  yield* logFormatted(checks, format);
408
488
  }),
409
489
  ),
410
- ).pipe(Command.withDescription("Fetch CI check status for a PR (optionally watch with timeout)"));
490
+ ).pipe(
491
+ Command.withDescription(
492
+ "Fetch CI check status for a PR (--watch to block; --prs 1,2,3 for a batch snapshot)",
493
+ ),
494
+ );
411
495
 
412
496
  export const prChecksFailedCommand = Command.make(
413
497
  "checks-failed",
@@ -1,4 +1,4 @@
1
- import { Console, Effect, Option, Result } from "effect";
1
+ import { Clock, Console, Duration, Effect, Option, Result } from "effect";
2
2
 
3
3
  import type {
4
4
  BranchPRDetail,
@@ -482,6 +482,41 @@ export const listPRs = Effect.fn("pr.listPRs")(function* (opts: {
482
482
  return yield* gh.runGhJson<PRInfo[]>(args);
483
483
  });
484
484
 
485
+ // GitHub recomputes mergeability asynchronously after CI; poll until it settles out of "UNKNOWN".
486
+ const MAX_MERGEABLE_WAIT_SECONDS = 180;
487
+ const MERGEABLE_POLL_INTERVAL_MS = 3000;
488
+
489
+ export const waitForMergeable = Effect.fn("pr.waitForMergeable")(function* (
490
+ pr: number | null,
491
+ timeoutSeconds: number,
492
+ ) {
493
+ const cappedSeconds = Math.min(timeoutSeconds, MAX_MERGEABLE_WAIT_SECONDS);
494
+ const start = yield* Clock.currentTimeMillis;
495
+ const deadlineMs = Number(start) + cappedSeconds * 1000;
496
+
497
+ // Effect.whileLoop (not recursion) so TestClock.adjust can advance Effect.sleep without real waits.
498
+ let latest = yield* viewPR(pr);
499
+ let timedOut = false;
500
+ yield* Effect.whileLoop({
501
+ while: () => latest.mergeable === "UNKNOWN" && !timedOut,
502
+ body: () =>
503
+ Effect.gen(function* () {
504
+ const now = yield* Clock.currentTimeMillis;
505
+ if (Number(now) >= deadlineMs) {
506
+ timedOut = true;
507
+ return;
508
+ }
509
+ // Cap the sleep to the remaining budget so the total wait doesn't overshoot the deadline.
510
+ const remaining = deadlineMs - Number(now);
511
+ yield* Effect.sleep(Duration.millis(Math.min(MERGEABLE_POLL_INTERVAL_MS, remaining)));
512
+ latest = yield* viewPR(pr);
513
+ }),
514
+ step: () => undefined,
515
+ });
516
+
517
+ return latest;
518
+ });
519
+
485
520
  export const createPR = Effect.fn("pr.createPR")(function* (opts: {
486
521
  base: string | null;
487
522
  title: string;
@@ -18,6 +18,7 @@ export {
18
18
  prStatusCommand,
19
19
  prSubmitReviewCommand,
20
20
  prThreadsCommand,
21
+ prWaitMergeableCommand,
21
22
  prReviewTriageCommand,
22
23
  prReviewTriageBatchCommand,
23
24
  prViewCommand,