@blogic-cz/agent-tools 0.14.23 → 0.14.25
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 +6 -6
- package/src/config/index.ts +1 -0
- package/src/config/loader.ts +27 -1
- package/src/gh-tool/issue/commands.ts +32 -32
- package/src/gh-tool/pr/commands.ts +319 -210
- package/src/gh-tool/pr/core.ts +16 -7
- package/src/gh-tool/release.ts +16 -16
- package/src/gh-tool/service.ts +49 -7
- package/src/gh-tool/text-input.ts +48 -45
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blogic-cz/agent-tools",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.25",
|
|
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",
|
|
@@ -133,13 +133,13 @@
|
|
|
133
133
|
"test": "vitest run"
|
|
134
134
|
},
|
|
135
135
|
"dependencies": {
|
|
136
|
-
"@effect/platform-bun": "4.0.0-beta.
|
|
136
|
+
"@effect/platform-bun": "4.0.0-beta.74",
|
|
137
137
|
"@toon-format/toon": "2.1.0",
|
|
138
|
-
"effect": "4.0.0-beta.
|
|
138
|
+
"effect": "4.0.0-beta.74"
|
|
139
139
|
},
|
|
140
140
|
"devDependencies": {
|
|
141
|
-
"@effect/language-service": "0.
|
|
142
|
-
"@effect/vitest": "4.0.0-beta.
|
|
141
|
+
"@effect/language-service": "0.86.2",
|
|
142
|
+
"@effect/vitest": "4.0.0-beta.74",
|
|
143
143
|
"@types/bun": "1.3.12",
|
|
144
144
|
"oxfmt": "0.44.0",
|
|
145
145
|
"oxlint": "1.59.0",
|
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
"vitest": "^4.1.4"
|
|
148
148
|
},
|
|
149
149
|
"overrides": {
|
|
150
|
-
"@effect/platform-node-shared": "4.0.0-beta.
|
|
150
|
+
"@effect/platform-node-shared": "4.0.0-beta.74"
|
|
151
151
|
},
|
|
152
152
|
"engines": {
|
|
153
153
|
"bun": ">=1.0.0"
|
package/src/config/index.ts
CHANGED
package/src/config/loader.ts
CHANGED
|
@@ -318,10 +318,36 @@ export function getGitHubConfig(
|
|
|
318
318
|
if (keys.length === 0) return undefined;
|
|
319
319
|
|
|
320
320
|
if (profile) return repos[profile];
|
|
321
|
-
if (keys.length === 1) return repos[keys[0] ?? ""];
|
|
322
321
|
if ("default" in repos) return repos.default;
|
|
322
|
+
if (keys.length === 1) return repos[keys[0] ?? ""];
|
|
323
323
|
|
|
324
324
|
throw new Error(
|
|
325
325
|
`Multiple github profiles found: [${keys.join(", ")}]. Use --repo <name> to select one.`,
|
|
326
326
|
);
|
|
327
327
|
}
|
|
328
|
+
|
|
329
|
+
export function resolveGitHubRepoTarget(
|
|
330
|
+
config: AgentToolsConfig | undefined,
|
|
331
|
+
target?: string | null,
|
|
332
|
+
): string | undefined {
|
|
333
|
+
if (target && target.includes("/")) {
|
|
334
|
+
return target;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const repos = config?.github;
|
|
338
|
+
if (!repos) return target ?? undefined;
|
|
339
|
+
|
|
340
|
+
const keys = Object.keys(repos);
|
|
341
|
+
if (target) {
|
|
342
|
+
const repo = repos[target];
|
|
343
|
+
if (!repo) {
|
|
344
|
+
throw new Error(
|
|
345
|
+
`Unknown github profile '${target}'. Available profiles: [${keys.join(", ")}]. Use --repo owner/name for an explicit repository.`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
return `${repo.owner}/${repo.repo}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const repo = getGitHubConfig(config);
|
|
352
|
+
return repo ? `${repo.owner}/${repo.repo}` : undefined;
|
|
353
|
+
}
|
|
@@ -105,14 +105,14 @@ export const issueCloseCommand = Command.make(
|
|
|
105
105
|
},
|
|
106
106
|
({ comment, commentFile, format, issue, reason }) =>
|
|
107
107
|
Effect.gen(function* () {
|
|
108
|
-
const resolvedComment = yield* resolveOptionalTextInput(
|
|
109
|
-
"gh-tool issue close",
|
|
110
|
-
Option.getOrNull(comment),
|
|
111
|
-
Option.getOrNull(commentFile),
|
|
112
|
-
"--comment",
|
|
113
|
-
"--comment-file",
|
|
114
|
-
"comment",
|
|
115
|
-
);
|
|
108
|
+
const resolvedComment = yield* resolveOptionalTextInput({
|
|
109
|
+
command: "gh-tool issue close",
|
|
110
|
+
value: Option.getOrNull(comment),
|
|
111
|
+
fileValue: Option.getOrNull(commentFile),
|
|
112
|
+
valueFlag: "--comment",
|
|
113
|
+
fileFlag: "--comment-file",
|
|
114
|
+
label: "comment",
|
|
115
|
+
});
|
|
116
116
|
|
|
117
117
|
const result = yield* closeIssue({
|
|
118
118
|
comment: resolvedComment,
|
|
@@ -139,14 +139,14 @@ export const issueReopenCommand = Command.make(
|
|
|
139
139
|
},
|
|
140
140
|
({ comment, commentFile, format, issue }) =>
|
|
141
141
|
Effect.gen(function* () {
|
|
142
|
-
const resolvedComment = yield* resolveOptionalTextInput(
|
|
143
|
-
"gh-tool issue reopen",
|
|
144
|
-
Option.getOrNull(comment),
|
|
145
|
-
Option.getOrNull(commentFile),
|
|
146
|
-
"--comment",
|
|
147
|
-
"--comment-file",
|
|
148
|
-
"comment",
|
|
149
|
-
);
|
|
142
|
+
const resolvedComment = yield* resolveOptionalTextInput({
|
|
143
|
+
command: "gh-tool issue reopen",
|
|
144
|
+
value: Option.getOrNull(comment),
|
|
145
|
+
fileValue: Option.getOrNull(commentFile),
|
|
146
|
+
valueFlag: "--comment",
|
|
147
|
+
fileFlag: "--comment-file",
|
|
148
|
+
label: "comment",
|
|
149
|
+
});
|
|
150
150
|
|
|
151
151
|
const result = yield* reopenIssue({
|
|
152
152
|
comment: resolvedComment,
|
|
@@ -169,14 +169,14 @@ export const issueCommentCommand = Command.make(
|
|
|
169
169
|
},
|
|
170
170
|
({ body, bodyFile, format, issue }) =>
|
|
171
171
|
Effect.gen(function* () {
|
|
172
|
-
const resolvedBody = yield* resolveRequiredTextInput(
|
|
173
|
-
"gh-tool issue comment",
|
|
174
|
-
Option.getOrNull(body),
|
|
175
|
-
Option.getOrNull(bodyFile),
|
|
176
|
-
"--body",
|
|
177
|
-
"--body-file",
|
|
178
|
-
"body",
|
|
179
|
-
);
|
|
172
|
+
const resolvedBody = yield* resolveRequiredTextInput({
|
|
173
|
+
command: "gh-tool issue comment",
|
|
174
|
+
value: Option.getOrNull(body),
|
|
175
|
+
fileValue: Option.getOrNull(bodyFile),
|
|
176
|
+
valueFlag: "--body",
|
|
177
|
+
fileFlag: "--body-file",
|
|
178
|
+
label: "body",
|
|
179
|
+
});
|
|
180
180
|
|
|
181
181
|
const result = yield* commentOnIssue({ body: resolvedBody, issue });
|
|
182
182
|
yield* logFormatted(result, format);
|
|
@@ -223,14 +223,14 @@ export const issueEditCommand = Command.make(
|
|
|
223
223
|
title,
|
|
224
224
|
}) =>
|
|
225
225
|
Effect.gen(function* () {
|
|
226
|
-
const resolvedBody = yield* resolveOptionalTextInput(
|
|
227
|
-
"gh-tool issue edit",
|
|
228
|
-
Option.getOrNull(body),
|
|
229
|
-
Option.getOrNull(bodyFile),
|
|
230
|
-
"--body",
|
|
231
|
-
"--body-file",
|
|
232
|
-
"body",
|
|
233
|
-
);
|
|
226
|
+
const resolvedBody = yield* resolveOptionalTextInput({
|
|
227
|
+
command: "gh-tool issue edit",
|
|
228
|
+
value: Option.getOrNull(body),
|
|
229
|
+
fileValue: Option.getOrNull(bodyFile),
|
|
230
|
+
valueFlag: "--body",
|
|
231
|
+
fileFlag: "--body-file",
|
|
232
|
+
label: "body",
|
|
233
|
+
});
|
|
234
234
|
|
|
235
235
|
const result = yield* editIssue({
|
|
236
236
|
addAssignee: Option.getOrNull(addAssignee),
|
|
@@ -4,6 +4,7 @@ import { Effect, Option } from "effect";
|
|
|
4
4
|
import type { PRStatusResult } from "#gh/types";
|
|
5
5
|
|
|
6
6
|
import { formatOption, logFormatted } from "#shared";
|
|
7
|
+
import { GitHubService } from "#gh/service";
|
|
7
8
|
import {
|
|
8
9
|
resolveDefaultTextInput,
|
|
9
10
|
resolveOptionalTextInput,
|
|
@@ -44,6 +45,17 @@ import {
|
|
|
44
45
|
// CLI Commands
|
|
45
46
|
// ---------------------------------------------------------------------------
|
|
46
47
|
|
|
48
|
+
const repoOption = Flag.string("repo").pipe(
|
|
49
|
+
Flag.withDescription("Target repository profile name or owner/name"),
|
|
50
|
+
Flag.optional,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const withRepo = <A, E, R>(repo: Option.Option<string>, effect: Effect.Effect<A, E, R>) =>
|
|
54
|
+
Effect.gen(function* () {
|
|
55
|
+
const gh = yield* GitHubService;
|
|
56
|
+
return yield* gh.withRepoTarget(Option.getOrNull(repo), effect);
|
|
57
|
+
});
|
|
58
|
+
|
|
47
59
|
export const prViewCommand = Command.make(
|
|
48
60
|
"view",
|
|
49
61
|
{
|
|
@@ -52,20 +64,30 @@ export const prViewCommand = Command.make(
|
|
|
52
64
|
Flag.withDescription("PR number (default: current branch PR)"),
|
|
53
65
|
Flag.optional,
|
|
54
66
|
),
|
|
67
|
+
repo: repoOption,
|
|
55
68
|
},
|
|
56
|
-
({ format, pr }) =>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
({ format, pr, repo }) =>
|
|
70
|
+
withRepo(
|
|
71
|
+
repo,
|
|
72
|
+
Effect.gen(function* () {
|
|
73
|
+
const prNumber = Option.getOrNull(pr);
|
|
74
|
+
const info = yield* viewPR(prNumber);
|
|
75
|
+
yield* logFormatted(info, format);
|
|
76
|
+
}),
|
|
77
|
+
),
|
|
62
78
|
).pipe(Command.withDescription("View PR information"));
|
|
63
79
|
|
|
64
|
-
export const prStatusCommand = Command.make(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
80
|
+
export const prStatusCommand = Command.make(
|
|
81
|
+
"status",
|
|
82
|
+
{ format: formatOption, repo: repoOption },
|
|
83
|
+
({ format, repo }) =>
|
|
84
|
+
withRepo(
|
|
85
|
+
repo,
|
|
86
|
+
Effect.gen(function* () {
|
|
87
|
+
const result: PRStatusResult = yield* detectPRStatus();
|
|
88
|
+
yield* logFormatted(result, format);
|
|
89
|
+
}),
|
|
90
|
+
),
|
|
69
91
|
).pipe(
|
|
70
92
|
Command.withDescription("Auto-detect PR for current branch or GitButler workspace branches"),
|
|
71
93
|
);
|
|
@@ -82,6 +104,10 @@ export const prCreateCommand = Command.make(
|
|
|
82
104
|
Flag.withDescription("Read PR body from a file path or '-' for stdin"),
|
|
83
105
|
Flag.optional,
|
|
84
106
|
),
|
|
107
|
+
bodyStdin: Flag.boolean("body-stdin").pipe(
|
|
108
|
+
Flag.withDescription("Read PR body from stdin"),
|
|
109
|
+
Flag.withDefault(false),
|
|
110
|
+
),
|
|
85
111
|
draft: Flag.boolean("draft").pipe(
|
|
86
112
|
Flag.withDescription("Create as draft PR"),
|
|
87
113
|
Flag.withDefault(false),
|
|
@@ -91,29 +117,35 @@ export const prCreateCommand = Command.make(
|
|
|
91
117
|
Flag.withDescription("Source branch name (required in GitButler workspace mode)"),
|
|
92
118
|
Flag.optional,
|
|
93
119
|
),
|
|
120
|
+
repo: repoOption,
|
|
94
121
|
title: Flag.string("title").pipe(Flag.withDescription("PR title")),
|
|
95
122
|
},
|
|
96
|
-
({ base, body, bodyFile, draft, format, head, title }) =>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
123
|
+
({ base, body, bodyFile, bodyStdin, draft, format, head, repo, title }) =>
|
|
124
|
+
withRepo(
|
|
125
|
+
repo,
|
|
126
|
+
Effect.gen(function* () {
|
|
127
|
+
const resolvedBody = yield* resolveDefaultTextInput({
|
|
128
|
+
command: "gh-tool pr create",
|
|
129
|
+
value: Option.getOrNull(body),
|
|
130
|
+
fileValue: Option.getOrNull(bodyFile),
|
|
131
|
+
stdin: bodyStdin,
|
|
132
|
+
valueFlag: "--body",
|
|
133
|
+
fileFlag: "--body-file",
|
|
134
|
+
stdinFlag: "--body-stdin",
|
|
135
|
+
label: "body",
|
|
136
|
+
defaultValue: "",
|
|
137
|
+
});
|
|
107
138
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
139
|
+
const info = yield* createPR({
|
|
140
|
+
base,
|
|
141
|
+
body: resolvedBody,
|
|
142
|
+
draft,
|
|
143
|
+
head: Option.getOrNull(head),
|
|
144
|
+
title,
|
|
145
|
+
});
|
|
146
|
+
yield* logFormatted(info, format);
|
|
147
|
+
}),
|
|
148
|
+
),
|
|
117
149
|
).pipe(Command.withDescription("Create or update a PR for current branch"));
|
|
118
150
|
|
|
119
151
|
export const prEditCommand = Command.make(
|
|
@@ -125,29 +157,39 @@ export const prEditCommand = Command.make(
|
|
|
125
157
|
Flag.withDescription("Read PR body from a file path or '-' for stdin"),
|
|
126
158
|
Flag.optional,
|
|
127
159
|
),
|
|
160
|
+
bodyStdin: Flag.boolean("body-stdin").pipe(
|
|
161
|
+
Flag.withDescription("Read PR body from stdin"),
|
|
162
|
+
Flag.withDefault(false),
|
|
163
|
+
),
|
|
128
164
|
format: formatOption,
|
|
129
165
|
pr: Flag.integer("pr").pipe(Flag.withDescription("PR number to edit")),
|
|
166
|
+
repo: repoOption,
|
|
130
167
|
title: Flag.string("title").pipe(Flag.withDescription("New PR title"), Flag.optional),
|
|
131
168
|
},
|
|
132
|
-
({ base, body, bodyFile, format, pr, title }) =>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
169
|
+
({ base, body, bodyFile, bodyStdin, format, pr, repo, title }) =>
|
|
170
|
+
withRepo(
|
|
171
|
+
repo,
|
|
172
|
+
Effect.gen(function* () {
|
|
173
|
+
const resolvedBody = yield* resolveOptionalTextInput({
|
|
174
|
+
command: "gh-tool pr edit",
|
|
175
|
+
value: Option.getOrNull(body),
|
|
176
|
+
fileValue: Option.getOrNull(bodyFile),
|
|
177
|
+
stdin: bodyStdin,
|
|
178
|
+
valueFlag: "--body",
|
|
179
|
+
fileFlag: "--body-file",
|
|
180
|
+
stdinFlag: "--body-stdin",
|
|
181
|
+
label: "body",
|
|
182
|
+
});
|
|
142
183
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
184
|
+
const info = yield* editPR({
|
|
185
|
+
pr,
|
|
186
|
+
title: Option.getOrNull(title),
|
|
187
|
+
body: resolvedBody,
|
|
188
|
+
base: Option.getOrNull(base),
|
|
189
|
+
});
|
|
190
|
+
yield* logFormatted(info, format);
|
|
191
|
+
}),
|
|
192
|
+
),
|
|
151
193
|
).pipe(Command.withDescription("Edit an existing PR's title, body, or other metadata"));
|
|
152
194
|
|
|
153
195
|
export const prCloseCommand = Command.make(
|
|
@@ -167,25 +209,29 @@ export const prCloseCommand = Command.make(
|
|
|
167
209
|
),
|
|
168
210
|
format: formatOption,
|
|
169
211
|
pr: Flag.integer("pr").pipe(Flag.withDescription("PR number to close")),
|
|
212
|
+
repo: repoOption,
|
|
170
213
|
},
|
|
171
|
-
({ comment, commentFile, deleteBranch, format, pr }) =>
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
214
|
+
({ comment, commentFile, deleteBranch, format, pr, repo }) =>
|
|
215
|
+
withRepo(
|
|
216
|
+
repo,
|
|
217
|
+
Effect.gen(function* () {
|
|
218
|
+
const resolvedComment = yield* resolveOptionalTextInput({
|
|
219
|
+
command: "gh-tool pr close",
|
|
220
|
+
value: Option.getOrNull(comment),
|
|
221
|
+
fileValue: Option.getOrNull(commentFile),
|
|
222
|
+
valueFlag: "--comment",
|
|
223
|
+
fileFlag: "--comment-file",
|
|
224
|
+
label: "comment",
|
|
225
|
+
});
|
|
181
226
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
227
|
+
const result = yield* closePR({
|
|
228
|
+
comment: resolvedComment,
|
|
229
|
+
deleteBranch,
|
|
230
|
+
pr,
|
|
231
|
+
});
|
|
232
|
+
yield* logFormatted(result, format);
|
|
233
|
+
}),
|
|
234
|
+
),
|
|
189
235
|
).pipe(Command.withDescription("Close a PR with optional comment and branch deletion"));
|
|
190
236
|
|
|
191
237
|
export const prMergeCommand = Command.make(
|
|
@@ -201,21 +247,25 @@ export const prMergeCommand = Command.make(
|
|
|
201
247
|
),
|
|
202
248
|
format: formatOption,
|
|
203
249
|
pr: Flag.integer("pr").pipe(Flag.withDescription("PR number to merge")),
|
|
250
|
+
repo: repoOption,
|
|
204
251
|
strategy: Flag.choice("strategy", MERGE_STRATEGIES).pipe(
|
|
205
252
|
Flag.withDescription("Merge strategy: squash, merge, or rebase"),
|
|
206
253
|
Flag.withDefault(DEFAULT_MERGE_STRATEGY),
|
|
207
254
|
),
|
|
208
255
|
},
|
|
209
|
-
({ confirm, deleteBranch, format, pr, strategy }) =>
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
256
|
+
({ confirm, deleteBranch, format, pr, repo, strategy }) =>
|
|
257
|
+
withRepo(
|
|
258
|
+
repo,
|
|
259
|
+
Effect.gen(function* () {
|
|
260
|
+
const result = yield* mergePR({
|
|
261
|
+
confirm,
|
|
262
|
+
deleteBranch,
|
|
263
|
+
pr,
|
|
264
|
+
strategy,
|
|
265
|
+
});
|
|
266
|
+
yield* logFormatted(result, format);
|
|
267
|
+
}),
|
|
268
|
+
),
|
|
219
269
|
).pipe(Command.withDescription("Merge a PR (dry-run by default, use --confirm to execute)"));
|
|
220
270
|
|
|
221
271
|
export const prChecksCommand = Command.make(
|
|
@@ -230,6 +280,7 @@ export const prChecksCommand = Command.make(
|
|
|
230
280
|
Flag.withDescription("PR number (default: current branch PR)"),
|
|
231
281
|
Flag.optional,
|
|
232
282
|
),
|
|
283
|
+
repo: repoOption,
|
|
233
284
|
timeout: Flag.integer("timeout").pipe(
|
|
234
285
|
Flag.withDefault(CI_CHECK_WATCH_TIMEOUT_MS / 1000),
|
|
235
286
|
Flag.withDescription("Timeout in seconds for watch mode (default: 600)"),
|
|
@@ -239,12 +290,15 @@ export const prChecksCommand = Command.make(
|
|
|
239
290
|
Flag.withDescription("Watch until checks complete or timeout"),
|
|
240
291
|
),
|
|
241
292
|
},
|
|
242
|
-
({ failFast, format, pr, timeout, watch }) =>
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
293
|
+
({ failFast, format, pr, repo, timeout, watch }) =>
|
|
294
|
+
withRepo(
|
|
295
|
+
repo,
|
|
296
|
+
Effect.gen(function* () {
|
|
297
|
+
const prNumber = Option.getOrNull(pr);
|
|
298
|
+
const checks = yield* fetchChecksForCommand(prNumber, watch, failFast, timeout);
|
|
299
|
+
yield* logFormatted(checks, format);
|
|
300
|
+
}),
|
|
301
|
+
),
|
|
248
302
|
).pipe(Command.withDescription("Fetch CI check status for a PR (optionally watch with timeout)"));
|
|
249
303
|
|
|
250
304
|
export const prChecksFailedCommand = Command.make(
|
|
@@ -255,13 +309,17 @@ export const prChecksFailedCommand = Command.make(
|
|
|
255
309
|
Flag.withDescription("PR number (default: current branch PR)"),
|
|
256
310
|
Flag.optional,
|
|
257
311
|
),
|
|
312
|
+
repo: repoOption,
|
|
258
313
|
},
|
|
259
|
-
({ format, pr }) =>
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
314
|
+
({ format, pr, repo }) =>
|
|
315
|
+
withRepo(
|
|
316
|
+
repo,
|
|
317
|
+
Effect.gen(function* () {
|
|
318
|
+
const prNumber = Option.getOrNull(pr);
|
|
319
|
+
const checks = yield* fetchFailedChecks(prNumber);
|
|
320
|
+
yield* logFormatted(checks, format);
|
|
321
|
+
}),
|
|
322
|
+
),
|
|
265
323
|
).pipe(Command.withDescription("Fetch only failed CI checks for a PR"));
|
|
266
324
|
|
|
267
325
|
export const prRerunChecksCommand = Command.make(
|
|
@@ -272,17 +330,21 @@ export const prRerunChecksCommand = Command.make(
|
|
|
272
330
|
Flag.withDescription("PR number (default: current branch PR)"),
|
|
273
331
|
Flag.optional,
|
|
274
332
|
),
|
|
333
|
+
repo: repoOption,
|
|
275
334
|
failedOnly: Flag.boolean("failed-only").pipe(
|
|
276
335
|
Flag.withDefault(true),
|
|
277
336
|
Flag.withDescription("Only rerun failed checks (default: true)"),
|
|
278
337
|
),
|
|
279
338
|
},
|
|
280
|
-
({ failedOnly, format, pr }) =>
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
339
|
+
({ failedOnly, format, pr, repo }) =>
|
|
340
|
+
withRepo(
|
|
341
|
+
repo,
|
|
342
|
+
Effect.gen(function* () {
|
|
343
|
+
const prNumber = Option.getOrNull(pr);
|
|
344
|
+
const result = yield* rerunChecks(prNumber, failedOnly);
|
|
345
|
+
yield* logFormatted(result, format);
|
|
346
|
+
}),
|
|
347
|
+
),
|
|
286
348
|
).pipe(
|
|
287
349
|
Command.withDescription("Rerun CI checks for a PR (GitHub Actions only, failed by default)"),
|
|
288
350
|
);
|
|
@@ -295,6 +357,7 @@ export const prThreadsCommand = Command.make(
|
|
|
295
357
|
Flag.withDescription("PR number (default: current branch PR)"),
|
|
296
358
|
Flag.optional,
|
|
297
359
|
),
|
|
360
|
+
repo: repoOption,
|
|
298
361
|
unresolvedOnly: Flag.boolean("unresolved-only").pipe(
|
|
299
362
|
Flag.withDescription("Only show unresolved threads"),
|
|
300
363
|
Flag.withDefault(true),
|
|
@@ -306,12 +369,15 @@ export const prThreadsCommand = Command.make(
|
|
|
306
369
|
Flag.withDefault(false),
|
|
307
370
|
),
|
|
308
371
|
},
|
|
309
|
-
({ format, pr, unresolvedOnly, visibleOpenOnly }) =>
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
372
|
+
({ format, pr, repo, unresolvedOnly, visibleOpenOnly }) =>
|
|
373
|
+
withRepo(
|
|
374
|
+
repo,
|
|
375
|
+
Effect.gen(function* () {
|
|
376
|
+
const prNumber = Option.getOrNull(pr);
|
|
377
|
+
const threads = yield* fetchThreads(prNumber, unresolvedOnly, visibleOpenOnly);
|
|
378
|
+
yield* logFormatted(threads, format);
|
|
379
|
+
}),
|
|
380
|
+
),
|
|
315
381
|
).pipe(
|
|
316
382
|
Command.withDescription(
|
|
317
383
|
"Fetch review threads for a PR (unresolved by default, or use --visible-open-only for reply-aware human-visible open items)",
|
|
@@ -326,18 +392,22 @@ export const prCommentsCommand = Command.make(
|
|
|
326
392
|
Flag.withDescription("PR number (default: current branch PR)"),
|
|
327
393
|
Flag.optional,
|
|
328
394
|
),
|
|
395
|
+
repo: repoOption,
|
|
329
396
|
since: Flag.string("since").pipe(
|
|
330
397
|
Flag.withDescription("ISO timestamp to filter comments created after"),
|
|
331
398
|
Flag.optional,
|
|
332
399
|
),
|
|
333
400
|
},
|
|
334
|
-
({ format, pr, since }) =>
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
401
|
+
({ format, pr, repo, since }) =>
|
|
402
|
+
withRepo(
|
|
403
|
+
repo,
|
|
404
|
+
Effect.gen(function* () {
|
|
405
|
+
const prNumber = Option.getOrNull(pr);
|
|
406
|
+
const sinceValue = Option.getOrNull(since);
|
|
407
|
+
const comments = yield* fetchComments(prNumber, sinceValue);
|
|
408
|
+
yield* logFormatted(comments, format);
|
|
409
|
+
}),
|
|
410
|
+
),
|
|
341
411
|
).pipe(Command.withDescription("Fetch review comments for a PR (optionally filter by --since)"));
|
|
342
412
|
|
|
343
413
|
export const prIssueCommentsCommand = Command.make(
|
|
@@ -356,26 +426,30 @@ export const prIssueCommentsCommand = Command.make(
|
|
|
356
426
|
Flag.withDescription("PR number (default: current branch PR)"),
|
|
357
427
|
Flag.optional,
|
|
358
428
|
),
|
|
429
|
+
repo: repoOption,
|
|
359
430
|
since: Flag.string("since").pipe(
|
|
360
431
|
Flag.withDescription("ISO timestamp to filter comments created after"),
|
|
361
432
|
Flag.optional,
|
|
362
433
|
),
|
|
363
434
|
},
|
|
364
|
-
({ author, bodyContains, format, pr, since }) =>
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
435
|
+
({ author, bodyContains, format, pr, repo, since }) =>
|
|
436
|
+
withRepo(
|
|
437
|
+
repo,
|
|
438
|
+
Effect.gen(function* () {
|
|
439
|
+
const prNumber = Option.getOrNull(pr);
|
|
440
|
+
const sinceValue = Option.getOrNull(since);
|
|
441
|
+
const authorValue = Option.getOrNull(author);
|
|
442
|
+
const bodyContainsValue = Option.getOrNull(bodyContains);
|
|
370
443
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
444
|
+
const comments = yield* fetchIssueComments(
|
|
445
|
+
prNumber,
|
|
446
|
+
sinceValue,
|
|
447
|
+
authorValue,
|
|
448
|
+
bodyContainsValue,
|
|
449
|
+
);
|
|
450
|
+
yield* logFormatted(comments, format);
|
|
451
|
+
}),
|
|
452
|
+
),
|
|
379
453
|
).pipe(Command.withDescription("Fetch general PR discussion comments (issue comments)"));
|
|
380
454
|
|
|
381
455
|
export const prIssueCommentsLatestCommand = Command.make(
|
|
@@ -394,16 +468,20 @@ export const prIssueCommentsLatestCommand = Command.make(
|
|
|
394
468
|
Flag.withDescription("PR number (default: current branch PR)"),
|
|
395
469
|
Flag.optional,
|
|
396
470
|
),
|
|
471
|
+
repo: repoOption,
|
|
397
472
|
},
|
|
398
|
-
({ author, bodyContains, format, pr }) =>
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
473
|
+
({ author, bodyContains, format, pr, repo }) =>
|
|
474
|
+
withRepo(
|
|
475
|
+
repo,
|
|
476
|
+
Effect.gen(function* () {
|
|
477
|
+
const prNumber = Option.getOrNull(pr);
|
|
478
|
+
const authorValue = Option.getOrNull(author);
|
|
479
|
+
const bodyContainsValue = Option.getOrNull(bodyContains);
|
|
403
480
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
481
|
+
const comment = yield* fetchLatestIssueComment(prNumber, authorValue, bodyContainsValue);
|
|
482
|
+
yield* logFormatted(comment, format);
|
|
483
|
+
}),
|
|
484
|
+
),
|
|
407
485
|
).pipe(Command.withDescription("Fetch latest general PR discussion comment"));
|
|
408
486
|
|
|
409
487
|
export const prCommentCommand = Command.make(
|
|
@@ -422,21 +500,25 @@ export const prCommentCommand = Command.make(
|
|
|
422
500
|
Flag.withDescription("PR number (default: current branch PR)"),
|
|
423
501
|
Flag.optional,
|
|
424
502
|
),
|
|
503
|
+
repo: repoOption,
|
|
425
504
|
},
|
|
426
|
-
({ body, bodyFile, format, pr }) =>
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
505
|
+
({ body, bodyFile, format, pr, repo }) =>
|
|
506
|
+
withRepo(
|
|
507
|
+
repo,
|
|
508
|
+
Effect.gen(function* () {
|
|
509
|
+
const prNumber = Option.getOrNull(pr);
|
|
510
|
+
const resolvedBody = yield* resolveRequiredTextInput({
|
|
511
|
+
command: "gh-tool pr comment",
|
|
512
|
+
value: Option.getOrNull(body),
|
|
513
|
+
fileValue: Option.getOrNull(bodyFile),
|
|
514
|
+
valueFlag: "--body",
|
|
515
|
+
fileFlag: "--body-file",
|
|
516
|
+
label: "body",
|
|
517
|
+
});
|
|
518
|
+
const result = yield* postIssueComment(prNumber, resolvedBody);
|
|
519
|
+
yield* logFormatted(result, format);
|
|
520
|
+
}),
|
|
521
|
+
),
|
|
440
522
|
).pipe(Command.withDescription("Post a general PR discussion comment"));
|
|
441
523
|
|
|
442
524
|
export const prDiscussionSummaryCommand = Command.make(
|
|
@@ -447,13 +529,17 @@ export const prDiscussionSummaryCommand = Command.make(
|
|
|
447
529
|
Flag.withDescription("PR number (default: current branch PR)"),
|
|
448
530
|
Flag.optional,
|
|
449
531
|
),
|
|
532
|
+
repo: repoOption,
|
|
450
533
|
},
|
|
451
|
-
({ format, pr }) =>
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
534
|
+
({ format, pr, repo }) =>
|
|
535
|
+
withRepo(
|
|
536
|
+
repo,
|
|
537
|
+
Effect.gen(function* () {
|
|
538
|
+
const prNumber = Option.getOrNull(pr);
|
|
539
|
+
const summary = yield* fetchDiscussionSummary(prNumber);
|
|
540
|
+
yield* logFormatted(summary, format);
|
|
541
|
+
}),
|
|
542
|
+
),
|
|
457
543
|
).pipe(
|
|
458
544
|
Command.withDescription("Fetch counts and latest comment across PR discussions and reviews"),
|
|
459
545
|
);
|
|
@@ -474,21 +560,25 @@ export const prReplyCommand = Command.make(
|
|
|
474
560
|
Flag.withDescription("PR number (default: current branch PR)"),
|
|
475
561
|
Flag.optional,
|
|
476
562
|
),
|
|
563
|
+
repo: repoOption,
|
|
477
564
|
},
|
|
478
|
-
({ body, bodyFile, commentId, format, pr }) =>
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
565
|
+
({ body, bodyFile, commentId, format, pr, repo }) =>
|
|
566
|
+
withRepo(
|
|
567
|
+
repo,
|
|
568
|
+
Effect.gen(function* () {
|
|
569
|
+
const prNumber = Option.getOrNull(pr);
|
|
570
|
+
const resolvedBody = yield* resolveRequiredTextInput({
|
|
571
|
+
command: "gh-tool pr reply",
|
|
572
|
+
value: Option.getOrNull(body),
|
|
573
|
+
fileValue: Option.getOrNull(bodyFile),
|
|
574
|
+
valueFlag: "--body",
|
|
575
|
+
fileFlag: "--body-file",
|
|
576
|
+
label: "body",
|
|
577
|
+
});
|
|
578
|
+
const result = yield* replyToComment(prNumber, commentId, resolvedBody);
|
|
579
|
+
yield* logFormatted(result, format);
|
|
580
|
+
}),
|
|
581
|
+
),
|
|
492
582
|
).pipe(Command.withDescription("Reply to an inline review comment"));
|
|
493
583
|
|
|
494
584
|
export const prResolveCommand = Command.make(
|
|
@@ -498,12 +588,16 @@ export const prResolveCommand = Command.make(
|
|
|
498
588
|
threadId: Flag.string("thread-id").pipe(
|
|
499
589
|
Flag.withDescription("GraphQL node ID of the thread to resolve"),
|
|
500
590
|
),
|
|
591
|
+
repo: repoOption,
|
|
501
592
|
},
|
|
502
|
-
({ format, threadId }) =>
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
593
|
+
({ format, repo, threadId }) =>
|
|
594
|
+
withRepo(
|
|
595
|
+
repo,
|
|
596
|
+
Effect.gen(function* () {
|
|
597
|
+
const result = yield* resolveThread(threadId);
|
|
598
|
+
yield* logFormatted(result, format);
|
|
599
|
+
}),
|
|
600
|
+
),
|
|
507
601
|
).pipe(Command.withDescription("Resolve a review thread via GraphQL"));
|
|
508
602
|
|
|
509
603
|
export const prSubmitReviewCommand = Command.make(
|
|
@@ -522,6 +616,7 @@ export const prSubmitReviewCommand = Command.make(
|
|
|
522
616
|
Flag.withDescription("PR number (default: current branch PR)"),
|
|
523
617
|
Flag.optional,
|
|
524
618
|
),
|
|
619
|
+
repo: repoOption,
|
|
525
620
|
reviewId: Flag.string("review-id").pipe(
|
|
526
621
|
Flag.withDescription(
|
|
527
622
|
"Pending review GraphQL ID (defaults to current user's pending review on PR)",
|
|
@@ -529,21 +624,24 @@ export const prSubmitReviewCommand = Command.make(
|
|
|
529
624
|
Flag.optional,
|
|
530
625
|
),
|
|
531
626
|
},
|
|
532
|
-
({ body, bodyFile, format, pr, reviewId }) =>
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
627
|
+
({ body, bodyFile, format, pr, repo, reviewId }) =>
|
|
628
|
+
withRepo(
|
|
629
|
+
repo,
|
|
630
|
+
Effect.gen(function* () {
|
|
631
|
+
const prNumber = Option.getOrNull(pr);
|
|
632
|
+
const reviewIdValue = Option.getOrNull(reviewId);
|
|
633
|
+
const bodyValue = yield* resolveOptionalTextInput({
|
|
634
|
+
command: "gh-tool pr submit-review",
|
|
635
|
+
value: Option.getOrNull(body),
|
|
636
|
+
fileValue: Option.getOrNull(bodyFile),
|
|
637
|
+
valueFlag: "--body",
|
|
638
|
+
fileFlag: "--body-file",
|
|
639
|
+
label: "body",
|
|
640
|
+
});
|
|
641
|
+
const result = yield* submitPendingReview(prNumber, reviewIdValue, bodyValue);
|
|
642
|
+
yield* logFormatted(result, format);
|
|
643
|
+
}),
|
|
644
|
+
),
|
|
547
645
|
).pipe(
|
|
548
646
|
Command.withDescription(
|
|
549
647
|
"Submit a pending review as COMMENT (auto-detects your pending review if --review-id is omitted)",
|
|
@@ -558,19 +656,26 @@ export const prReviewTriageCommand = Command.make(
|
|
|
558
656
|
Flag.withDescription("PR number (default: current branch PR)"),
|
|
559
657
|
Flag.optional,
|
|
560
658
|
),
|
|
659
|
+
repo: repoOption,
|
|
561
660
|
},
|
|
562
|
-
({ format, pr }) =>
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
661
|
+
({ format, pr, repo }) =>
|
|
662
|
+
withRepo(
|
|
663
|
+
repo,
|
|
664
|
+
Effect.gen(function* () {
|
|
665
|
+
const prNumber = Option.getOrNull(pr);
|
|
666
|
+
const [info, unresolvedThreads, visibleOpenThreads, summary, checks] = yield* Effect.all([
|
|
667
|
+
viewPR(prNumber),
|
|
668
|
+
fetchThreads(prNumber, true),
|
|
669
|
+
fetchThreads(prNumber, false, true),
|
|
670
|
+
fetchDiscussionSummary(prNumber),
|
|
671
|
+
fetchChecks(prNumber, false, false, 0),
|
|
672
|
+
]);
|
|
673
|
+
yield* logFormatted(
|
|
674
|
+
{ info, unresolvedThreads, visibleOpenThreads, summary, checks },
|
|
675
|
+
format,
|
|
676
|
+
);
|
|
677
|
+
}),
|
|
678
|
+
),
|
|
574
679
|
).pipe(
|
|
575
680
|
Command.withDescription(
|
|
576
681
|
"Composite: PR info + unresolved threads + visible-open threads + discussion summary + checks status in one call",
|
|
@@ -593,25 +698,29 @@ export const prReplyAndResolveCommand = Command.make(
|
|
|
593
698
|
Flag.withDescription("PR number (default: current branch PR)"),
|
|
594
699
|
Flag.optional,
|
|
595
700
|
),
|
|
701
|
+
repo: repoOption,
|
|
596
702
|
threadId: Flag.string("thread-id").pipe(
|
|
597
703
|
Flag.withDescription("GraphQL node ID of the thread to resolve"),
|
|
598
704
|
),
|
|
599
705
|
},
|
|
600
|
-
({ body, bodyFile, commentId, format, pr, threadId }) =>
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
706
|
+
({ body, bodyFile, commentId, format, pr, repo, threadId }) =>
|
|
707
|
+
withRepo(
|
|
708
|
+
repo,
|
|
709
|
+
Effect.gen(function* () {
|
|
710
|
+
const prNumber = Option.getOrNull(pr);
|
|
711
|
+
const resolvedBody = yield* resolveRequiredTextInput({
|
|
712
|
+
command: "gh-tool pr reply-and-resolve",
|
|
713
|
+
value: Option.getOrNull(body),
|
|
714
|
+
fileValue: Option.getOrNull(bodyFile),
|
|
715
|
+
valueFlag: "--body",
|
|
716
|
+
fileFlag: "--body-file",
|
|
717
|
+
label: "body",
|
|
718
|
+
});
|
|
719
|
+
const replyResult = yield* replyToComment(prNumber, commentId, resolvedBody);
|
|
720
|
+
const resolveResult = yield* resolveThread(threadId);
|
|
721
|
+
yield* logFormatted({ reply: replyResult, resolve: resolveResult }, format);
|
|
722
|
+
}),
|
|
723
|
+
),
|
|
615
724
|
).pipe(
|
|
616
725
|
Command.withDescription(
|
|
617
726
|
"Composite: reply to a review comment and resolve its thread in one call",
|
package/src/gh-tool/pr/core.ts
CHANGED
|
@@ -445,9 +445,12 @@ export const createPR = Effect.fn("pr.createPR")(function* (opts: {
|
|
|
445
445
|
|
|
446
446
|
if (Option.isSome(existing)) {
|
|
447
447
|
const pr = existing.value;
|
|
448
|
-
yield*
|
|
449
|
-
|
|
450
|
-
|
|
448
|
+
return yield* editPR({
|
|
449
|
+
pr: pr.number,
|
|
450
|
+
title: opts.title,
|
|
451
|
+
body: opts.body,
|
|
452
|
+
base: null,
|
|
453
|
+
});
|
|
451
454
|
}
|
|
452
455
|
|
|
453
456
|
const createArgs = [
|
|
@@ -647,17 +650,23 @@ export const editPR = Effect.fn("pr.editPR")(function* (opts: {
|
|
|
647
650
|
}
|
|
648
651
|
|
|
649
652
|
const gh = yield* GitHubService;
|
|
653
|
+
const repo = yield* gh.getRepoInfo();
|
|
650
654
|
|
|
651
|
-
const editArgs = [
|
|
655
|
+
const editArgs = [
|
|
656
|
+
"api",
|
|
657
|
+
"--method",
|
|
658
|
+
"PATCH",
|
|
659
|
+
`repos/${repo.owner}/${repo.name}/pulls/${opts.pr}`,
|
|
660
|
+
];
|
|
652
661
|
|
|
653
662
|
if (opts.title) {
|
|
654
|
-
editArgs.push("
|
|
663
|
+
editArgs.push("-f", `title=${opts.title}`);
|
|
655
664
|
}
|
|
656
665
|
if (opts.body) {
|
|
657
|
-
editArgs.push("
|
|
666
|
+
editArgs.push("-f", `body=${opts.body}`);
|
|
658
667
|
}
|
|
659
668
|
if (opts.base) {
|
|
660
|
-
editArgs.push("
|
|
669
|
+
editArgs.push("-f", `base=${opts.base}`);
|
|
661
670
|
}
|
|
662
671
|
|
|
663
672
|
yield* gh.runGh(editArgs);
|
package/src/gh-tool/release.ts
CHANGED
|
@@ -416,14 +416,14 @@ export const releaseCreateCommand = Command.make(
|
|
|
416
416
|
verifyTag,
|
|
417
417
|
}) =>
|
|
418
418
|
Effect.gen(function* () {
|
|
419
|
-
const resolvedBody = yield* resolveOptionalTextInput(
|
|
420
|
-
"gh-tool release create",
|
|
421
|
-
Option.getOrNull(body),
|
|
422
|
-
Option.getOrNull(bodyFile),
|
|
423
|
-
"--body",
|
|
424
|
-
"--body-file",
|
|
425
|
-
"body",
|
|
426
|
-
);
|
|
419
|
+
const resolvedBody = yield* resolveOptionalTextInput({
|
|
420
|
+
command: "gh-tool release create",
|
|
421
|
+
value: Option.getOrNull(body),
|
|
422
|
+
fileValue: Option.getOrNull(bodyFile),
|
|
423
|
+
valueFlag: "--body",
|
|
424
|
+
fileFlag: "--body-file",
|
|
425
|
+
label: "body",
|
|
426
|
+
});
|
|
427
427
|
|
|
428
428
|
const result = yield* createRelease({
|
|
429
429
|
tag,
|
|
@@ -526,14 +526,14 @@ export const releaseEditCommand = Command.make(
|
|
|
526
526
|
},
|
|
527
527
|
({ body, bodyFile, draft, format, latest, prerelease, repo, tag, title }) =>
|
|
528
528
|
Effect.gen(function* () {
|
|
529
|
-
const resolvedBody = yield* resolveOptionalTextInput(
|
|
530
|
-
"gh-tool release edit",
|
|
531
|
-
Option.getOrNull(body),
|
|
532
|
-
Option.getOrNull(bodyFile),
|
|
533
|
-
"--body",
|
|
534
|
-
"--body-file",
|
|
535
|
-
"body",
|
|
536
|
-
);
|
|
529
|
+
const resolvedBody = yield* resolveOptionalTextInput({
|
|
530
|
+
command: "gh-tool release edit",
|
|
531
|
+
value: Option.getOrNull(body),
|
|
532
|
+
fileValue: Option.getOrNull(bodyFile),
|
|
533
|
+
valueFlag: "--body",
|
|
534
|
+
fileFlag: "--body-file",
|
|
535
|
+
label: "body",
|
|
536
|
+
});
|
|
537
537
|
|
|
538
538
|
const edited = yield* editRelease({
|
|
539
539
|
tag,
|
package/src/gh-tool/service.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { RepoInfo } from "./types";
|
|
|
5
5
|
|
|
6
6
|
import { GH_BINARY } from "./config";
|
|
7
7
|
import { GitHubAuthError, GitHubCommandError, GitHubNotFoundError } from "./errors";
|
|
8
|
-
import { ConfigService,
|
|
8
|
+
import { ConfigService, resolveGitHubRepoTarget } from "#config";
|
|
9
9
|
|
|
10
10
|
type GhResult = {
|
|
11
11
|
stdout: string;
|
|
@@ -25,6 +25,10 @@ export class GitHubService extends Context.Service<
|
|
|
25
25
|
variables: Record<string, string | number | null>,
|
|
26
26
|
) => Effect.Effect<unknown, GhError>;
|
|
27
27
|
readonly getRepoInfo: () => Effect.Effect<RepoInfo, GhError>;
|
|
28
|
+
readonly withRepoTarget: <A, E, R>(
|
|
29
|
+
target: string | null,
|
|
30
|
+
effect: Effect.Effect<A, E, R>,
|
|
31
|
+
) => Effect.Effect<A, E | GitHubCommandError, R>;
|
|
28
32
|
}
|
|
29
33
|
>()("@agent-tools/GitHubService") {
|
|
30
34
|
static readonly layer = Layer.effect(
|
|
@@ -33,14 +37,49 @@ export class GitHubService extends Context.Service<
|
|
|
33
37
|
Effect.gen(function* () {
|
|
34
38
|
const executor = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
35
39
|
const config = yield* ConfigService;
|
|
36
|
-
const
|
|
37
|
-
|
|
40
|
+
const initialRepoTarget = (() => {
|
|
41
|
+
try {
|
|
42
|
+
return resolveGitHubRepoTarget(config);
|
|
43
|
+
} catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
47
|
+
const RepoTarget = Context.Reference<string | undefined>(
|
|
48
|
+
"@agent-tools/GitHubService/RepoTarget",
|
|
49
|
+
{
|
|
50
|
+
defaultValue: () => initialRepoTarget,
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const repoInfoCache = new Map<string | null, RepoInfo>();
|
|
55
|
+
|
|
56
|
+
const resolveRepoTarget = Effect.fn("GitHubService.resolveRepoTarget")(function* (
|
|
57
|
+
target: string | null,
|
|
58
|
+
) {
|
|
59
|
+
const resolved = yield* Effect.try({
|
|
60
|
+
try: () => resolveGitHubRepoTarget(config, target),
|
|
61
|
+
catch: (error) =>
|
|
62
|
+
new GitHubCommandError({
|
|
63
|
+
message: error instanceof Error ? error.message : String(error),
|
|
64
|
+
command: "gh-tool --repo",
|
|
65
|
+
exitCode: 1,
|
|
66
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return resolved;
|
|
71
|
+
});
|
|
38
72
|
|
|
39
|
-
|
|
73
|
+
const withRepoTarget = <A, E, R>(target: string | null, effect: Effect.Effect<A, E, R>) =>
|
|
74
|
+
Effect.gen(function* () {
|
|
75
|
+
const resolved = yield* resolveRepoTarget(target);
|
|
76
|
+
return yield* effect.pipe(Effect.provideService(RepoTarget, resolved));
|
|
77
|
+
});
|
|
40
78
|
|
|
41
79
|
const executeGh = (args: string[]) =>
|
|
42
80
|
Effect.scoped(
|
|
43
81
|
Effect.gen(function* () {
|
|
82
|
+
const ghRepo = yield* RepoTarget;
|
|
44
83
|
const command = ChildProcess.make(GH_BINARY, args, {
|
|
45
84
|
stdout: "pipe",
|
|
46
85
|
stderr: "pipe",
|
|
@@ -177,7 +216,10 @@ export class GitHubService extends Context.Service<
|
|
|
177
216
|
});
|
|
178
217
|
|
|
179
218
|
const getRepoInfo = Effect.fn("GitHubService.getRepoInfo")(function* () {
|
|
180
|
-
|
|
219
|
+
const ghRepo = yield* RepoTarget;
|
|
220
|
+
const cacheKey = ghRepo ?? null;
|
|
221
|
+
const cachedRepoInfo = repoInfoCache.get(cacheKey);
|
|
222
|
+
if (cachedRepoInfo) {
|
|
181
223
|
return cachedRepoInfo;
|
|
182
224
|
}
|
|
183
225
|
|
|
@@ -195,11 +237,11 @@ export class GitHubService extends Context.Service<
|
|
|
195
237
|
url: result.url,
|
|
196
238
|
};
|
|
197
239
|
|
|
198
|
-
|
|
240
|
+
repoInfoCache.set(cacheKey, repoInfo);
|
|
199
241
|
return repoInfo;
|
|
200
242
|
});
|
|
201
243
|
|
|
202
|
-
return { runGh, runGhJson, runGraphQL, getRepoInfo };
|
|
244
|
+
return { runGh, runGhJson, runGraphQL, getRepoInfo, withRepoTarget };
|
|
203
245
|
}),
|
|
204
246
|
),
|
|
205
247
|
);
|
|
@@ -28,8 +28,10 @@ type ResolveTextInputOptions = {
|
|
|
28
28
|
command: string;
|
|
29
29
|
value: string | null;
|
|
30
30
|
fileValue: string | null;
|
|
31
|
+
stdin?: boolean;
|
|
31
32
|
valueFlag: string;
|
|
32
33
|
fileFlag: string;
|
|
34
|
+
stdinFlag?: string;
|
|
33
35
|
missingMode: Schema.Schema.Type<typeof MissingMode>;
|
|
34
36
|
missingValue?: string;
|
|
35
37
|
label: string;
|
|
@@ -38,16 +40,33 @@ type ResolveTextInputOptions = {
|
|
|
38
40
|
const resolveTextInputInternal = Effect.fn("gh.resolveTextInputInternal")(function* (
|
|
39
41
|
options: ResolveTextInputOptions,
|
|
40
42
|
) {
|
|
41
|
-
const {
|
|
42
|
-
|
|
43
|
+
const {
|
|
44
|
+
command,
|
|
45
|
+
fileFlag,
|
|
46
|
+
fileValue,
|
|
47
|
+
label,
|
|
48
|
+
missingMode,
|
|
49
|
+
missingValue,
|
|
50
|
+
stdin = false,
|
|
51
|
+
stdinFlag,
|
|
52
|
+
value,
|
|
53
|
+
valueFlag,
|
|
54
|
+
} = options;
|
|
55
|
+
|
|
56
|
+
const sourceFlags = [valueFlag, fileFlag, ...(stdinFlag ? [stdinFlag] : [])];
|
|
57
|
+
const sourceFlagList =
|
|
58
|
+
sourceFlags.length === 2
|
|
59
|
+
? `${sourceFlags[0]} or ${sourceFlags[1]}`
|
|
60
|
+
: `${sourceFlags.slice(0, -1).join(", ")}, or ${sourceFlags.at(-1)}`;
|
|
43
61
|
|
|
44
|
-
|
|
62
|
+
const providedCount = [value !== null, fileValue !== null, stdin].filter(Boolean).length;
|
|
63
|
+
if (providedCount > 1) {
|
|
45
64
|
return yield* Effect.fail(
|
|
46
65
|
new GitHubCommandError({
|
|
47
66
|
command,
|
|
48
67
|
exitCode: 1,
|
|
49
|
-
stderr: `Provide exactly one of ${
|
|
50
|
-
message: `Provide exactly one of ${
|
|
68
|
+
stderr: `Provide exactly one of ${sourceFlagList}`,
|
|
69
|
+
message: `Provide exactly one of ${sourceFlagList}`,
|
|
51
70
|
}),
|
|
52
71
|
);
|
|
53
72
|
}
|
|
@@ -71,6 +90,19 @@ const resolveTextInputInternal = Effect.fn("gh.resolveTextInputInternal")(functi
|
|
|
71
90
|
});
|
|
72
91
|
}
|
|
73
92
|
|
|
93
|
+
if (stdin) {
|
|
94
|
+
return yield* Effect.tryPromise({
|
|
95
|
+
try: () => readTextFromStdin(),
|
|
96
|
+
catch: (error) =>
|
|
97
|
+
new GitHubCommandError({
|
|
98
|
+
command,
|
|
99
|
+
exitCode: 1,
|
|
100
|
+
stderr: `Failed to read ${label} from stdin: ${error instanceof Error ? error.message : String(error)}`,
|
|
101
|
+
message: `Failed to read ${label} from stdin: ${error instanceof Error ? error.message : String(error)}`,
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
74
106
|
if (missingMode === "null") {
|
|
75
107
|
return null;
|
|
76
108
|
}
|
|
@@ -83,64 +115,35 @@ const resolveTextInputInternal = Effect.fn("gh.resolveTextInputInternal")(functi
|
|
|
83
115
|
new GitHubCommandError({
|
|
84
116
|
command,
|
|
85
117
|
exitCode: 1,
|
|
86
|
-
stderr: `Missing ${label}. Provide ${
|
|
87
|
-
message: `Missing ${label}. Provide ${
|
|
118
|
+
stderr: `Missing ${label}. Provide ${sourceFlagList}`,
|
|
119
|
+
message: `Missing ${label}. Provide ${sourceFlagList}`,
|
|
88
120
|
}),
|
|
89
121
|
);
|
|
90
122
|
});
|
|
91
123
|
|
|
92
124
|
export const resolveRequiredTextInput = (
|
|
93
|
-
|
|
94
|
-
value: string | null,
|
|
95
|
-
fileValue: string | null,
|
|
96
|
-
valueFlag: string,
|
|
97
|
-
fileFlag: string,
|
|
98
|
-
label: string,
|
|
125
|
+
options: Omit<ResolveTextInputOptions, "missingMode" | "missingValue">,
|
|
99
126
|
): Effect.Effect<string, GitHubCommandError> =>
|
|
100
127
|
resolveTextInputInternal({
|
|
101
|
-
|
|
102
|
-
value,
|
|
103
|
-
fileValue,
|
|
104
|
-
valueFlag,
|
|
105
|
-
fileFlag,
|
|
128
|
+
...options,
|
|
106
129
|
missingMode: "error",
|
|
107
|
-
label,
|
|
108
130
|
}).pipe(Effect.map((resolvedValue) => ensureResolvedText(resolvedValue, "required text input")));
|
|
109
131
|
|
|
110
132
|
export const resolveOptionalTextInput = (
|
|
111
|
-
|
|
112
|
-
value: string | null,
|
|
113
|
-
fileValue: string | null,
|
|
114
|
-
valueFlag: string,
|
|
115
|
-
fileFlag: string,
|
|
116
|
-
label: string,
|
|
133
|
+
options: Omit<ResolveTextInputOptions, "missingMode" | "missingValue">,
|
|
117
134
|
): Effect.Effect<string | null, GitHubCommandError> =>
|
|
118
135
|
resolveTextInputInternal({
|
|
119
|
-
|
|
120
|
-
value,
|
|
121
|
-
fileValue,
|
|
122
|
-
valueFlag,
|
|
123
|
-
fileFlag,
|
|
136
|
+
...options,
|
|
124
137
|
missingMode: "null",
|
|
125
|
-
label,
|
|
126
138
|
});
|
|
127
139
|
|
|
128
140
|
export const resolveDefaultTextInput = (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
valueFlag: string,
|
|
133
|
-
fileFlag: string,
|
|
134
|
-
label: string,
|
|
135
|
-
defaultValue: string,
|
|
141
|
+
options: Omit<ResolveTextInputOptions, "missingMode" | "missingValue"> & {
|
|
142
|
+
defaultValue: string;
|
|
143
|
+
},
|
|
136
144
|
): Effect.Effect<string, GitHubCommandError> =>
|
|
137
145
|
resolveTextInputInternal({
|
|
138
|
-
|
|
139
|
-
value,
|
|
140
|
-
fileValue,
|
|
141
|
-
valueFlag,
|
|
142
|
-
fileFlag,
|
|
146
|
+
...options,
|
|
143
147
|
missingMode: "default",
|
|
144
|
-
missingValue: defaultValue,
|
|
145
|
-
label,
|
|
148
|
+
missingValue: options.defaultValue,
|
|
146
149
|
}).pipe(Effect.map((resolvedValue) => ensureResolvedText(resolvedValue, "default text input")));
|