@blogic-cz/agent-tools 0.2.7 → 0.4.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.
- package/README.md +82 -5
- package/package.json +1 -1
- package/src/gh-tool/index.ts +33 -6
- package/src/gh-tool/issue/commands.ts +145 -0
- package/src/gh-tool/issue/core.ts +208 -0
- package/src/gh-tool/issue/index.ts +9 -0
- package/src/gh-tool/issue/triage.ts +320 -0
- package/src/gh-tool/release.ts +561 -0
- package/src/gh-tool/issue.ts +0 -361
package/README.md
CHANGED
|
@@ -15,10 +15,10 @@ These tools wrap each CLI with:
|
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
18
|
-
> **Recommended:** Copy the repo URL and tell your AI agent to install it. The agent will set up everything —
|
|
18
|
+
> **Recommended:** Copy the repo URL and tell your AI agent to install it. The agent will set up everything — dev dependency, config file, credential guard, skill — in the right places for your project.
|
|
19
19
|
>
|
|
20
20
|
> ```
|
|
21
|
-
> Install @blogic-cz/agent-tools from https://github.com/blogic-cz/agent-tools
|
|
21
|
+
> Install @blogic-cz/agent-tools from https://github.com/blogic-cz/agent-tools as a dev dependency, install the agent-tools skill, set up the credential guard, and configure the tools I need for this project.
|
|
22
22
|
> ```
|
|
23
23
|
|
|
24
24
|
### Manual installation
|
|
@@ -31,11 +31,88 @@ bun add -d @blogic-cz/agent-tools
|
|
|
31
31
|
|
|
32
32
|
### For agents reading this
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Follow these steps to set up agent-tools in the consumer project.
|
|
35
|
+
|
|
36
|
+
**Step 1 — Install**
|
|
37
|
+
|
|
38
|
+
- **JavaScript/TypeScript project** (has `package.json`): Install as a **dev dependency**:
|
|
39
|
+
```bash
|
|
40
|
+
bun add -d @blogic-cz/agent-tools
|
|
41
|
+
```
|
|
42
|
+
- **Non-JavaScript project** (no `package.json`): Skip installation — run tools directly with `bunx`:
|
|
43
|
+
```bash
|
|
44
|
+
bunx agent-tools-gh pr status
|
|
45
|
+
bunx agent-tools-k8s pods --env test
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Step 2 — Configure**
|
|
49
|
+
|
|
50
|
+
Ask the user which tools they need (gh, db, k8s, az, logs, session). Create `agent-tools.json5` in the project root with **only the selected tools configured**. See [`examples/agent-tools.json5`](./examples/agent-tools.json5) for the full config reference with all options documented.
|
|
51
|
+
|
|
52
|
+
Minimal starting config:
|
|
53
|
+
|
|
54
|
+
```json5
|
|
55
|
+
{
|
|
56
|
+
$schema: "https://raw.githubusercontent.com/blogic-cz/agent-tools/main/schemas/agent-tools.schema.json",
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Step 3 — Credential Guard**
|
|
61
|
+
|
|
62
|
+
Set up the credential guard for the agent's platform:
|
|
63
|
+
|
|
64
|
+
- **Claude Code**: Add the `PreToolUse` hook to `.claude/settings.json` — see [Setup for Claude Code](#setup-for-claude-code)
|
|
65
|
+
- **OpenCode**: Create the plugin at `.opencode/plugins/credential-guard.ts` — see [Setup for OpenCode](#setup-for-opencode)
|
|
66
|
+
|
|
67
|
+
**Step 4 — Test & Demo**
|
|
68
|
+
|
|
69
|
+
For each tool the user selected in Step 2:
|
|
70
|
+
|
|
71
|
+
1. Run `bunx agent-tools-<tool> --help` to verify it works
|
|
72
|
+
2. Summarize the key commands available
|
|
73
|
+
3. Show the user what data they have access to based on their `agent-tools.json5` — e.g. which environments, clusters, namespaces, databases, or profiles are configured and reachable
|
|
74
|
+
|
|
75
|
+
**Step 5 — Skill & Agent Docs**
|
|
76
|
+
|
|
77
|
+
Tell the user to run the following command themselves — it's interactive and lets them choose which agents to install the skill for:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx skills add blogic-cz/agent-tools
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Do not run this command for the user** — it requires interactive selection.
|
|
84
|
+
|
|
85
|
+
Then update the project's `AGENTS.md` and/or `CLAUDE.md`:
|
|
86
|
+
|
|
87
|
+
1. Add an `agent-tools` row to the skills table (if one exists):
|
|
88
|
+
```markdown
|
|
89
|
+
| Agent wrapper tools (`db-tool`, `k8s-tool`, `logs-tool`, `az-tool`, `gh` patterns) | `agent-tools` |
|
|
90
|
+
```
|
|
91
|
+
2. Add or update the **Tooling** section:
|
|
92
|
+
|
|
93
|
+
```markdown
|
|
94
|
+
## Tooling
|
|
95
|
+
|
|
96
|
+
For tool wrappers and operational patterns, load `agent-tools`.
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Step 6 — Custom Tool Scaffold**
|
|
100
|
+
|
|
101
|
+
Create an `agent-tools/` directory in the project root with an example tool so the user has a working template for building project-specific tools. Copy the scaffold from [`examples/custom-tool/`](./examples/custom-tool/):
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
agent-tools/
|
|
105
|
+
package.json # private package depending on @blogic-cz/agent-tools
|
|
106
|
+
tsconfig.json # extends root tsconfig
|
|
107
|
+
noop.ts # placeholder export for typecheck
|
|
108
|
+
example-tool/
|
|
109
|
+
index.ts # ping-pong example using Effect CLI
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
After creating the files, run `bun install` in the `agent-tools/` directory (or from the workspace root if it's a monorepo). Then verify:
|
|
35
113
|
|
|
36
114
|
```bash
|
|
37
|
-
|
|
38
|
-
ls src/ # gh-tool/ db-tool/ k8s-tool/ az-tool/ logs-tool/ session-tool/ credential-guard/
|
|
115
|
+
bun run agent-tools/example-tool/index.ts ping
|
|
39
116
|
```
|
|
40
117
|
|
|
41
118
|
## Quick Start
|
package/package.json
CHANGED
package/src/gh-tool/index.ts
CHANGED
|
@@ -5,12 +5,13 @@ import { Effect, Layer } from "effect";
|
|
|
5
5
|
|
|
6
6
|
import { renderCauseToStderr, VERSION } from "#shared";
|
|
7
7
|
import {
|
|
8
|
-
issueListCommand,
|
|
9
|
-
issueViewCommand,
|
|
10
8
|
issueCloseCommand,
|
|
11
|
-
issueReopenCommand,
|
|
12
9
|
issueCommentCommand,
|
|
13
10
|
issueEditCommand,
|
|
11
|
+
issueListCommand,
|
|
12
|
+
issueReopenCommand,
|
|
13
|
+
issueTriageSummaryCommand,
|
|
14
|
+
issueViewCommand,
|
|
14
15
|
} from "./issue";
|
|
15
16
|
import {
|
|
16
17
|
prViewCommand,
|
|
@@ -33,6 +34,14 @@ import {
|
|
|
33
34
|
prReplyAndResolveCommand,
|
|
34
35
|
prReviewTriageCommand,
|
|
35
36
|
} from "./pr/index";
|
|
37
|
+
import {
|
|
38
|
+
releaseCreateCommand,
|
|
39
|
+
releaseDeleteCommand,
|
|
40
|
+
releaseEditCommand,
|
|
41
|
+
releaseListCommand,
|
|
42
|
+
releaseStatusCommand,
|
|
43
|
+
releaseViewCommand,
|
|
44
|
+
} from "./release";
|
|
36
45
|
import { repoInfoCommand, repoListCommand, repoSearchCodeCommand } from "./repo";
|
|
37
46
|
import { GitHubService } from "./service";
|
|
38
47
|
import {
|
|
@@ -72,7 +81,9 @@ const prCommand = Command.make("pr", {}).pipe(
|
|
|
72
81
|
);
|
|
73
82
|
|
|
74
83
|
const issueCommand = Command.make("issue", {}).pipe(
|
|
75
|
-
Command.withDescription(
|
|
84
|
+
Command.withDescription(
|
|
85
|
+
"Issue operations (list, view, close, reopen, comment, edit, triage-summary)",
|
|
86
|
+
),
|
|
76
87
|
Command.withSubcommands([
|
|
77
88
|
issueListCommand,
|
|
78
89
|
issueViewCommand,
|
|
@@ -80,6 +91,7 @@ const issueCommand = Command.make("issue", {}).pipe(
|
|
|
80
91
|
issueReopenCommand,
|
|
81
92
|
issueCommentCommand,
|
|
82
93
|
issueEditCommand,
|
|
94
|
+
issueTriageSummaryCommand,
|
|
83
95
|
]),
|
|
84
96
|
);
|
|
85
97
|
|
|
@@ -104,6 +116,18 @@ const workflowCommand = Command.make("workflow", {}).pipe(
|
|
|
104
116
|
]),
|
|
105
117
|
);
|
|
106
118
|
|
|
119
|
+
const releaseCommand = Command.make("release", {}).pipe(
|
|
120
|
+
Command.withDescription("Release operations (create, list, view, edit, delete, status)"),
|
|
121
|
+
Command.withSubcommands([
|
|
122
|
+
releaseCreateCommand,
|
|
123
|
+
releaseListCommand,
|
|
124
|
+
releaseViewCommand,
|
|
125
|
+
releaseEditCommand,
|
|
126
|
+
releaseDeleteCommand,
|
|
127
|
+
releaseStatusCommand,
|
|
128
|
+
]),
|
|
129
|
+
);
|
|
130
|
+
|
|
107
131
|
const mainCommand = Command.make("gh-tool", {}).pipe(
|
|
108
132
|
Command.withDescription(
|
|
109
133
|
`GitHub CLI Tool for Coding Agents
|
|
@@ -126,9 +150,12 @@ WORKFLOW FOR AI AGENTS:
|
|
|
126
150
|
12. Use 'workflow view --run N' to inspect a specific run with jobs/steps
|
|
127
151
|
13. Use 'workflow logs --run N' to get logs (failed jobs by default)
|
|
128
152
|
14. Use 'workflow job-logs --run N --job "build-web-app"' to get clean parsed logs for a specific job
|
|
129
|
-
15. Use 'workflow watch --run N' to watch until completion
|
|
153
|
+
15. Use 'workflow watch --run N' to watch until completion
|
|
154
|
+
16. Use 'release status' to inspect latest release + repository context
|
|
155
|
+
17. Use 'release create --tag vX.Y.Z --generate-notes' to publish a release
|
|
156
|
+
18. Use 'release edit/view/list/delete' to maintain existing releases`,
|
|
130
157
|
),
|
|
131
|
-
Command.withSubcommands([prCommand, issueCommand, repoCommand, workflowCommand]),
|
|
158
|
+
Command.withSubcommands([prCommand, issueCommand, repoCommand, workflowCommand, releaseCommand]),
|
|
132
159
|
);
|
|
133
160
|
|
|
134
161
|
const cli = Command.run(mainCommand, {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
2
|
+
import { Effect, Option } from "effect";
|
|
3
|
+
|
|
4
|
+
import { formatOption, logFormatted } from "#shared";
|
|
5
|
+
|
|
6
|
+
import { closeIssue, commentOnIssue, editIssue, listIssues, reopenIssue, viewIssue } from "./core";
|
|
7
|
+
|
|
8
|
+
export const issueListCommand = Command.make(
|
|
9
|
+
"list",
|
|
10
|
+
{
|
|
11
|
+
format: formatOption,
|
|
12
|
+
labels: Flag.string("labels").pipe(
|
|
13
|
+
Flag.withDescription("Filter by label (comma-separated)"),
|
|
14
|
+
Flag.optional,
|
|
15
|
+
),
|
|
16
|
+
limit: Flag.integer("limit").pipe(
|
|
17
|
+
Flag.withDescription("Maximum number of issues to return"),
|
|
18
|
+
Flag.withDefault(30),
|
|
19
|
+
),
|
|
20
|
+
state: Flag.choice("state", ["open", "closed", "all"]).pipe(
|
|
21
|
+
Flag.withDescription("Filter by state: open, closed, all"),
|
|
22
|
+
Flag.withDefault("open"),
|
|
23
|
+
),
|
|
24
|
+
},
|
|
25
|
+
({ format, labels, limit, state }) =>
|
|
26
|
+
Effect.gen(function* () {
|
|
27
|
+
const issues = yield* listIssues({
|
|
28
|
+
labels: Option.getOrNull(labels),
|
|
29
|
+
limit,
|
|
30
|
+
state,
|
|
31
|
+
});
|
|
32
|
+
yield* logFormatted(issues, format);
|
|
33
|
+
}),
|
|
34
|
+
).pipe(Command.withDescription("List issues (default: open, use --state to filter)"));
|
|
35
|
+
|
|
36
|
+
export const issueViewCommand = Command.make(
|
|
37
|
+
"view",
|
|
38
|
+
{
|
|
39
|
+
format: formatOption,
|
|
40
|
+
issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number")),
|
|
41
|
+
},
|
|
42
|
+
({ format, issue }) =>
|
|
43
|
+
Effect.gen(function* () {
|
|
44
|
+
const info = yield* viewIssue(issue);
|
|
45
|
+
yield* logFormatted(info, format);
|
|
46
|
+
}),
|
|
47
|
+
).pipe(Command.withDescription("View issue details"));
|
|
48
|
+
|
|
49
|
+
export const issueCloseCommand = Command.make(
|
|
50
|
+
"close",
|
|
51
|
+
{
|
|
52
|
+
comment: Flag.string("comment").pipe(
|
|
53
|
+
Flag.withDescription("Comment to add when closing"),
|
|
54
|
+
Flag.optional,
|
|
55
|
+
),
|
|
56
|
+
format: formatOption,
|
|
57
|
+
issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to close")),
|
|
58
|
+
reason: Flag.choice("reason", ["completed", "not planned"]).pipe(
|
|
59
|
+
Flag.withDescription("Close reason: completed, not planned"),
|
|
60
|
+
Flag.withDefault("completed"),
|
|
61
|
+
),
|
|
62
|
+
},
|
|
63
|
+
({ comment, format, issue, reason }) =>
|
|
64
|
+
Effect.gen(function* () {
|
|
65
|
+
const result = yield* closeIssue({
|
|
66
|
+
comment: Option.getOrNull(comment),
|
|
67
|
+
issue,
|
|
68
|
+
reason,
|
|
69
|
+
});
|
|
70
|
+
yield* logFormatted(result, format);
|
|
71
|
+
}),
|
|
72
|
+
).pipe(Command.withDescription("Close an issue with optional comment and reason"));
|
|
73
|
+
|
|
74
|
+
export const issueReopenCommand = Command.make(
|
|
75
|
+
"reopen",
|
|
76
|
+
{
|
|
77
|
+
comment: Flag.string("comment").pipe(
|
|
78
|
+
Flag.withDescription("Comment to add when reopening"),
|
|
79
|
+
Flag.optional,
|
|
80
|
+
),
|
|
81
|
+
format: formatOption,
|
|
82
|
+
issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to reopen")),
|
|
83
|
+
},
|
|
84
|
+
({ comment, format, issue }) =>
|
|
85
|
+
Effect.gen(function* () {
|
|
86
|
+
const result = yield* reopenIssue({
|
|
87
|
+
comment: Option.getOrNull(comment),
|
|
88
|
+
issue,
|
|
89
|
+
});
|
|
90
|
+
yield* logFormatted(result, format);
|
|
91
|
+
}),
|
|
92
|
+
).pipe(Command.withDescription("Reopen a closed issue"));
|
|
93
|
+
|
|
94
|
+
export const issueCommentCommand = Command.make(
|
|
95
|
+
"comment",
|
|
96
|
+
{
|
|
97
|
+
body: Flag.string("body").pipe(Flag.withDescription("Comment body text")),
|
|
98
|
+
format: formatOption,
|
|
99
|
+
issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to comment on")),
|
|
100
|
+
},
|
|
101
|
+
({ body, format, issue }) =>
|
|
102
|
+
Effect.gen(function* () {
|
|
103
|
+
const result = yield* commentOnIssue({ body, issue });
|
|
104
|
+
yield* logFormatted(result, format);
|
|
105
|
+
}),
|
|
106
|
+
).pipe(Command.withDescription("Post a comment on an issue"));
|
|
107
|
+
|
|
108
|
+
export const issueEditCommand = Command.make(
|
|
109
|
+
"edit",
|
|
110
|
+
{
|
|
111
|
+
addAssignee: Flag.string("add-assignee").pipe(
|
|
112
|
+
Flag.withDescription("Add assignee login (comma-separated for multiple)"),
|
|
113
|
+
Flag.optional,
|
|
114
|
+
),
|
|
115
|
+
addLabels: Flag.string("add-labels").pipe(
|
|
116
|
+
Flag.withDescription("Add labels (comma-separated)"),
|
|
117
|
+
Flag.optional,
|
|
118
|
+
),
|
|
119
|
+
body: Flag.string("body").pipe(Flag.withDescription("New issue body"), Flag.optional),
|
|
120
|
+
format: formatOption,
|
|
121
|
+
issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to edit")),
|
|
122
|
+
removeAssignee: Flag.string("remove-assignee").pipe(
|
|
123
|
+
Flag.withDescription("Remove assignee login (comma-separated for multiple)"),
|
|
124
|
+
Flag.optional,
|
|
125
|
+
),
|
|
126
|
+
removeLabels: Flag.string("remove-labels").pipe(
|
|
127
|
+
Flag.withDescription("Remove labels (comma-separated)"),
|
|
128
|
+
Flag.optional,
|
|
129
|
+
),
|
|
130
|
+
title: Flag.string("title").pipe(Flag.withDescription("New issue title"), Flag.optional),
|
|
131
|
+
},
|
|
132
|
+
({ addAssignee, addLabels, body, format, issue, removeAssignee, removeLabels, title }) =>
|
|
133
|
+
Effect.gen(function* () {
|
|
134
|
+
const result = yield* editIssue({
|
|
135
|
+
addAssignee: Option.getOrNull(addAssignee),
|
|
136
|
+
addLabels: Option.getOrNull(addLabels),
|
|
137
|
+
body: Option.getOrNull(body),
|
|
138
|
+
issue,
|
|
139
|
+
removeAssignee: Option.getOrNull(removeAssignee),
|
|
140
|
+
removeLabels: Option.getOrNull(removeLabels),
|
|
141
|
+
title: Option.getOrNull(title),
|
|
142
|
+
});
|
|
143
|
+
yield* logFormatted(result, format);
|
|
144
|
+
}),
|
|
145
|
+
).pipe(Command.withDescription("Edit issue title, body, labels, or assignees"));
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
|
|
3
|
+
import { GitHubCommandError } from "#gh/errors";
|
|
4
|
+
import { GitHubService } from "#gh/service";
|
|
5
|
+
|
|
6
|
+
export type IssueInfo = {
|
|
7
|
+
number: number;
|
|
8
|
+
title: string;
|
|
9
|
+
state: string;
|
|
10
|
+
url: string;
|
|
11
|
+
labels: Array<{ name: string }>;
|
|
12
|
+
assignees: Array<{ login: string }>;
|
|
13
|
+
author: { login: string };
|
|
14
|
+
createdAt: string;
|
|
15
|
+
closedAt: string | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type IssueListItem = {
|
|
19
|
+
number: number;
|
|
20
|
+
title: string;
|
|
21
|
+
state: string;
|
|
22
|
+
url: string;
|
|
23
|
+
labels: Array<{ name: string }>;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type RawIssueComment = {
|
|
28
|
+
id: number;
|
|
29
|
+
user: { login: string };
|
|
30
|
+
body: string;
|
|
31
|
+
created_at: string;
|
|
32
|
+
html_url: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const listIssues = Effect.fn("issue.listIssues")(function* (opts: {
|
|
36
|
+
state: string;
|
|
37
|
+
labels: string | null;
|
|
38
|
+
limit: number;
|
|
39
|
+
}) {
|
|
40
|
+
const gh = yield* GitHubService;
|
|
41
|
+
|
|
42
|
+
const args = [
|
|
43
|
+
"issue",
|
|
44
|
+
"list",
|
|
45
|
+
"--state",
|
|
46
|
+
opts.state,
|
|
47
|
+
"--limit",
|
|
48
|
+
String(opts.limit),
|
|
49
|
+
"--json",
|
|
50
|
+
"number,title,state,url,labels,createdAt",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
if (opts.labels !== null) {
|
|
54
|
+
args.push("--label", opts.labels);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return yield* gh.runGhJson<IssueListItem[]>(args);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const viewIssue = Effect.fn("issue.viewIssue")(function* (issueNumber: number) {
|
|
61
|
+
const gh = yield* GitHubService;
|
|
62
|
+
|
|
63
|
+
return yield* gh.runGhJson<IssueInfo>([
|
|
64
|
+
"issue",
|
|
65
|
+
"view",
|
|
66
|
+
String(issueNumber),
|
|
67
|
+
"--json",
|
|
68
|
+
"number,title,state,url,labels,assignees,author,createdAt,closedAt",
|
|
69
|
+
]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const closeIssue = Effect.fn("issue.closeIssue")(function* (opts: {
|
|
73
|
+
issue: number;
|
|
74
|
+
comment: string | null;
|
|
75
|
+
reason: string;
|
|
76
|
+
}) {
|
|
77
|
+
const gh = yield* GitHubService;
|
|
78
|
+
|
|
79
|
+
const args = ["issue", "close", String(opts.issue), "--reason", opts.reason];
|
|
80
|
+
|
|
81
|
+
if (opts.comment !== null) {
|
|
82
|
+
args.push("--comment", opts.comment);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
yield* gh.runGh(args);
|
|
86
|
+
|
|
87
|
+
return yield* gh.runGhJson<IssueInfo>([
|
|
88
|
+
"issue",
|
|
89
|
+
"view",
|
|
90
|
+
String(opts.issue),
|
|
91
|
+
"--json",
|
|
92
|
+
"number,title,state,url,labels,assignees,author,createdAt,closedAt",
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
export const reopenIssue = Effect.fn("issue.reopenIssue")(function* (opts: {
|
|
97
|
+
issue: number;
|
|
98
|
+
comment: string | null;
|
|
99
|
+
}) {
|
|
100
|
+
const gh = yield* GitHubService;
|
|
101
|
+
|
|
102
|
+
const args = ["issue", "reopen", String(opts.issue)];
|
|
103
|
+
|
|
104
|
+
if (opts.comment !== null) {
|
|
105
|
+
args.push("--comment", opts.comment);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
yield* gh.runGh(args);
|
|
109
|
+
|
|
110
|
+
return yield* gh.runGhJson<IssueInfo>([
|
|
111
|
+
"issue",
|
|
112
|
+
"view",
|
|
113
|
+
String(opts.issue),
|
|
114
|
+
"--json",
|
|
115
|
+
"number,title,state,url,labels,assignees,author,createdAt,closedAt",
|
|
116
|
+
]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
export const commentOnIssue = Effect.fn("issue.commentOnIssue")(function* (opts: {
|
|
120
|
+
issue: number;
|
|
121
|
+
body: string;
|
|
122
|
+
}) {
|
|
123
|
+
const gh = yield* GitHubService;
|
|
124
|
+
const repoInfo = yield* gh.getRepoInfo();
|
|
125
|
+
|
|
126
|
+
const trimmedBody = opts.body.trim();
|
|
127
|
+
if (trimmedBody.length === 0) {
|
|
128
|
+
return yield* Effect.fail(
|
|
129
|
+
new GitHubCommandError({
|
|
130
|
+
command: "gh-tool issue comment",
|
|
131
|
+
exitCode: 0,
|
|
132
|
+
stderr: "Comment body cannot be empty",
|
|
133
|
+
message: "Comment body cannot be empty",
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const result = yield* gh.runGh([
|
|
139
|
+
"api",
|
|
140
|
+
"-X",
|
|
141
|
+
"POST",
|
|
142
|
+
`repos/${repoInfo.owner}/${repoInfo.name}/issues/${opts.issue}/comments`,
|
|
143
|
+
"-f",
|
|
144
|
+
`body=${trimmedBody}`,
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
const rawComment = yield* Effect.try({
|
|
148
|
+
try: () => JSON.parse(result.stdout) as RawIssueComment,
|
|
149
|
+
catch: (error) =>
|
|
150
|
+
new GitHubCommandError({
|
|
151
|
+
command: "gh-tool issue comment",
|
|
152
|
+
exitCode: 0,
|
|
153
|
+
stderr: `Failed to parse response: ${error instanceof Error ? error.message : String(error)}`,
|
|
154
|
+
message: `Failed to parse response: ${error instanceof Error ? error.message : String(error)}`,
|
|
155
|
+
}),
|
|
156
|
+
}).pipe(Effect.mapError((error) => error as GitHubCommandError));
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
id: rawComment.id,
|
|
160
|
+
author: rawComment.user.login,
|
|
161
|
+
body: rawComment.body,
|
|
162
|
+
createdAt: rawComment.created_at,
|
|
163
|
+
url: rawComment.html_url,
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
export const editIssue = Effect.fn("issue.editIssue")(function* (opts: {
|
|
168
|
+
issue: number;
|
|
169
|
+
title: string | null;
|
|
170
|
+
body: string | null;
|
|
171
|
+
addLabels: string | null;
|
|
172
|
+
removeLabels: string | null;
|
|
173
|
+
addAssignee: string | null;
|
|
174
|
+
removeAssignee: string | null;
|
|
175
|
+
}) {
|
|
176
|
+
const gh = yield* GitHubService;
|
|
177
|
+
|
|
178
|
+
const args = ["issue", "edit", String(opts.issue)];
|
|
179
|
+
|
|
180
|
+
if (opts.title !== null) {
|
|
181
|
+
args.push("--title", opts.title);
|
|
182
|
+
}
|
|
183
|
+
if (opts.body !== null) {
|
|
184
|
+
args.push("--body", opts.body);
|
|
185
|
+
}
|
|
186
|
+
if (opts.addLabels !== null) {
|
|
187
|
+
args.push("--add-label", opts.addLabels);
|
|
188
|
+
}
|
|
189
|
+
if (opts.removeLabels !== null) {
|
|
190
|
+
args.push("--remove-label", opts.removeLabels);
|
|
191
|
+
}
|
|
192
|
+
if (opts.addAssignee !== null) {
|
|
193
|
+
args.push("--add-assignee", opts.addAssignee);
|
|
194
|
+
}
|
|
195
|
+
if (opts.removeAssignee !== null) {
|
|
196
|
+
args.push("--remove-assignee", opts.removeAssignee);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
yield* gh.runGh(args);
|
|
200
|
+
|
|
201
|
+
return yield* gh.runGhJson<IssueInfo>([
|
|
202
|
+
"issue",
|
|
203
|
+
"view",
|
|
204
|
+
String(opts.issue),
|
|
205
|
+
"--json",
|
|
206
|
+
"number,title,state,url,labels,assignees,author,createdAt,closedAt",
|
|
207
|
+
]);
|
|
208
|
+
});
|