@blogic-cz/agent-tools 0.8.14 → 0.8.15
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 +3 -1
- package/src/gh-tool/issue/commands.ts +72 -9
- package/src/gh-tool/pr/commands.ts +97 -18
- package/src/gh-tool/release.ts +31 -3
- package/src/gh-tool/text-input.ts +146 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blogic-cz/agent-tools",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.15",
|
|
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",
|
|
@@ -112,8 +112,10 @@
|
|
|
112
112
|
"check:ci": "bun check.ts ci",
|
|
113
113
|
"format": "oxfmt",
|
|
114
114
|
"format:check": "oxfmt --check",
|
|
115
|
+
"gh-tool": "bun src/gh-tool/index.ts",
|
|
115
116
|
"lint": "oxlint -c ./.oxlintrc.json --deny-warnings",
|
|
116
117
|
"lint:fix": "oxlint -c ./.oxlintrc.json --fix",
|
|
118
|
+
"session-tool": "bun src/session-tool/index.ts",
|
|
117
119
|
"update:skills": "bun run .agents/skills/update-packages/references/skills-update-local.ts",
|
|
118
120
|
"test": "vitest run"
|
|
119
121
|
},
|
|
@@ -2,6 +2,7 @@ import { Command, Flag } from "effect/unstable/cli";
|
|
|
2
2
|
import { Effect, Option } from "effect";
|
|
3
3
|
|
|
4
4
|
import { formatOption, logFormatted } from "#shared";
|
|
5
|
+
import { resolveOptionalTextInput, resolveRequiredTextInput } from "#gh/text-input";
|
|
5
6
|
|
|
6
7
|
import {
|
|
7
8
|
closeIssue,
|
|
@@ -91,6 +92,10 @@ export const issueCloseCommand = Command.make(
|
|
|
91
92
|
Flag.withDescription("Comment to add when closing"),
|
|
92
93
|
Flag.optional,
|
|
93
94
|
),
|
|
95
|
+
commentFile: Flag.string("comment-file").pipe(
|
|
96
|
+
Flag.withDescription("Read close comment from a file path or '-' for stdin"),
|
|
97
|
+
Flag.optional,
|
|
98
|
+
),
|
|
94
99
|
format: formatOption,
|
|
95
100
|
issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to close")),
|
|
96
101
|
reason: Flag.choice("reason", ["completed", "not planned"]).pipe(
|
|
@@ -98,10 +103,19 @@ export const issueCloseCommand = Command.make(
|
|
|
98
103
|
Flag.withDefault("completed"),
|
|
99
104
|
),
|
|
100
105
|
},
|
|
101
|
-
({ comment, format, issue, reason }) =>
|
|
106
|
+
({ comment, commentFile, format, issue, reason }) =>
|
|
102
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
|
+
);
|
|
116
|
+
|
|
103
117
|
const result = yield* closeIssue({
|
|
104
|
-
comment:
|
|
118
|
+
comment: resolvedComment,
|
|
105
119
|
issue,
|
|
106
120
|
reason,
|
|
107
121
|
});
|
|
@@ -116,13 +130,26 @@ export const issueReopenCommand = Command.make(
|
|
|
116
130
|
Flag.withDescription("Comment to add when reopening"),
|
|
117
131
|
Flag.optional,
|
|
118
132
|
),
|
|
133
|
+
commentFile: Flag.string("comment-file").pipe(
|
|
134
|
+
Flag.withDescription("Read reopen comment from a file path or '-' for stdin"),
|
|
135
|
+
Flag.optional,
|
|
136
|
+
),
|
|
119
137
|
format: formatOption,
|
|
120
138
|
issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to reopen")),
|
|
121
139
|
},
|
|
122
|
-
({ comment, format, issue }) =>
|
|
140
|
+
({ comment, commentFile, format, issue }) =>
|
|
123
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
|
+
);
|
|
150
|
+
|
|
124
151
|
const result = yield* reopenIssue({
|
|
125
|
-
comment:
|
|
152
|
+
comment: resolvedComment,
|
|
126
153
|
issue,
|
|
127
154
|
});
|
|
128
155
|
yield* logFormatted(result, format);
|
|
@@ -132,13 +159,26 @@ export const issueReopenCommand = Command.make(
|
|
|
132
159
|
export const issueCommentCommand = Command.make(
|
|
133
160
|
"comment",
|
|
134
161
|
{
|
|
135
|
-
body: Flag.string("body").pipe(Flag.withDescription("Comment body text")),
|
|
162
|
+
body: Flag.string("body").pipe(Flag.withDescription("Comment body text"), Flag.optional),
|
|
163
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
164
|
+
Flag.withDescription("Read comment body from a file path or '-' for stdin"),
|
|
165
|
+
Flag.optional,
|
|
166
|
+
),
|
|
136
167
|
format: formatOption,
|
|
137
168
|
issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to comment on")),
|
|
138
169
|
},
|
|
139
|
-
({ body, format, issue }) =>
|
|
170
|
+
({ body, bodyFile, format, issue }) =>
|
|
140
171
|
Effect.gen(function* () {
|
|
141
|
-
const
|
|
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
|
+
);
|
|
180
|
+
|
|
181
|
+
const result = yield* commentOnIssue({ body: resolvedBody, issue });
|
|
142
182
|
yield* logFormatted(result, format);
|
|
143
183
|
}),
|
|
144
184
|
).pipe(Command.withDescription("Post a comment on an issue"));
|
|
@@ -155,6 +195,10 @@ export const issueEditCommand = Command.make(
|
|
|
155
195
|
Flag.optional,
|
|
156
196
|
),
|
|
157
197
|
body: Flag.string("body").pipe(Flag.withDescription("New issue body"), Flag.optional),
|
|
198
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
199
|
+
Flag.withDescription("Read issue body from a file path or '-' for stdin"),
|
|
200
|
+
Flag.optional,
|
|
201
|
+
),
|
|
158
202
|
format: formatOption,
|
|
159
203
|
issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to edit")),
|
|
160
204
|
removeAssignee: Flag.string("remove-assignee").pipe(
|
|
@@ -167,12 +211,31 @@ export const issueEditCommand = Command.make(
|
|
|
167
211
|
),
|
|
168
212
|
title: Flag.string("title").pipe(Flag.withDescription("New issue title"), Flag.optional),
|
|
169
213
|
},
|
|
170
|
-
({
|
|
214
|
+
({
|
|
215
|
+
addAssignee,
|
|
216
|
+
addLabels,
|
|
217
|
+
body,
|
|
218
|
+
bodyFile,
|
|
219
|
+
format,
|
|
220
|
+
issue,
|
|
221
|
+
removeAssignee,
|
|
222
|
+
removeLabels,
|
|
223
|
+
title,
|
|
224
|
+
}) =>
|
|
171
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
|
+
);
|
|
234
|
+
|
|
172
235
|
const result = yield* editIssue({
|
|
173
236
|
addAssignee: Option.getOrNull(addAssignee),
|
|
174
237
|
addLabels: Option.getOrNull(addLabels),
|
|
175
|
-
body:
|
|
238
|
+
body: resolvedBody,
|
|
176
239
|
issue,
|
|
177
240
|
removeAssignee: Option.getOrNull(removeAssignee),
|
|
178
241
|
removeLabels: Option.getOrNull(removeLabels),
|
|
@@ -4,6 +4,11 @@ import { Effect, Option } from "effect";
|
|
|
4
4
|
import type { PRStatusResult } from "#gh/types";
|
|
5
5
|
|
|
6
6
|
import { formatOption, logFormatted } from "#shared";
|
|
7
|
+
import {
|
|
8
|
+
resolveDefaultTextInput,
|
|
9
|
+
resolveOptionalTextInput,
|
|
10
|
+
resolveRequiredTextInput,
|
|
11
|
+
} from "#gh/text-input";
|
|
7
12
|
import {
|
|
8
13
|
CI_CHECK_WATCH_TIMEOUT_MS,
|
|
9
14
|
DEFAULT_DELETE_BRANCH,
|
|
@@ -70,9 +75,10 @@ export const prCreateCommand = Command.make(
|
|
|
70
75
|
Flag.withDescription("Base branch for the PR"),
|
|
71
76
|
Flag.withDefault("test"),
|
|
72
77
|
),
|
|
73
|
-
body: Flag.string("body").pipe(
|
|
74
|
-
|
|
75
|
-
Flag.
|
|
78
|
+
body: Flag.string("body").pipe(Flag.withDescription("PR body/description"), Flag.optional),
|
|
79
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
80
|
+
Flag.withDescription("Read PR body from a file path or '-' for stdin"),
|
|
81
|
+
Flag.optional,
|
|
76
82
|
),
|
|
77
83
|
draft: Flag.boolean("draft").pipe(
|
|
78
84
|
Flag.withDescription("Create as draft PR"),
|
|
@@ -85,11 +91,21 @@ export const prCreateCommand = Command.make(
|
|
|
85
91
|
),
|
|
86
92
|
title: Flag.string("title").pipe(Flag.withDescription("PR title")),
|
|
87
93
|
},
|
|
88
|
-
({ base, body, draft, format, head, title }) =>
|
|
94
|
+
({ base, body, bodyFile, draft, format, head, title }) =>
|
|
89
95
|
Effect.gen(function* () {
|
|
96
|
+
const resolvedBody = yield* resolveDefaultTextInput(
|
|
97
|
+
"gh-tool pr create",
|
|
98
|
+
Option.getOrNull(body),
|
|
99
|
+
Option.getOrNull(bodyFile),
|
|
100
|
+
"--body",
|
|
101
|
+
"--body-file",
|
|
102
|
+
"body",
|
|
103
|
+
"",
|
|
104
|
+
);
|
|
105
|
+
|
|
90
106
|
const info = yield* createPR({
|
|
91
107
|
base,
|
|
92
|
-
body,
|
|
108
|
+
body: resolvedBody,
|
|
93
109
|
draft,
|
|
94
110
|
head: Option.getOrNull(head),
|
|
95
111
|
title,
|
|
@@ -102,16 +118,29 @@ export const prEditCommand = Command.make(
|
|
|
102
118
|
"edit",
|
|
103
119
|
{
|
|
104
120
|
body: Flag.string("body").pipe(Flag.withDescription("New PR body/description"), Flag.optional),
|
|
121
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
122
|
+
Flag.withDescription("Read PR body from a file path or '-' for stdin"),
|
|
123
|
+
Flag.optional,
|
|
124
|
+
),
|
|
105
125
|
format: formatOption,
|
|
106
126
|
pr: Flag.integer("pr").pipe(Flag.withDescription("PR number to edit")),
|
|
107
127
|
title: Flag.string("title").pipe(Flag.withDescription("New PR title"), Flag.optional),
|
|
108
128
|
},
|
|
109
|
-
({ body, format, pr, title }) =>
|
|
129
|
+
({ body, bodyFile, format, pr, title }) =>
|
|
110
130
|
Effect.gen(function* () {
|
|
131
|
+
const resolvedBody = yield* resolveOptionalTextInput(
|
|
132
|
+
"gh-tool pr edit",
|
|
133
|
+
Option.getOrNull(body),
|
|
134
|
+
Option.getOrNull(bodyFile),
|
|
135
|
+
"--body",
|
|
136
|
+
"--body-file",
|
|
137
|
+
"body",
|
|
138
|
+
);
|
|
139
|
+
|
|
111
140
|
const info = yield* editPR({
|
|
112
141
|
pr,
|
|
113
142
|
title: Option.getOrNull(title),
|
|
114
|
-
body:
|
|
143
|
+
body: resolvedBody,
|
|
115
144
|
});
|
|
116
145
|
yield* logFormatted(info, format);
|
|
117
146
|
}),
|
|
@@ -338,17 +367,32 @@ export const prIssueCommentsLatestCommand = Command.make(
|
|
|
338
367
|
export const prCommentCommand = Command.make(
|
|
339
368
|
"comment",
|
|
340
369
|
{
|
|
341
|
-
body: Flag.string("body").pipe(
|
|
370
|
+
body: Flag.string("body").pipe(
|
|
371
|
+
Flag.withDescription("General PR comment body text"),
|
|
372
|
+
Flag.optional,
|
|
373
|
+
),
|
|
374
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
375
|
+
Flag.withDescription("Read general PR comment body from a file path or '-' for stdin"),
|
|
376
|
+
Flag.optional,
|
|
377
|
+
),
|
|
342
378
|
format: formatOption,
|
|
343
379
|
pr: Flag.integer("pr").pipe(
|
|
344
380
|
Flag.withDescription("PR number (default: current branch PR)"),
|
|
345
381
|
Flag.optional,
|
|
346
382
|
),
|
|
347
383
|
},
|
|
348
|
-
({ body, format, pr }) =>
|
|
384
|
+
({ body, bodyFile, format, pr }) =>
|
|
349
385
|
Effect.gen(function* () {
|
|
350
386
|
const prNumber = Option.getOrNull(pr);
|
|
351
|
-
const
|
|
387
|
+
const resolvedBody = yield* resolveRequiredTextInput(
|
|
388
|
+
"gh-tool pr comment",
|
|
389
|
+
Option.getOrNull(body),
|
|
390
|
+
Option.getOrNull(bodyFile),
|
|
391
|
+
"--body",
|
|
392
|
+
"--body-file",
|
|
393
|
+
"body",
|
|
394
|
+
);
|
|
395
|
+
const result = yield* postIssueComment(prNumber, resolvedBody);
|
|
352
396
|
yield* logFormatted(result, format);
|
|
353
397
|
}),
|
|
354
398
|
).pipe(Command.withDescription("Post a general PR discussion comment"));
|
|
@@ -375,7 +419,11 @@ export const prDiscussionSummaryCommand = Command.make(
|
|
|
375
419
|
export const prReplyCommand = Command.make(
|
|
376
420
|
"reply",
|
|
377
421
|
{
|
|
378
|
-
body: Flag.string("body").pipe(Flag.withDescription("Reply body text")),
|
|
422
|
+
body: Flag.string("body").pipe(Flag.withDescription("Reply body text"), Flag.optional),
|
|
423
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
424
|
+
Flag.withDescription("Read reply body from a file path or '-' for stdin"),
|
|
425
|
+
Flag.optional,
|
|
426
|
+
),
|
|
379
427
|
commentId: Flag.integer("comment-id").pipe(
|
|
380
428
|
Flag.withDescription("ID of the comment to reply to"),
|
|
381
429
|
),
|
|
@@ -385,10 +433,18 @@ export const prReplyCommand = Command.make(
|
|
|
385
433
|
Flag.optional,
|
|
386
434
|
),
|
|
387
435
|
},
|
|
388
|
-
({ body, commentId, format, pr }) =>
|
|
436
|
+
({ body, bodyFile, commentId, format, pr }) =>
|
|
389
437
|
Effect.gen(function* () {
|
|
390
438
|
const prNumber = Option.getOrNull(pr);
|
|
391
|
-
const
|
|
439
|
+
const resolvedBody = yield* resolveRequiredTextInput(
|
|
440
|
+
"gh-tool pr reply",
|
|
441
|
+
Option.getOrNull(body),
|
|
442
|
+
Option.getOrNull(bodyFile),
|
|
443
|
+
"--body",
|
|
444
|
+
"--body-file",
|
|
445
|
+
"body",
|
|
446
|
+
);
|
|
447
|
+
const result = yield* replyToComment(prNumber, commentId, resolvedBody);
|
|
392
448
|
yield* logFormatted(result, format);
|
|
393
449
|
}),
|
|
394
450
|
).pipe(Command.withDescription("Reply to an inline review comment"));
|
|
@@ -415,6 +471,10 @@ export const prSubmitReviewCommand = Command.make(
|
|
|
415
471
|
Flag.withDescription("Optional review body text when submitting"),
|
|
416
472
|
Flag.optional,
|
|
417
473
|
),
|
|
474
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
475
|
+
Flag.withDescription("Read review body from a file path or '-' for stdin"),
|
|
476
|
+
Flag.optional,
|
|
477
|
+
),
|
|
418
478
|
format: formatOption,
|
|
419
479
|
pr: Flag.integer("pr").pipe(
|
|
420
480
|
Flag.withDescription("PR number (default: current branch PR)"),
|
|
@@ -427,11 +487,18 @@ export const prSubmitReviewCommand = Command.make(
|
|
|
427
487
|
Flag.optional,
|
|
428
488
|
),
|
|
429
489
|
},
|
|
430
|
-
({ body, format, pr, reviewId }) =>
|
|
490
|
+
({ body, bodyFile, format, pr, reviewId }) =>
|
|
431
491
|
Effect.gen(function* () {
|
|
432
492
|
const prNumber = Option.getOrNull(pr);
|
|
433
493
|
const reviewIdValue = Option.getOrNull(reviewId);
|
|
434
|
-
const bodyValue =
|
|
494
|
+
const bodyValue = yield* resolveOptionalTextInput(
|
|
495
|
+
"gh-tool pr submit-review",
|
|
496
|
+
Option.getOrNull(body),
|
|
497
|
+
Option.getOrNull(bodyFile),
|
|
498
|
+
"--body",
|
|
499
|
+
"--body-file",
|
|
500
|
+
"body",
|
|
501
|
+
);
|
|
435
502
|
const result = yield* submitPendingReview(prNumber, reviewIdValue, bodyValue);
|
|
436
503
|
yield* logFormatted(result, format);
|
|
437
504
|
}),
|
|
@@ -471,7 +538,11 @@ export const prReviewTriageCommand = Command.make(
|
|
|
471
538
|
export const prReplyAndResolveCommand = Command.make(
|
|
472
539
|
"reply-and-resolve",
|
|
473
540
|
{
|
|
474
|
-
body: Flag.string("body").pipe(Flag.withDescription("Reply body text")),
|
|
541
|
+
body: Flag.string("body").pipe(Flag.withDescription("Reply body text"), Flag.optional),
|
|
542
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
543
|
+
Flag.withDescription("Read reply body from a file path or '-' for stdin"),
|
|
544
|
+
Flag.optional,
|
|
545
|
+
),
|
|
475
546
|
commentId: Flag.integer("comment-id").pipe(
|
|
476
547
|
Flag.withDescription("ID of the comment to reply to"),
|
|
477
548
|
),
|
|
@@ -484,10 +555,18 @@ export const prReplyAndResolveCommand = Command.make(
|
|
|
484
555
|
Flag.withDescription("GraphQL node ID of the thread to resolve"),
|
|
485
556
|
),
|
|
486
557
|
},
|
|
487
|
-
({ body, commentId, format, pr, threadId }) =>
|
|
558
|
+
({ body, bodyFile, commentId, format, pr, threadId }) =>
|
|
488
559
|
Effect.gen(function* () {
|
|
489
560
|
const prNumber = Option.getOrNull(pr);
|
|
490
|
-
const
|
|
561
|
+
const resolvedBody = yield* resolveRequiredTextInput(
|
|
562
|
+
"gh-tool pr reply-and-resolve",
|
|
563
|
+
Option.getOrNull(body),
|
|
564
|
+
Option.getOrNull(bodyFile),
|
|
565
|
+
"--body",
|
|
566
|
+
"--body-file",
|
|
567
|
+
"body",
|
|
568
|
+
);
|
|
569
|
+
const replyResult = yield* replyToComment(prNumber, commentId, resolvedBody);
|
|
491
570
|
const resolveResult = yield* resolveThread(threadId);
|
|
492
571
|
yield* logFormatted({ reply: replyResult, resolve: resolveResult }, format);
|
|
493
572
|
}),
|
package/src/gh-tool/release.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Command, Flag } from "effect/unstable/cli";
|
|
|
2
2
|
import { Effect, Option } from "effect";
|
|
3
3
|
|
|
4
4
|
import { formatOption, logFormatted } from "#shared";
|
|
5
|
+
import { resolveOptionalTextInput } from "#gh/text-input";
|
|
5
6
|
import { GitHubService } from "./service";
|
|
6
7
|
|
|
7
8
|
type ReleaseListItem = {
|
|
@@ -343,6 +344,10 @@ export const releaseCreateCommand = Command.make(
|
|
|
343
344
|
Flag.withDescription("Release notes body (markdown)"),
|
|
344
345
|
Flag.optional,
|
|
345
346
|
),
|
|
347
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
348
|
+
Flag.withDescription("Read release notes body from a file path or '-' for stdin"),
|
|
349
|
+
Flag.optional,
|
|
350
|
+
),
|
|
346
351
|
draft: Flag.boolean("draft").pipe(
|
|
347
352
|
Flag.withDescription("Create as draft release"),
|
|
348
353
|
Flag.withDefault(false),
|
|
@@ -388,6 +393,7 @@ export const releaseCreateCommand = Command.make(
|
|
|
388
393
|
},
|
|
389
394
|
({
|
|
390
395
|
body,
|
|
396
|
+
bodyFile,
|
|
391
397
|
draft,
|
|
392
398
|
format,
|
|
393
399
|
generateNotes,
|
|
@@ -402,10 +408,19 @@ export const releaseCreateCommand = Command.make(
|
|
|
402
408
|
verifyTag,
|
|
403
409
|
}) =>
|
|
404
410
|
Effect.gen(function* () {
|
|
411
|
+
const resolvedBody = yield* resolveOptionalTextInput(
|
|
412
|
+
"gh-tool release create",
|
|
413
|
+
Option.getOrNull(body),
|
|
414
|
+
Option.getOrNull(bodyFile),
|
|
415
|
+
"--body",
|
|
416
|
+
"--body-file",
|
|
417
|
+
"body",
|
|
418
|
+
);
|
|
419
|
+
|
|
405
420
|
const result = yield* createRelease({
|
|
406
421
|
tag,
|
|
407
422
|
title: Option.getOrNull(title),
|
|
408
|
-
body:
|
|
423
|
+
body: resolvedBody,
|
|
409
424
|
notesFile: Option.getOrNull(notesFile),
|
|
410
425
|
draft,
|
|
411
426
|
prerelease,
|
|
@@ -477,6 +492,10 @@ export const releaseEditCommand = Command.make(
|
|
|
477
492
|
Flag.withDescription("New release notes body (markdown)"),
|
|
478
493
|
Flag.optional,
|
|
479
494
|
),
|
|
495
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
496
|
+
Flag.withDescription("Read release notes body from a file path or '-' for stdin"),
|
|
497
|
+
Flag.optional,
|
|
498
|
+
),
|
|
480
499
|
draft: Flag.boolean("draft").pipe(
|
|
481
500
|
Flag.withDescription("Set draft status (true/false). Omit to keep current value"),
|
|
482
501
|
Flag.optional,
|
|
@@ -497,12 +516,21 @@ export const releaseEditCommand = Command.make(
|
|
|
497
516
|
tag: Flag.string("tag").pipe(Flag.withDescription("Release tag to edit (e.g., v1.2.3)")),
|
|
498
517
|
title: Flag.string("title").pipe(Flag.withDescription("New release title"), Flag.optional),
|
|
499
518
|
},
|
|
500
|
-
({ body, draft, format, latest, prerelease, repo, tag, title }) =>
|
|
519
|
+
({ body, bodyFile, draft, format, latest, prerelease, repo, tag, title }) =>
|
|
501
520
|
Effect.gen(function* () {
|
|
521
|
+
const resolvedBody = yield* resolveOptionalTextInput(
|
|
522
|
+
"gh-tool release edit",
|
|
523
|
+
Option.getOrNull(body),
|
|
524
|
+
Option.getOrNull(bodyFile),
|
|
525
|
+
"--body",
|
|
526
|
+
"--body-file",
|
|
527
|
+
"body",
|
|
528
|
+
);
|
|
529
|
+
|
|
502
530
|
const edited = yield* editRelease({
|
|
503
531
|
tag,
|
|
504
532
|
title: Option.getOrNull(title),
|
|
505
|
-
body:
|
|
533
|
+
body: resolvedBody,
|
|
506
534
|
draft: Option.getOrNull(draft),
|
|
507
535
|
prerelease: Option.getOrNull(prerelease),
|
|
508
536
|
latest: Option.getOrNull(latest),
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
import { GitHubCommandError } from "#gh/errors";
|
|
4
|
+
|
|
5
|
+
const STDIN_SENTINEL = "-";
|
|
6
|
+
const SENSITIVE_PATH_PATTERNS = [/\.env(\..+)?$/, /\.envrc$/, /\.(pem|key|p12|pfx|cer|crt)$/i];
|
|
7
|
+
const MissingMode = Schema.Literals(["error", "null", "default"]);
|
|
8
|
+
|
|
9
|
+
const readTextFromStdin = () => Bun.stdin.text();
|
|
10
|
+
|
|
11
|
+
const readTextFile = (filePath: string) => {
|
|
12
|
+
if (SENSITIVE_PATH_PATTERNS.some((pattern) => pattern.test(filePath))) {
|
|
13
|
+
return Promise.reject(new Error(`Refusing to read sensitive file: ${filePath}`));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return Bun.file(filePath).text();
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const ensureResolvedText = (resolvedValue: string | null, context: string) => {
|
|
20
|
+
if (resolvedValue === null) {
|
|
21
|
+
throw new Error(`Invariant violation: ${context} resolved to null`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return resolvedValue;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type ResolveTextInputOptions = {
|
|
28
|
+
command: string;
|
|
29
|
+
value: string | null;
|
|
30
|
+
fileValue: string | null;
|
|
31
|
+
valueFlag: string;
|
|
32
|
+
fileFlag: string;
|
|
33
|
+
missingMode: Schema.Schema.Type<typeof MissingMode>;
|
|
34
|
+
missingValue?: string;
|
|
35
|
+
label: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const resolveTextInputInternal = Effect.fn("gh.resolveTextInputInternal")(function* (
|
|
39
|
+
options: ResolveTextInputOptions,
|
|
40
|
+
) {
|
|
41
|
+
const { command, fileFlag, fileValue, label, missingMode, missingValue, value, valueFlag } =
|
|
42
|
+
options;
|
|
43
|
+
|
|
44
|
+
if (value !== null && fileValue !== null) {
|
|
45
|
+
return yield* Effect.fail(
|
|
46
|
+
new GitHubCommandError({
|
|
47
|
+
command,
|
|
48
|
+
exitCode: 1,
|
|
49
|
+
stderr: `Provide exactly one of ${valueFlag} or ${fileFlag}`,
|
|
50
|
+
message: `Provide exactly one of ${valueFlag} or ${fileFlag}`,
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (value !== null) {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (fileValue !== null) {
|
|
60
|
+
const source = fileValue === STDIN_SENTINEL ? "stdin" : fileValue;
|
|
61
|
+
|
|
62
|
+
return yield* Effect.tryPromise({
|
|
63
|
+
try: () => (fileValue === STDIN_SENTINEL ? readTextFromStdin() : readTextFile(fileValue)),
|
|
64
|
+
catch: (error) =>
|
|
65
|
+
new GitHubCommandError({
|
|
66
|
+
command,
|
|
67
|
+
exitCode: 1,
|
|
68
|
+
stderr: `Failed to read ${label} from ${source}: ${error instanceof Error ? error.message : String(error)}`,
|
|
69
|
+
message: `Failed to read ${label} from ${source}: ${error instanceof Error ? error.message : String(error)}`,
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (missingMode === "null") {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (missingMode === "default") {
|
|
79
|
+
return missingValue ?? "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return yield* Effect.fail(
|
|
83
|
+
new GitHubCommandError({
|
|
84
|
+
command,
|
|
85
|
+
exitCode: 1,
|
|
86
|
+
stderr: `Missing ${label}. Provide ${valueFlag} or ${fileFlag}`,
|
|
87
|
+
message: `Missing ${label}. Provide ${valueFlag} or ${fileFlag}`,
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export const resolveRequiredTextInput = (
|
|
93
|
+
command: string,
|
|
94
|
+
value: string | null,
|
|
95
|
+
fileValue: string | null,
|
|
96
|
+
valueFlag: string,
|
|
97
|
+
fileFlag: string,
|
|
98
|
+
label: string,
|
|
99
|
+
): Effect.Effect<string, GitHubCommandError> =>
|
|
100
|
+
resolveTextInputInternal({
|
|
101
|
+
command,
|
|
102
|
+
value,
|
|
103
|
+
fileValue,
|
|
104
|
+
valueFlag,
|
|
105
|
+
fileFlag,
|
|
106
|
+
missingMode: "error",
|
|
107
|
+
label,
|
|
108
|
+
}).pipe(Effect.map((resolvedValue) => ensureResolvedText(resolvedValue, "required text input")));
|
|
109
|
+
|
|
110
|
+
export const resolveOptionalTextInput = (
|
|
111
|
+
command: string,
|
|
112
|
+
value: string | null,
|
|
113
|
+
fileValue: string | null,
|
|
114
|
+
valueFlag: string,
|
|
115
|
+
fileFlag: string,
|
|
116
|
+
label: string,
|
|
117
|
+
): Effect.Effect<string | null, GitHubCommandError> =>
|
|
118
|
+
resolveTextInputInternal({
|
|
119
|
+
command,
|
|
120
|
+
value,
|
|
121
|
+
fileValue,
|
|
122
|
+
valueFlag,
|
|
123
|
+
fileFlag,
|
|
124
|
+
missingMode: "null",
|
|
125
|
+
label,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export const resolveDefaultTextInput = (
|
|
129
|
+
command: string,
|
|
130
|
+
value: string | null,
|
|
131
|
+
fileValue: string | null,
|
|
132
|
+
valueFlag: string,
|
|
133
|
+
fileFlag: string,
|
|
134
|
+
label: string,
|
|
135
|
+
defaultValue: string,
|
|
136
|
+
): Effect.Effect<string, GitHubCommandError> =>
|
|
137
|
+
resolveTextInputInternal({
|
|
138
|
+
command,
|
|
139
|
+
value,
|
|
140
|
+
fileValue,
|
|
141
|
+
valueFlag,
|
|
142
|
+
fileFlag,
|
|
143
|
+
missingMode: "default",
|
|
144
|
+
missingValue: defaultValue,
|
|
145
|
+
label,
|
|
146
|
+
}).pipe(Effect.map((resolvedValue) => ensureResolvedText(resolvedValue, "default text input")));
|