@blogic-cz/agent-tools 0.15.4 → 0.15.6
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 +1 -1
- package/src/gh-tool/index.ts +3 -1
- package/src/gh-tool/issue/commands.ts +57 -1
- package/src/gh-tool/issue/core.ts +34 -0
- package/src/gh-tool/issue/index.ts +1 -0
- package/src/k8s-tool/index.ts +43 -14
- package/src/k8s-tool/profile.ts +22 -0
package/package.json
CHANGED
package/src/gh-tool/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
issueCloseCommand,
|
|
11
11
|
issueCommentCommand,
|
|
12
12
|
issueCommentsCommand,
|
|
13
|
+
issueCreateCommand,
|
|
13
14
|
issueEditCommand,
|
|
14
15
|
issueListCommand,
|
|
15
16
|
issueReopenCommand,
|
|
@@ -109,7 +110,7 @@ const prCommand = Command.make("pr", {}).pipe(
|
|
|
109
110
|
|
|
110
111
|
const issueCommand = Command.make("issue", {}).pipe(
|
|
111
112
|
Command.withDescription(
|
|
112
|
-
"Issue operations (list, view, comments, triage, snapshot-batch, close, reopen, comment, edit)",
|
|
113
|
+
"Issue operations (list, view, comments, triage, snapshot-batch, create, close, reopen, comment, edit)",
|
|
113
114
|
),
|
|
114
115
|
Command.withSubcommands([
|
|
115
116
|
issueListCommand,
|
|
@@ -117,6 +118,7 @@ const issueCommand = Command.make("issue", {}).pipe(
|
|
|
117
118
|
issueCommentsCommand,
|
|
118
119
|
issueTriageCommand,
|
|
119
120
|
issueSnapshotBatchCommand,
|
|
121
|
+
issueCreateCommand,
|
|
120
122
|
issueCloseCommand,
|
|
121
123
|
issueReopenCommand,
|
|
122
124
|
issueCommentCommand,
|
|
@@ -2,11 +2,16 @@ 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 {
|
|
5
|
+
import {
|
|
6
|
+
resolveDefaultTextInput,
|
|
7
|
+
resolveOptionalTextInput,
|
|
8
|
+
resolveRequiredTextInput,
|
|
9
|
+
} from "#gh/text-input";
|
|
6
10
|
|
|
7
11
|
import {
|
|
8
12
|
closeIssue,
|
|
9
13
|
commentOnIssue,
|
|
14
|
+
createIssue,
|
|
10
15
|
editIssue,
|
|
11
16
|
fetchIssueComments,
|
|
12
17
|
listIssues,
|
|
@@ -192,6 +197,57 @@ export const issueReopenCommand = Command.make(
|
|
|
192
197
|
),
|
|
193
198
|
).pipe(Command.withDescription("Reopen a closed issue"));
|
|
194
199
|
|
|
200
|
+
export const issueCreateCommand = Command.make(
|
|
201
|
+
"create",
|
|
202
|
+
{
|
|
203
|
+
assignee: Flag.string("assignee").pipe(
|
|
204
|
+
Flag.withDescription("Assignee login (comma-separated for multiple)"),
|
|
205
|
+
Flag.optional,
|
|
206
|
+
),
|
|
207
|
+
body: Flag.string("body").pipe(Flag.withDescription("Issue body"), Flag.optional),
|
|
208
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
209
|
+
Flag.withDescription("Read issue body from a file path or '-' for stdin"),
|
|
210
|
+
Flag.optional,
|
|
211
|
+
),
|
|
212
|
+
bodyStdin: Flag.boolean("body-stdin").pipe(
|
|
213
|
+
Flag.withDescription("Read issue body from stdin"),
|
|
214
|
+
Flag.withDefault(false),
|
|
215
|
+
),
|
|
216
|
+
format: formatOption,
|
|
217
|
+
labels: Flag.string("labels").pipe(
|
|
218
|
+
Flag.withDescription("Labels to apply (comma-separated)"),
|
|
219
|
+
Flag.optional,
|
|
220
|
+
),
|
|
221
|
+
repo: repoOption,
|
|
222
|
+
title: Flag.string("title").pipe(Flag.withDescription("Issue title")),
|
|
223
|
+
},
|
|
224
|
+
({ assignee, body, bodyFile, bodyStdin, format, labels, repo, title }) =>
|
|
225
|
+
withRepo(
|
|
226
|
+
repo,
|
|
227
|
+
Effect.gen(function* () {
|
|
228
|
+
const resolvedBody = yield* resolveDefaultTextInput({
|
|
229
|
+
command: "gh-tool issue create",
|
|
230
|
+
value: Option.getOrNull(body),
|
|
231
|
+
fileValue: Option.getOrNull(bodyFile),
|
|
232
|
+
stdin: bodyStdin,
|
|
233
|
+
valueFlag: "--body",
|
|
234
|
+
fileFlag: "--body-file",
|
|
235
|
+
stdinFlag: "--body-stdin",
|
|
236
|
+
label: "body",
|
|
237
|
+
defaultValue: "",
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const result = yield* createIssue({
|
|
241
|
+
assignee: Option.getOrNull(assignee),
|
|
242
|
+
body: resolvedBody,
|
|
243
|
+
labels: Option.getOrNull(labels),
|
|
244
|
+
title,
|
|
245
|
+
});
|
|
246
|
+
yield* logFormatted(result, format);
|
|
247
|
+
}),
|
|
248
|
+
),
|
|
249
|
+
).pipe(Command.withDescription("Create a new issue"));
|
|
250
|
+
|
|
195
251
|
export const issueCommentCommand = Command.make(
|
|
196
252
|
"comment",
|
|
197
253
|
{
|
|
@@ -257,6 +257,40 @@ export const commentOnIssue = Effect.fn("issue.commentOnIssue")(function* (opts:
|
|
|
257
257
|
};
|
|
258
258
|
});
|
|
259
259
|
|
|
260
|
+
export const createIssue = Effect.fn("issue.createIssue")(function* (opts: {
|
|
261
|
+
title: string;
|
|
262
|
+
body: string;
|
|
263
|
+
assignee: string | null;
|
|
264
|
+
labels: string | null;
|
|
265
|
+
}) {
|
|
266
|
+
const gh = yield* GitHubService;
|
|
267
|
+
|
|
268
|
+
const args = ["issue", "create", "--title", opts.title, "--body", opts.body];
|
|
269
|
+
|
|
270
|
+
if (opts.assignee !== null) {
|
|
271
|
+
args.push("--assignee", opts.assignee);
|
|
272
|
+
}
|
|
273
|
+
if (opts.labels !== null) {
|
|
274
|
+
args.push("--label", opts.labels);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const result = yield* gh.runGh(args);
|
|
278
|
+
|
|
279
|
+
const urlMatch = result.stdout.match(/\/issues\/(\d+)/);
|
|
280
|
+
if (!urlMatch?.[1]) {
|
|
281
|
+
return yield* Effect.fail(
|
|
282
|
+
new GitHubCommandError({
|
|
283
|
+
command: "gh issue create",
|
|
284
|
+
exitCode: 0,
|
|
285
|
+
stderr: "Issue was created but its number could not be parsed from the output.",
|
|
286
|
+
message: "Issue was created but its number could not be parsed from the output.",
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return yield* viewIssue(Number(urlMatch[1]));
|
|
292
|
+
});
|
|
293
|
+
|
|
260
294
|
export const editIssue = Effect.fn("issue.editIssue")(function* (opts: {
|
|
261
295
|
issue: number;
|
|
262
296
|
title: string | null;
|
package/src/k8s-tool/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { K8sService, K8sServiceLayer } from "./service";
|
|
|
17
17
|
import { ConfigService, ConfigServiceLayer, getDefaultEnvironment, getToolConfig } from "#config";
|
|
18
18
|
import type { K8sConfig } from "#config";
|
|
19
19
|
import { K8sContextError } from "./errors";
|
|
20
|
+
import { hasKubernetesConfig, selectK8sProfile } from "./profile";
|
|
20
21
|
import {
|
|
21
22
|
transformDescribe,
|
|
22
23
|
transformGenericKubectl,
|
|
@@ -60,6 +61,21 @@ const resolveEnv = (
|
|
|
60
61
|
});
|
|
61
62
|
});
|
|
62
63
|
|
|
64
|
+
const resolveEnvAndProfile = (
|
|
65
|
+
envOption: Option.Option<string>,
|
|
66
|
+
profileOption: Option.Option<string>,
|
|
67
|
+
config: Parameters<typeof getDefaultEnvironment>[0],
|
|
68
|
+
) =>
|
|
69
|
+
Effect.gen(function* () {
|
|
70
|
+
const resolvedEnv = yield* resolveEnv(envOption, config);
|
|
71
|
+
const profileName = selectK8sProfile(
|
|
72
|
+
config?.kubernetes,
|
|
73
|
+
Option.getOrUndefined(profileOption),
|
|
74
|
+
resolvedEnv,
|
|
75
|
+
);
|
|
76
|
+
return { env: resolvedEnv, profile: profileName };
|
|
77
|
+
});
|
|
78
|
+
|
|
63
79
|
type CommonK8sCommandOptions = {
|
|
64
80
|
readonly env: Option.Option<string>;
|
|
65
81
|
readonly dryRun: boolean;
|
|
@@ -67,26 +83,36 @@ type CommonK8sCommandOptions = {
|
|
|
67
83
|
readonly profile: Option.Option<string>;
|
|
68
84
|
};
|
|
69
85
|
|
|
86
|
+
const noKubernetesConfigResult = (): CommandResult => ({
|
|
87
|
+
success: false,
|
|
88
|
+
error: "No Kubernetes configuration found",
|
|
89
|
+
hint: "Add a 'kubernetes' section to agent-tools.json5 with clusterId and namespaces.",
|
|
90
|
+
nextCommand:
|
|
91
|
+
"echo '{ kubernetes: { default: { clusterId: \"my-cluster\" } } }' > agent-tools.json5",
|
|
92
|
+
executionTimeMs: 0,
|
|
93
|
+
});
|
|
94
|
+
|
|
70
95
|
const executeK8sCommand = (command: string, options: CommonK8sCommandOptions) =>
|
|
71
96
|
Effect.gen(function* () {
|
|
72
97
|
const config = yield* ConfigService;
|
|
73
|
-
|
|
98
|
+
|
|
99
|
+
// Check for a missing 'kubernetes' section before resolving --env, so that case still
|
|
100
|
+
// surfaces "add a kubernetes section" instead of an env-resolution error (e.g. prod-safety).
|
|
101
|
+
if (!hasKubernetesConfig(config?.kubernetes)) {
|
|
102
|
+
return noKubernetesConfigResult();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { env: resolvedEnv, profile: profileName } = yield* resolveEnvAndProfile(
|
|
106
|
+
options.env,
|
|
107
|
+
options.profile,
|
|
108
|
+
config,
|
|
109
|
+
);
|
|
74
110
|
const k8sConfig = getToolConfig<K8sConfig>(config, "kubernetes", profileName);
|
|
75
111
|
|
|
76
112
|
if (!k8sConfig) {
|
|
77
|
-
|
|
78
|
-
success: false,
|
|
79
|
-
error: "No Kubernetes configuration found",
|
|
80
|
-
hint: "Add a 'kubernetes' section to agent-tools.json5 with clusterId and namespaces.",
|
|
81
|
-
nextCommand:
|
|
82
|
-
"echo '{ kubernetes: { default: { clusterId: \"my-cluster\" } } }' > agent-tools.json5",
|
|
83
|
-
executionTimeMs: 0,
|
|
84
|
-
};
|
|
85
|
-
return result;
|
|
113
|
+
return noKubernetesConfigResult();
|
|
86
114
|
}
|
|
87
115
|
|
|
88
|
-
const resolvedEnv = yield* resolveEnv(options.env, config);
|
|
89
|
-
|
|
90
116
|
const k8sService = yield* K8sService;
|
|
91
117
|
const result = yield* k8sService.runKubectl(command, options.dryRun, profileName).pipe(
|
|
92
118
|
Effect.catchTags({
|
|
@@ -190,8 +216,11 @@ const resolveStructuredNamespace = (
|
|
|
190
216
|
if (explicitNamespace) return explicitNamespace;
|
|
191
217
|
|
|
192
218
|
const config = yield* ConfigService;
|
|
193
|
-
const resolvedEnv = yield*
|
|
194
|
-
|
|
219
|
+
const { env: resolvedEnv, profile: profileName } = yield* resolveEnvAndProfile(
|
|
220
|
+
envOption,
|
|
221
|
+
profileOption,
|
|
222
|
+
config,
|
|
223
|
+
);
|
|
195
224
|
const k8sConfig = getToolConfig<K8sConfig>(config, "kubernetes", profileName);
|
|
196
225
|
|
|
197
226
|
if (!k8sConfig) return undefined;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// --profile wins if given; else a kubernetes.<env> section (different cluster per env) wins; else undefined (getToolConfig default).
|
|
2
|
+
export function selectK8sProfile(
|
|
3
|
+
kubernetesSection: Record<string, unknown> | undefined,
|
|
4
|
+
explicitProfile: string | undefined,
|
|
5
|
+
resolvedEnv: string,
|
|
6
|
+
): string | undefined {
|
|
7
|
+
if (explicitProfile) {
|
|
8
|
+
return explicitProfile;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (kubernetesSection && Object.hasOwn(kubernetesSection, resolvedEnv)) {
|
|
12
|
+
return resolvedEnv;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function hasKubernetesConfig(
|
|
19
|
+
kubernetesSection: Record<string, unknown> | undefined,
|
|
20
|
+
): boolean {
|
|
21
|
+
return !!kubernetesSection && Object.keys(kubernetesSection).length > 0;
|
|
22
|
+
}
|