@blogic-cz/agent-tools 1.7.4 → 1.8.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.
Files changed (128) hide show
  1. package/README.md +2 -1
  2. package/dist/db-tool/service.d.ts.map +1 -1
  3. package/dist/gh-tool/branch.d.ts +2 -3
  4. package/dist/gh-tool/branch.d.ts.map +1 -1
  5. package/dist/gh-tool/environment.d.ts +1 -1
  6. package/dist/gh-tool/environment.d.ts.map +1 -1
  7. package/dist/gh-tool/gist.d.ts +8 -28
  8. package/dist/gh-tool/gist.d.ts.map +1 -1
  9. package/dist/gh-tool/issue/commands.d.ts +9 -9
  10. package/dist/gh-tool/issue/commands.d.ts.map +1 -1
  11. package/dist/gh-tool/issue/core.d.ts +103 -9
  12. package/dist/gh-tool/issue/core.d.ts.map +1 -1
  13. package/dist/gh-tool/issue/triage.d.ts +6 -6
  14. package/dist/gh-tool/issue/triage.d.ts.map +1 -1
  15. package/dist/gh-tool/pr/commands.d.ts +53 -46
  16. package/dist/gh-tool/pr/commands.d.ts.map +1 -1
  17. package/dist/gh-tool/pr/core.d.ts +179 -74
  18. package/dist/gh-tool/pr/core.d.ts.map +1 -1
  19. package/dist/gh-tool/pr/review.d.ts +16 -16
  20. package/dist/gh-tool/pr/review.d.ts.map +1 -1
  21. package/dist/gh-tool/pr/snapshot.d.ts +9 -0
  22. package/dist/gh-tool/pr/snapshot.d.ts.map +1 -0
  23. package/dist/gh-tool/pr/stack-read.d.ts +1 -6
  24. package/dist/gh-tool/pr/stack-read.d.ts.map +1 -1
  25. package/dist/gh-tool/pr/stack.d.ts +3 -3
  26. package/dist/gh-tool/pr/stack.d.ts.map +1 -1
  27. package/dist/gh-tool/{api.d.ts → read/api.d.ts} +9 -3
  28. package/dist/gh-tool/read/api.d.ts.map +1 -0
  29. package/dist/gh-tool/read/cache.d.ts +44 -0
  30. package/dist/gh-tool/read/cache.d.ts.map +1 -0
  31. package/dist/gh-tool/read/catalog.d.ts +170 -0
  32. package/dist/gh-tool/read/catalog.d.ts.map +1 -0
  33. package/dist/gh-tool/read/classes.d.ts +94 -0
  34. package/dist/gh-tool/read/classes.d.ts.map +1 -0
  35. package/dist/gh-tool/read/commands.d.ts +3 -0
  36. package/dist/gh-tool/read/commands.d.ts.map +1 -0
  37. package/dist/gh-tool/read/gated.d.ts +14 -0
  38. package/dist/gh-tool/read/gated.d.ts.map +1 -0
  39. package/dist/gh-tool/read/index.d.ts +5 -0
  40. package/dist/gh-tool/read/index.d.ts.map +1 -0
  41. package/dist/gh-tool/read/issues.d.ts +68 -0
  42. package/dist/gh-tool/read/issues.d.ts.map +1 -0
  43. package/dist/gh-tool/read/keys.d.ts +61 -0
  44. package/dist/gh-tool/read/keys.d.ts.map +1 -0
  45. package/dist/gh-tool/read/porcelain.d.ts +68 -0
  46. package/dist/gh-tool/read/porcelain.d.ts.map +1 -0
  47. package/dist/gh-tool/read/pulls.d.ts +137 -0
  48. package/dist/gh-tool/read/pulls.d.ts.map +1 -0
  49. package/dist/gh-tool/read/reads.d.ts +209 -0
  50. package/dist/gh-tool/read/reads.d.ts.map +1 -0
  51. package/dist/gh-tool/read/rest.d.ts +49 -0
  52. package/dist/gh-tool/read/rest.d.ts.map +1 -0
  53. package/dist/gh-tool/read/review-guards.d.ts +20 -0
  54. package/dist/gh-tool/read/review-guards.d.ts.map +1 -0
  55. package/dist/gh-tool/read/review-model.d.ts +190 -0
  56. package/dist/gh-tool/read/review-model.d.ts.map +1 -0
  57. package/dist/gh-tool/read/reviews.d.ts +9 -0
  58. package/dist/gh-tool/read/reviews.d.ts.map +1 -0
  59. package/dist/gh-tool/read/stacks.d.ts +7 -0
  60. package/dist/gh-tool/read/stacks.d.ts.map +1 -0
  61. package/dist/gh-tool/read/store.d.ts +70 -0
  62. package/dist/gh-tool/read/store.d.ts.map +1 -0
  63. package/dist/gh-tool/{service.d.ts → read/transport.d.ts} +5 -3
  64. package/dist/gh-tool/read/transport.d.ts.map +1 -0
  65. package/dist/gh-tool/read/waits.d.ts +22 -0
  66. package/dist/gh-tool/read/waits.d.ts.map +1 -0
  67. package/dist/gh-tool/read/writes.d.ts +16 -0
  68. package/dist/gh-tool/read/writes.d.ts.map +1 -0
  69. package/dist/gh-tool/release.d.ts +10 -26
  70. package/dist/gh-tool/release.d.ts.map +1 -1
  71. package/dist/gh-tool/repo.d.ts +4 -4
  72. package/dist/gh-tool/repo.d.ts.map +1 -1
  73. package/dist/gh-tool/types.d.ts +1 -1
  74. package/dist/gh-tool/types.d.ts.map +1 -1
  75. package/dist/gh-tool/workflow.d.ts +43 -21
  76. package/dist/gh-tool/workflow.d.ts.map +1 -1
  77. package/dist/observability-tool/shared.d.ts.map +1 -1
  78. package/dist/shared/content-security.d.ts +2 -0
  79. package/dist/shared/content-security.d.ts.map +1 -1
  80. package/package.json +6 -6
  81. package/src/api-tool/proxy-entry.ts +3 -1
  82. package/src/db-tool/service.ts +4 -1
  83. package/src/gh-tool/branch.ts +5 -8
  84. package/src/gh-tool/environment.ts +2 -1
  85. package/src/gh-tool/gist.ts +8 -35
  86. package/src/gh-tool/issue/commands.ts +1 -1
  87. package/src/gh-tool/issue/core.ts +55 -122
  88. package/src/gh-tool/issue/triage.ts +4 -26
  89. package/src/gh-tool/main.ts +5 -2
  90. package/src/gh-tool/pr/commands.ts +19 -23
  91. package/src/gh-tool/pr/core.ts +542 -582
  92. package/src/gh-tool/pr/review.ts +106 -454
  93. package/src/gh-tool/pr/snapshot.ts +23 -0
  94. package/src/gh-tool/pr/stack-read.ts +1 -73
  95. package/src/gh-tool/pr/stack.ts +37 -36
  96. package/src/gh-tool/{api.ts → read/api.ts} +40 -7
  97. package/src/gh-tool/read/cache.ts +192 -0
  98. package/src/gh-tool/read/catalog.ts +310 -0
  99. package/src/gh-tool/read/classes.ts +30 -0
  100. package/src/gh-tool/read/commands.ts +25 -0
  101. package/src/gh-tool/read/gated.ts +56 -0
  102. package/src/gh-tool/read/index.ts +4 -0
  103. package/src/gh-tool/read/issues.ts +107 -0
  104. package/src/gh-tool/read/keys.ts +102 -0
  105. package/src/gh-tool/read/porcelain.ts +58 -0
  106. package/src/gh-tool/read/pulls.ts +266 -0
  107. package/src/gh-tool/read/reads.ts +306 -0
  108. package/src/gh-tool/read/rest.ts +111 -0
  109. package/src/gh-tool/read/review-guards.ts +61 -0
  110. package/src/gh-tool/read/review-model.ts +286 -0
  111. package/src/gh-tool/read/reviews.ts +114 -0
  112. package/src/gh-tool/read/stacks.ts +73 -0
  113. package/src/gh-tool/read/store.ts +504 -0
  114. package/src/gh-tool/{service.ts → read/transport.ts} +26 -6
  115. package/src/gh-tool/read/waits.ts +52 -0
  116. package/src/gh-tool/read/writes.ts +37 -0
  117. package/src/gh-tool/release.ts +15 -168
  118. package/src/gh-tool/repo.ts +5 -78
  119. package/src/gh-tool/types.ts +1 -1
  120. package/src/gh-tool/workflow.ts +123 -214
  121. package/src/observability-tool/shared.ts +2 -1
  122. package/src/shared/content-security.ts +10 -1
  123. package/src/shared/credential-patterns.ts +1 -1
  124. package/dist/gh-tool/api.d.ts.map +0 -1
  125. package/dist/gh-tool/pr/check-status-cache.d.ts +0 -6
  126. package/dist/gh-tool/pr/check-status-cache.d.ts.map +0 -1
  127. package/dist/gh-tool/service.d.ts.map +0 -1
  128. package/src/gh-tool/pr/check-status-cache.ts +0 -142
@@ -3,7 +3,9 @@ import { Effect, Option } from "effect";
3
3
 
4
4
  import { formatOption, logFormatted } from "#shared";
5
5
  import type { BranchRenameResult } from "./types";
6
- import { GitHubService } from "./service";
6
+ import { writes } from "./read/writes";
7
+ import { resolveRepo } from "./read/reads";
8
+ import { repoSlug } from "./read/keys";
7
9
 
8
10
  // ---------------------------------------------------------------------------
9
11
  // Internal handlers
@@ -15,8 +17,6 @@ export const renameBranch = Effect.fn("branch.renameBranch")(function* (opts: {
15
17
  confirm: boolean;
16
18
  repo: string | null;
17
19
  }) {
18
- const gh = yield* GitHubService;
19
-
20
20
  if (!opts.confirm) {
21
21
  const scope = opts.repo !== null ? ` in ${opts.repo}` : "";
22
22
 
@@ -31,10 +31,7 @@ export const renameBranch = Effect.fn("branch.renameBranch")(function* (opts: {
31
31
  return dryRun;
32
32
  }
33
33
 
34
- const repoInfo =
35
- opts.repo !== null
36
- ? opts.repo
37
- : yield* gh.getRepoInfo().pipe(Effect.map((r) => `${r.owner}/${r.name}`));
34
+ const repoInfo = opts.repo !== null ? opts.repo : repoSlug(yield* resolveRepo(null));
38
35
 
39
36
  const args = [
40
37
  "api",
@@ -45,7 +42,7 @@ export const renameBranch = Effect.fn("branch.renameBranch")(function* (opts: {
45
42
  `new_name=${opts.newName}`,
46
43
  ];
47
44
 
48
- yield* gh.runGh(args);
45
+ yield* writes.run(args, [`repo:${repoInfo.toLowerCase()}`]);
49
46
 
50
47
  const result: BranchRenameResult = {
51
48
  renamed: true,
@@ -41,10 +41,11 @@ const GH_ENVIRONMENT_NAMES = [
41
41
 
42
42
  export const ghEnvironment = (
43
43
  environment: Record<string, string | undefined> = process.env,
44
+ credential: string | null = null,
44
45
  ): Record<string, string> =>
45
46
  Object.fromEntries(
46
47
  GH_ENVIRONMENT_NAMES.flatMap((name) => {
47
- const value = environment[name];
48
+ const value = name === "GH_TOKEN" && credential !== null ? credential : environment[name];
48
49
  return value === undefined ? [] : [[name, value]];
49
50
  }),
50
51
  );
@@ -1,3 +1,6 @@
1
+ import { passthrough } from "./read/catalog";
2
+ import type { GistApi } from "./read/catalog";
3
+ import { writes } from "./read/writes";
1
4
  import { Command, Flag } from "effect/cli";
2
5
  import { Effect, Option } from "effect";
3
6
  import { mkdtemp, rm, writeFile } from "node:fs/promises";
@@ -11,33 +14,12 @@ import {
11
14
  resolveOptionalTextInput,
12
15
  } from "#gh/text-input";
13
16
  import { GitHubCommandError } from "./errors";
14
- import { GitHubService } from "./service";
15
17
 
16
18
  // ---------------------------------------------------------------------------
17
19
  // Types
18
20
  // ---------------------------------------------------------------------------
19
21
 
20
22
  // `gh gist list/view` have no --json, so reads go through `gh api gists` (REST shape).
21
- type GistApiFile = {
22
- filename: string;
23
- language: string | null;
24
- type: string;
25
- size: number;
26
- truncated?: boolean;
27
- content?: string;
28
- };
29
-
30
- type GistApi = {
31
- id: string;
32
- description: string | null;
33
- public: boolean;
34
- html_url: string;
35
- created_at: string;
36
- updated_at: string;
37
- owner: { login: string } | null;
38
- files: Record<string, GistApiFile>;
39
- };
40
-
41
23
  type GistListItem = {
42
24
  id: string;
43
25
  description: string | null;
@@ -247,9 +229,7 @@ const listGists = Effect.fn("gist.listGists")(function* (opts: {
247
229
  limit: number;
248
230
  visibility: string | null;
249
231
  }) {
250
- const gh = yield* GitHubService;
251
-
252
- const gists = yield* gh.runGhJson<GistApi[]>(["api", `gists?per_page=${opts.limit}`]);
232
+ const gists = yield* passthrough.gistList(opts.limit);
253
233
 
254
234
  return gists
255
235
  .filter(
@@ -264,9 +244,7 @@ const viewGist = Effect.fn("gist.viewGist")(function* (opts: {
264
244
  filename: string | null;
265
245
  withContent: boolean;
266
246
  }) {
267
- const gh = yield* GitHubService;
268
-
269
- const gist = yield* gh.runGhJson<GistApi>(["api", `gists/${opts.id}`]);
247
+ const gist = yield* passthrough.gistView(opts.id);
270
248
 
271
249
  return toGistDetail(gist, { filename: opts.filename, withContent: opts.withContent });
272
250
  });
@@ -276,7 +254,6 @@ export const createGist = Effect.fn("gist.createGist")(function* (opts: {
276
254
  description: string | null;
277
255
  public: boolean;
278
256
  }) {
279
- const gh = yield* GitHubService;
280
257
  return yield* Effect.scoped(
281
258
  Effect.gen(function* () {
282
259
  const snapshots: string[] = [];
@@ -298,7 +275,7 @@ export const createGist = Effect.fn("gist.createGist")(function* (opts: {
298
275
  args.push("--public");
299
276
  }
300
277
 
301
- const result = yield* gh.runGh(args);
278
+ const result = yield* writes.run(args, []);
302
279
  const url =
303
280
  result.stdout
304
281
  .trim()
@@ -333,8 +310,6 @@ export const editGist = Effect.fn("gist.editGist")(function* (opts: {
333
310
  filename: string | null;
334
311
  sourcePath: string | null;
335
312
  }) {
336
- const gh = yield* GitHubService;
337
-
338
313
  const args = ["gist", "edit", opts.id];
339
314
  const changes: string[] = [];
340
315
 
@@ -371,7 +346,7 @@ export const editGist = Effect.fn("gist.editGist")(function* (opts: {
371
346
  args.push("--filename", opts.filename);
372
347
  }
373
348
 
374
- yield* gh.runGh(args);
349
+ yield* writes.run(args, []);
375
350
 
376
351
  const edited: GistEditResult = { edited: true, id: opts.id, changes };
377
352
 
@@ -379,8 +354,6 @@ export const editGist = Effect.fn("gist.editGist")(function* (opts: {
379
354
  });
380
355
 
381
356
  const deleteGist = Effect.fn("gist.deleteGist")(function* (opts: { id: string; confirm: boolean }) {
382
- const gh = yield* GitHubService;
383
-
384
357
  if (!opts.confirm) {
385
358
  const dryRun: GistDeleteResult = {
386
359
  deleted: false,
@@ -392,7 +365,7 @@ const deleteGist = Effect.fn("gist.deleteGist")(function* (opts: { id: string; c
392
365
  return dryRun;
393
366
  }
394
367
 
395
- yield* gh.runGh(["gist", "delete", opts.id, "--yes"]);
368
+ yield* writes.run(["gist", "delete", opts.id, "--yes"], []);
396
369
 
397
370
  const deleted: GistDeleteResult = { deleted: true, id: opts.id };
398
371
 
@@ -18,7 +18,7 @@ import {
18
18
  reopenIssue,
19
19
  viewIssue,
20
20
  } from "./core";
21
- import { GitHubService } from "#gh/service";
21
+ import { GitHubService } from "#gh/read/transport";
22
22
 
23
23
  const repoOption = Flag.String("repo").pipe(
24
24
  Flag.withDescription("Target repository profile name or owner/name"),
@@ -3,7 +3,11 @@ import { Effect } from "effect";
3
3
  import type { GitHubIssueCommentUrl, IssueComment, IssueCommentId, IsoTimestamp } from "#gh/types";
4
4
 
5
5
  import { GitHubCommandError } from "#gh/errors";
6
- import { GitHubService } from "#gh/service";
6
+ import { GitHubService } from "#gh/read/transport";
7
+ import { reads, resolveRepo } from "#gh/read/reads";
8
+ import { writes } from "#gh/read/writes";
9
+ import { project } from "#gh/read/porcelain";
10
+ import { repoSlug } from "#gh/read/keys";
7
11
 
8
12
  export type IssueInfo = {
9
13
  number: number;
@@ -34,52 +38,6 @@ export type RawIssueComment = {
34
38
  html_url: string;
35
39
  };
36
40
 
37
- const REST_PAGE_SIZE = 100;
38
-
39
- const parseJson = <T>(
40
- stdout: string,
41
- command: string,
42
- parseFailurePrefix: string,
43
- ): Effect.Effect<T, GitHubCommandError> =>
44
- Effect.try({
45
- try: () => JSON.parse(stdout) as T,
46
- catch: (error) =>
47
- new GitHubCommandError({
48
- command,
49
- exitCode: 0,
50
- stderr: `${parseFailurePrefix}: ${error instanceof Error ? error.message : String(error)}`,
51
- message: `${parseFailurePrefix}: ${error instanceof Error ? error.message : String(error)}`,
52
- }),
53
- });
54
-
55
- const fetchAllRestPages = Effect.fn("issue.fetchAllRestPages")(function* <T>(
56
- endpoint: string,
57
- command: string,
58
- parseFailurePrefix: string,
59
- ) {
60
- const service = yield* GitHubService;
61
-
62
- const results: T[] = [];
63
- let page = 1;
64
-
65
- while (true) {
66
- const separator = endpoint.includes("?") ? "&" : "?";
67
- const result = yield* service.runGh([
68
- "api",
69
- `${endpoint}${separator}per_page=${REST_PAGE_SIZE}&page=${page}`,
70
- ]);
71
-
72
- const rawPage = yield* parseJson<T[]>(result.stdout, command, parseFailurePrefix);
73
- results.push(...rawPage);
74
-
75
- if (rawPage.length < REST_PAGE_SIZE) {
76
- return results;
77
- }
78
-
79
- page += 1;
80
- }
81
- });
82
-
83
41
  const mapRawIssueComment = (comment: RawIssueComment): IssueComment => ({
84
42
  id: comment.id as IssueCommentId,
85
43
  commitSha: null,
@@ -95,35 +53,21 @@ export const listIssues = Effect.fn("issue.listIssues")(function* (opts: {
95
53
  labels: string | null;
96
54
  limit: number;
97
55
  }) {
98
- const gh = yield* GitHubService;
99
-
100
- const args = [
101
- "issue",
102
- "list",
103
- "--state",
104
- opts.state,
105
- "--limit",
106
- String(opts.limit),
107
- "--json",
108
- "number,title,state,url,labels,createdAt",
109
- ];
110
-
111
- if (opts.labels !== null) {
112
- args.push("--label", opts.labels);
113
- }
114
-
115
- return yield* gh.runGhJson<IssueListItem[]>(args);
56
+ return yield* reads.issueList(yield* resolveRepo(null), opts);
116
57
  });
117
58
 
118
59
  export const viewIssue = Effect.fn("issue.viewIssue")(function* (issueNumber: number) {
119
- const gh = yield* GitHubService;
120
-
121
- return yield* gh.runGhJson<IssueInfo>([
122
- "issue",
123
- "view",
124
- String(issueNumber),
125
- "--json",
126
- "number,title,state,url,labels,assignees,author,createdAt,closedAt",
60
+ const value = yield* reads.issueView(yield* resolveRepo(null), issueNumber);
61
+ return project(value, [
62
+ "number",
63
+ "title",
64
+ "state",
65
+ "url",
66
+ "labels",
67
+ "assignees",
68
+ "author",
69
+ "createdAt",
70
+ "closedAt",
127
71
  ]);
128
72
  });
129
73
 
@@ -133,14 +77,7 @@ export const fetchIssueComments = Effect.fn("issue.fetchIssueComments")(function
133
77
  author: string | null,
134
78
  bodyContains: string | null,
135
79
  ) {
136
- const gh = yield* GitHubService;
137
- const repoInfo = yield* gh.getRepoInfo();
138
-
139
- const raw = yield* fetchAllRestPages<RawIssueComment>(
140
- `repos/${repoInfo.owner}/${repoInfo.name}/issues/${issueNumber}/comments`,
141
- "gh-tool issue comments",
142
- "Failed to parse response",
143
- );
80
+ const raw = yield* reads.issueComments(yield* resolveRepo(null), issueNumber);
144
81
 
145
82
  let comments = raw.map(mapRawIssueComment);
146
83
 
@@ -167,46 +104,40 @@ export const closeIssue = Effect.fn("issue.closeIssue")(function* (opts: {
167
104
  comment: string | null;
168
105
  reason: string;
169
106
  }) {
170
- const gh = yield* GitHubService;
171
-
172
107
  const args = ["issue", "close", String(opts.issue), "--reason", opts.reason];
173
108
 
174
109
  if (opts.comment !== null) {
175
110
  args.push("--comment", opts.comment);
176
111
  }
177
112
 
178
- yield* gh.runGh(args);
179
-
180
- return yield* gh.runGhJson<IssueInfo>([
181
- "issue",
182
- "view",
183
- String(opts.issue),
184
- "--json",
185
- "number,title,state,url,labels,assignees,author,createdAt,closedAt",
113
+ const repo = yield* resolveRepo(null);
114
+ yield* writes.run(args, [
115
+ `issue:${repoSlug(repo)}#${opts.issue}`,
116
+ `issues:${repoSlug(repo)}`,
117
+ `issue-comments:${repoSlug(repo)}#${opts.issue}`,
186
118
  ]);
119
+
120
+ return yield* viewIssue(opts.issue);
187
121
  });
188
122
 
189
123
  export const reopenIssue = Effect.fn("issue.reopenIssue")(function* (opts: {
190
124
  issue: number;
191
125
  comment: string | null;
192
126
  }) {
193
- const gh = yield* GitHubService;
194
-
195
127
  const args = ["issue", "reopen", String(opts.issue)];
196
128
 
197
129
  if (opts.comment !== null) {
198
130
  args.push("--comment", opts.comment);
199
131
  }
200
132
 
201
- yield* gh.runGh(args);
202
-
203
- return yield* gh.runGhJson<IssueInfo>([
204
- "issue",
205
- "view",
206
- String(opts.issue),
207
- "--json",
208
- "number,title,state,url,labels,assignees,author,createdAt,closedAt",
133
+ const repo = yield* resolveRepo(null);
134
+ yield* writes.run(args, [
135
+ `issue:${repoSlug(repo)}#${opts.issue}`,
136
+ `issues:${repoSlug(repo)}`,
137
+ `issue-comments:${repoSlug(repo)}#${opts.issue}`,
209
138
  ]);
139
+
140
+ return yield* viewIssue(opts.issue);
210
141
  });
211
142
 
212
143
  export const commentOnIssue = Effect.fn("issue.commentOnIssue")(function* (opts: {
@@ -227,14 +158,20 @@ export const commentOnIssue = Effect.fn("issue.commentOnIssue")(function* (opts:
227
158
  );
228
159
  }
229
160
 
230
- const result = yield* gh.runGh([
231
- "api",
232
- "-X",
233
- "POST",
234
- `repos/${repoInfo.owner}/${repoInfo.name}/issues/${opts.issue}/comments`,
235
- "-f",
236
- `body=${opts.body}`,
237
- ]);
161
+ const result = yield* writes.run(
162
+ [
163
+ "api",
164
+ "-X",
165
+ "POST",
166
+ `repos/${repoInfo.owner}/${repoInfo.name}/issues/${opts.issue}/comments`,
167
+ "-f",
168
+ `body=${opts.body}`,
169
+ ],
170
+ [
171
+ `issue-comments:${repoInfo.owner.toLowerCase()}/${repoInfo.name.toLowerCase()}#${opts.issue}`,
172
+ `issue:${repoInfo.owner.toLowerCase()}/${repoInfo.name.toLowerCase()}#${opts.issue}`,
173
+ ],
174
+ );
238
175
 
239
176
  const rawComment = yield* Effect.try({
240
177
  try: () => JSON.parse(result.stdout) as RawIssueComment,
@@ -262,8 +199,6 @@ export const createIssue = Effect.fn("issue.createIssue")(function* (opts: {
262
199
  assignee: string | null;
263
200
  labels: string | null;
264
201
  }) {
265
- const gh = yield* GitHubService;
266
-
267
202
  const args = ["issue", "create", "--title", opts.title, "--body", opts.body];
268
203
 
269
204
  if (opts.assignee !== null) {
@@ -273,7 +208,8 @@ export const createIssue = Effect.fn("issue.createIssue")(function* (opts: {
273
208
  args.push("--label", opts.labels);
274
209
  }
275
210
 
276
- const result = yield* gh.runGh(args);
211
+ const repo = yield* resolveRepo(null);
212
+ const result = yield* writes.run(args, [`repo:${repoSlug(repo)}`]);
277
213
 
278
214
  const urlMatch = result.stdout.match(/\/issues\/(\d+)/);
279
215
  if (!urlMatch?.[1]) {
@@ -299,8 +235,6 @@ export const editIssue = Effect.fn("issue.editIssue")(function* (opts: {
299
235
  addAssignee: string | null;
300
236
  removeAssignee: string | null;
301
237
  }) {
302
- const gh = yield* GitHubService;
303
-
304
238
  const args = ["issue", "edit", String(opts.issue)];
305
239
 
306
240
  if (opts.title !== null) {
@@ -322,13 +256,12 @@ export const editIssue = Effect.fn("issue.editIssue")(function* (opts: {
322
256
  args.push("--remove-assignee", opts.removeAssignee);
323
257
  }
324
258
 
325
- yield* gh.runGh(args);
326
-
327
- return yield* gh.runGhJson<IssueInfo>([
328
- "issue",
329
- "view",
330
- String(opts.issue),
331
- "--json",
332
- "number,title,state,url,labels,assignees,author,createdAt,closedAt",
259
+ const repo = yield* resolveRepo(null);
260
+ yield* writes.run(args, [
261
+ `issue:${repoSlug(repo)}#${opts.issue}`,
262
+ `issues:${repoSlug(repo)}`,
263
+ `issue-comments:${repoSlug(repo)}#${opts.issue}`,
333
264
  ]);
265
+
266
+ return yield* viewIssue(opts.issue);
334
267
  });
@@ -4,10 +4,11 @@ import { Effect, Option, Schema } from "effect";
4
4
  import type { CheckResult, IssueComment, PRViewInfo, ReviewThread } from "#gh/types";
5
5
 
6
6
  import { formatOption, logFormatted } from "#shared";
7
- import { GitHubService } from "#gh/service";
7
+ import { GitHubService } from "#gh/read/transport";
8
8
  import { fetchReviewTriage } from "#gh/pr/commands";
9
9
 
10
10
  import { fetchIssueComments } from "./core";
11
+ import { reads, resolveRepo } from "#gh/read/reads";
11
12
 
12
13
  const repoOption = Flag.String("repo").pipe(
13
14
  Flag.withDescription("Target repository profile name or owner/name"),
@@ -23,19 +24,6 @@ const withRepo = <A, E, R>(repo: Option.Option<string>, effect: Effect.Effect<A,
23
24
  const TriageVerbosity = Schema.Literals(["compact", "full"]);
24
25
  type TriageVerbosity = typeof TriageVerbosity.Type;
25
26
 
26
- type RawTriageIssue = {
27
- number: number;
28
- title: string;
29
- state: string;
30
- url: string;
31
- labels: Array<{ name: string }>;
32
- assignees: Array<{ login: string }>;
33
- author: { login: string };
34
- body: string;
35
- createdAt: string;
36
- closedAt: string | null;
37
- };
38
-
39
27
  type TriageIssue = {
40
28
  number: number;
41
29
  title: string;
@@ -114,19 +102,9 @@ export const fetchIssueTriage = Effect.fn("issue.fetchIssueTriage")(function* (o
114
102
  issue: number;
115
103
  verbosity: TriageVerbosity;
116
104
  }) {
117
- const gh = yield* GitHubService;
118
-
105
+ const repo = yield* resolveRepo(null);
119
106
  const [rawIssue, comments] = yield* Effect.all(
120
- [
121
- gh.runGhJson<RawTriageIssue>([
122
- "issue",
123
- "view",
124
- String(opts.issue),
125
- "--json",
126
- "number,title,state,url,labels,assignees,author,body,createdAt,closedAt",
127
- ]),
128
- fetchIssueComments(opts.issue, null, null, null),
129
- ],
107
+ [reads.issueView(repo, opts.issue), fetchIssueComments(opts.issue, null, null, null)],
130
108
  { concurrency: "unbounded" },
131
109
  );
132
110
 
@@ -72,7 +72,9 @@ import {
72
72
  releaseViewCommand,
73
73
  } from "./release";
74
74
  import { repoInfoCommand, repoListCommand, repoSearchCodeCommand } from "./repo";
75
- import { GitHubService } from "./service";
75
+ import { GitHubService } from "./read/transport";
76
+ import { withReadCache } from "./read";
77
+ import { cacheCommand } from "./read/commands";
76
78
  import {
77
79
  workflowAnnotationsCommand,
78
80
  workflowCancelCommand,
@@ -240,6 +242,7 @@ WORKFLOW FOR AI AGENTS:
240
242
  releaseCommand,
241
243
  gistCommand,
242
244
  commandsCommand,
245
+ cacheCommand,
243
246
  ]),
244
247
  );
245
248
 
@@ -254,7 +257,7 @@ const MainLayer = GitHubService.layer.pipe(
254
257
  Layer.provideMerge(ConfigServiceLayer),
255
258
  );
256
259
 
257
- const program = withRedactedOutput(withAudit("gh", cli)).pipe(
260
+ const program = withReadCache(withRedactedOutput(withAudit("gh", cli))).pipe(
258
261
  Effect.provide(MainLayer),
259
262
  Effect.tapCause(renderCauseToStderr),
260
263
  );
@@ -1,10 +1,12 @@
1
+ import { reads, writes } from "#gh/read/index";
2
+ import { repoSlug } from "#gh/read/keys";
1
3
  import { Command, Flag, Param } from "effect/cli";
2
4
  import { Console, Effect, Option } from "effect";
3
5
 
4
6
  import type { CheckResult, PRStatusResult } from "#gh/types";
5
7
 
6
8
  import { formatOption, logFormatted, logText } from "#shared";
7
- import { GitHubService } from "#gh/service";
9
+ import { GitHubService } from "#gh/read/transport";
8
10
  import { GitHubCommandError } from "#gh/errors";
9
11
 
10
12
  const emptyBatchError = (batch: string) =>
@@ -40,6 +42,7 @@ import {
40
42
  editPR,
41
43
  fetchPRView,
42
44
  fetchChecks,
45
+ fetchChecksBatch,
43
46
  fetchChecksForCommand,
44
47
  fetchFailedChecks,
45
48
  listPRs,
@@ -151,20 +154,20 @@ export const requestReview = (pr: number, reviewers: string) =>
151
154
  const repo = yield* gh.getRepoInfo();
152
155
  // ponytail: not atomic — a concurrent request between GET and POST misreports newlyRequested.
153
156
  const pendingBefore = new Set(
154
- (yield* confirmedReviewers(
155
- yield* gh.runGhJson<RequestedReviewersResponse>([
156
- "api",
157
- `repos/${repo.owner}/${repo.name}/pulls/${pr}`,
158
- ]),
159
- )).map((login) => login.toLowerCase()),
157
+ (yield* confirmedReviewers(yield* reads.reviewGuards.requestedReviewers(repo, pr))).map(
158
+ (login) => login.toLowerCase(),
159
+ ),
160
+ );
161
+ const response = yield* writes.json<RequestedReviewersResponse>(
162
+ [
163
+ "api",
164
+ "--method",
165
+ "POST",
166
+ `repos/${repo.owner}/${repo.name}/pulls/${pr}/requested_reviewers`,
167
+ ...submittedReviewers.flatMap((reviewer) => ["-f", `reviewers[]=${reviewer}`]),
168
+ ],
169
+ [`pr:${repoSlug(repo)}#${pr}`, `reviews:${repoSlug(repo)}#${pr}`, `pulls:${repoSlug(repo)}`],
160
170
  );
161
- const response = yield* gh.runGhJson<RequestedReviewersResponse>([
162
- "api",
163
- "--method",
164
- "POST",
165
- `repos/${repo.owner}/${repo.name}/pulls/${pr}/requested_reviewers`,
166
- ...submittedReviewers.flatMap((reviewer) => ["-f", `reviewers[]=${reviewer}`]),
167
- ]);
168
171
  return {
169
172
  pr,
170
173
  submittedReviewers,
@@ -238,7 +241,7 @@ const withStableHead = Effect.fn("pr.withStableHead")(function* <A, E, R>(
238
241
  initial,
239
242
  (info) => collect(info.number, info.headSha),
240
243
  (info) =>
241
- fetchPRView(info.number).pipe(
244
+ fetchPRView(info.number, "origin").pipe(
242
245
  Effect.map((refreshed) => ({ ...refreshed, baseSha: initial.baseSha })),
243
246
  ),
244
247
  (before, after) => after.headSha === before.headSha,
@@ -877,14 +880,7 @@ export const prChecksCommand = Command.make(
877
880
  }
878
881
  }
879
882
  const numbers = yield* parsePrNumbers(batch);
880
- const results = yield* Effect.all(
881
- numbers.map((n) =>
882
- fetchChecks(n, false, failFast, timeout, format === "json").pipe(
883
- Effect.map((checks) => ({ pr: n, checks })),
884
- ),
885
- ),
886
- { concurrency: 5 },
887
- );
883
+ const results = yield* fetchChecksBatch(numbers);
888
884
  yield* logFormatted({ count: results.length, prs: results }, format);
889
885
  return;
890
886
  }