@blogic-cz/agent-tools 0.1.0 → 0.2.1
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 +68 -30
- package/package.json +7 -4
- package/schemas/agent-tools.schema.json +4 -0
- package/src/az-tool/build.ts +5 -0
- package/src/az-tool/errors.ts +12 -0
- package/src/az-tool/index.ts +129 -105
- package/src/az-tool/service.ts +13 -4
- package/src/config/index.ts +7 -1
- package/src/config/loader.ts +8 -0
- package/src/config/types.ts +2 -0
- package/src/credential-guard/index.ts +2 -1
- package/src/db-tool/config-service.ts +2 -2
- package/src/db-tool/errors.ts +15 -0
- package/src/db-tool/index.ts +47 -8
- package/src/db-tool/types.ts +1 -1
- package/src/gh-tool/errors.ts +15 -0
- package/src/gh-tool/index.ts +5 -1
- package/src/gh-tool/issue.ts +1 -1
- package/src/gh-tool/pr/commands.ts +58 -3
- package/src/gh-tool/pr/core.ts +28 -7
- package/src/gh-tool/pr/helpers.ts +1 -1
- package/src/gh-tool/pr/index.ts +2 -0
- package/src/gh-tool/pr/review.ts +10 -6
- package/src/gh-tool/repo.ts +1 -1
- package/src/gh-tool/service.ts +5 -0
- package/src/gh-tool/workflow.ts +5 -1
- package/src/k8s-tool/errors.ts +9 -0
- package/src/k8s-tool/index.ts +318 -66
- package/src/k8s-tool/service.ts +2 -2
- package/src/k8s-tool/types.ts +4 -0
- package/src/logs-tool/errors.ts +12 -0
- package/src/logs-tool/index.ts +73 -11
- package/src/logs-tool/service.ts +4 -4
- package/src/logs-tool/types.ts +4 -1
- package/src/session-tool/config.ts +1 -1
- package/src/session-tool/index.ts +1 -1
- package/src/session-tool/service.ts +16 -3
- package/src/session-tool/types.ts +1 -1
- package/src/shared/bun.ts +1 -1
- package/src/shared/error-renderer.ts +21 -11
- package/src/shared/index.ts +1 -0
- package/src/shared/types.ts +3 -0
package/src/k8s-tool/index.ts
CHANGED
|
@@ -5,80 +5,172 @@ import { Console, Effect, Layer, Option } from "effect";
|
|
|
5
5
|
|
|
6
6
|
import type { CommandResult } from "./types";
|
|
7
7
|
|
|
8
|
-
import { formatOption, formatOutput, renderCauseToStderr, VERSION } from "
|
|
8
|
+
import { formatOption, formatOutput, renderCauseToStderr, VERSION } from "#src/shared";
|
|
9
9
|
import { K8sService, K8sServiceLayer } from "./service";
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
ConfigService,
|
|
12
|
+
ConfigServiceLayer,
|
|
13
|
+
getDefaultEnvironment,
|
|
14
|
+
getToolConfig,
|
|
15
|
+
} from "#src/config";
|
|
16
|
+
import type { K8sConfig } from "#src/config";
|
|
17
|
+
import { K8sContextError } from "./errors";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve environment from explicit --env flag, config defaultEnvironment, or fail with hint.
|
|
21
|
+
* Rejects implicit prod: if defaultEnvironment is "prod" and --env was not passed, fail explicitly.
|
|
22
|
+
*/
|
|
23
|
+
const resolveEnv = (
|
|
24
|
+
envOption: Option.Option<string>,
|
|
25
|
+
config: Parameters<typeof getDefaultEnvironment>[0],
|
|
26
|
+
) =>
|
|
27
|
+
Effect.gen(function* () {
|
|
28
|
+
const explicit = Option.getOrUndefined(envOption);
|
|
29
|
+
if (explicit) return explicit;
|
|
30
|
+
|
|
31
|
+
const defaultEnv = getDefaultEnvironment(config);
|
|
32
|
+
|
|
33
|
+
if (defaultEnv === "prod") {
|
|
34
|
+
return yield* new K8sContextError({
|
|
35
|
+
message:
|
|
36
|
+
"Implicit prod access blocked. Config defaultEnvironment is 'prod' but --env was not passed explicitly.",
|
|
37
|
+
clusterId: "(prod-safety)",
|
|
38
|
+
hint: "Pass --env prod explicitly to confirm production access, or change defaultEnvironment to a non-prod value.",
|
|
39
|
+
nextCommand: 'agent-tools-k8s kubectl --env prod --cmd "get pods -n <namespace>"',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (defaultEnv) return defaultEnv;
|
|
44
|
+
|
|
45
|
+
return yield* new K8sContextError({
|
|
46
|
+
message:
|
|
47
|
+
"No environment specified. Use --env <name> or set defaultEnvironment in agent-tools.json5.",
|
|
48
|
+
clusterId: "(not specified)",
|
|
49
|
+
hint: 'Set defaultEnvironment in agent-tools.json5 (e.g. defaultEnvironment: "test") or pass --env explicitly.',
|
|
50
|
+
nextCommand: 'agent-tools-k8s kubectl --env test --cmd "get pods -n <namespace>"',
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
type CommonK8sCommandOptions = {
|
|
55
|
+
readonly env: Option.Option<string>;
|
|
56
|
+
readonly dryRun: boolean;
|
|
57
|
+
readonly format: "toon" | "json";
|
|
58
|
+
readonly profile: Option.Option<string>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const runK8sCommand = (command: string, options: CommonK8sCommandOptions) =>
|
|
62
|
+
Effect.gen(function* () {
|
|
63
|
+
const config = yield* ConfigService;
|
|
64
|
+
const profileName = Option.getOrUndefined(options.profile);
|
|
65
|
+
const k8sConfig = getToolConfig<K8sConfig>(config, "kubernetes", profileName);
|
|
66
|
+
|
|
67
|
+
if (!k8sConfig) {
|
|
68
|
+
const result: CommandResult = {
|
|
69
|
+
success: false,
|
|
70
|
+
error: "No Kubernetes configuration found",
|
|
71
|
+
hint: "Add a 'kubernetes' section to agent-tools.json5 with clusterId and namespaces.",
|
|
72
|
+
nextCommand:
|
|
73
|
+
"echo '{ kubernetes: { default: { clusterId: \"my-cluster\" } } }' > agent-tools.json5",
|
|
74
|
+
executionTimeMs: 0,
|
|
75
|
+
};
|
|
76
|
+
yield* Console.log(formatOutput(result, options.format));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const resolvedEnv = yield* resolveEnv(options.env, config);
|
|
81
|
+
|
|
82
|
+
const k8sService = yield* K8sService;
|
|
83
|
+
const result = yield* k8sService.runKubectl(command, options.dryRun).pipe(
|
|
84
|
+
Effect.catchTags({
|
|
85
|
+
K8sContextError: (error) => {
|
|
86
|
+
const errorResult: CommandResult = {
|
|
87
|
+
success: false,
|
|
88
|
+
error: error.message,
|
|
89
|
+
hint: `Verify cluster ID "${k8sConfig.clusterId}" matches a context in kubectl config. Run: kubectl config get-contexts`,
|
|
90
|
+
nextCommand: "kubectl config get-contexts",
|
|
91
|
+
executionTimeMs: 0,
|
|
92
|
+
};
|
|
93
|
+
return Effect.succeed(errorResult);
|
|
94
|
+
},
|
|
95
|
+
K8sCommandError: (error) => {
|
|
96
|
+
const errorResult: CommandResult = {
|
|
97
|
+
success: false,
|
|
98
|
+
error: error.message,
|
|
99
|
+
command: error.command,
|
|
100
|
+
hint:
|
|
101
|
+
error.hint ?? "Check command syntax and ensure the target namespace/resource exists.",
|
|
102
|
+
executionTimeMs: 0,
|
|
103
|
+
};
|
|
104
|
+
return Effect.succeed(errorResult);
|
|
105
|
+
},
|
|
106
|
+
K8sTimeoutError: (error) => {
|
|
107
|
+
const errorResult: CommandResult = {
|
|
108
|
+
success: false,
|
|
109
|
+
error: error.message,
|
|
110
|
+
command: error.command,
|
|
111
|
+
hint:
|
|
112
|
+
error.hint ??
|
|
113
|
+
`Command timed out after ${error.timeoutMs}ms. Consider increasing timeoutMs in config or narrowing the query.`,
|
|
114
|
+
executionTimeMs: error.timeoutMs,
|
|
115
|
+
};
|
|
116
|
+
return Effect.succeed(errorResult);
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
yield* Console.log(formatOutput({ ...result, environment: resolvedEnv }, options.format));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const commonFlags = {
|
|
125
|
+
env: Flag.optional(Flag.string("env")).pipe(
|
|
126
|
+
Flag.withDescription(
|
|
127
|
+
"Target environment (e.g. test, prod). Falls back to defaultEnvironment in config.",
|
|
128
|
+
),
|
|
129
|
+
),
|
|
130
|
+
dryRun: Flag.boolean("dry-run").pipe(
|
|
131
|
+
Flag.withAlias("d"),
|
|
132
|
+
Flag.withDescription("Show command without executing"),
|
|
133
|
+
Flag.withDefault(false),
|
|
134
|
+
),
|
|
135
|
+
format: formatOption,
|
|
136
|
+
profile: Flag.optional(Flag.string("profile")).pipe(
|
|
137
|
+
Flag.withDescription("Kubernetes profile name (if multiple configured)"),
|
|
138
|
+
),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const buildKubectlCommand = (base: string, args: ReadonlyArray<string>) => {
|
|
142
|
+
const extras = args.filter((part) => part.length > 0);
|
|
143
|
+
return extras.length === 0 ? base : `${base} ${extras.join(" ")}`;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const resolveStructuredNamespace = (
|
|
147
|
+
namespaceOption: Option.Option<string>,
|
|
148
|
+
envOption: Option.Option<string>,
|
|
149
|
+
profileOption: Option.Option<string>,
|
|
150
|
+
) =>
|
|
151
|
+
Effect.gen(function* () {
|
|
152
|
+
const explicitNamespace = Option.getOrUndefined(namespaceOption);
|
|
153
|
+
if (explicitNamespace) return explicitNamespace;
|
|
154
|
+
|
|
155
|
+
const config = yield* ConfigService;
|
|
156
|
+
const resolvedEnv = yield* resolveEnv(envOption, config);
|
|
157
|
+
const profileName = Option.getOrUndefined(profileOption);
|
|
158
|
+
const k8sConfig = getToolConfig<K8sConfig>(config, "kubernetes", profileName);
|
|
159
|
+
|
|
160
|
+
if (!k8sConfig) return undefined;
|
|
161
|
+
|
|
162
|
+
return k8sConfig.namespaces[resolvedEnv];
|
|
163
|
+
});
|
|
12
164
|
|
|
13
165
|
const kubectlCommand = Command.make(
|
|
14
166
|
"kubectl",
|
|
15
167
|
{
|
|
16
|
-
|
|
17
|
-
Flag.withDescription("Target environment: test or prod"),
|
|
18
|
-
),
|
|
168
|
+
...commonFlags,
|
|
19
169
|
cmd: Flag.string("cmd").pipe(
|
|
20
170
|
Flag.withDescription('kubectl command (without "kubectl" prefix)'),
|
|
21
171
|
),
|
|
22
|
-
dryRun: Flag.boolean("dry-run").pipe(
|
|
23
|
-
Flag.withAlias("d"),
|
|
24
|
-
Flag.withDescription("Show command without executing"),
|
|
25
|
-
Flag.withDefault(false),
|
|
26
|
-
),
|
|
27
|
-
format: formatOption,
|
|
28
|
-
profile: Flag.optional(Flag.string("profile")).pipe(
|
|
29
|
-
Flag.withDescription("Kubernetes profile name (if multiple configured)"),
|
|
30
|
-
),
|
|
31
172
|
},
|
|
32
|
-
({ cmd, dryRun, format, profile }) =>
|
|
33
|
-
Effect.gen(function* () {
|
|
34
|
-
const config = yield* ConfigService;
|
|
35
|
-
const profileName = profile ? Option.getOrUndefined(profile) : undefined;
|
|
36
|
-
const k8sConfig = getToolConfig<K8sConfig>(config, "kubernetes", profileName);
|
|
37
|
-
|
|
38
|
-
if (!k8sConfig) {
|
|
39
|
-
const result: CommandResult = {
|
|
40
|
-
success: false,
|
|
41
|
-
error: "No Kubernetes configuration found",
|
|
42
|
-
executionTimeMs: 0,
|
|
43
|
-
};
|
|
44
|
-
yield* Console.log(formatOutput(result, format));
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const k8sService = yield* K8sService;
|
|
49
|
-
const result = yield* k8sService.runKubectl(cmd, dryRun).pipe(
|
|
50
|
-
Effect.catchTags({
|
|
51
|
-
K8sContextError: (error) => {
|
|
52
|
-
const result: CommandResult = {
|
|
53
|
-
success: false,
|
|
54
|
-
error: error.message,
|
|
55
|
-
executionTimeMs: 0,
|
|
56
|
-
};
|
|
57
|
-
return Effect.succeed(result);
|
|
58
|
-
},
|
|
59
|
-
K8sCommandError: (error) => {
|
|
60
|
-
const result: CommandResult = {
|
|
61
|
-
success: false,
|
|
62
|
-
error: error.message,
|
|
63
|
-
command: error.command,
|
|
64
|
-
executionTimeMs: 0,
|
|
65
|
-
};
|
|
66
|
-
return Effect.succeed(result);
|
|
67
|
-
},
|
|
68
|
-
K8sTimeoutError: (error) => {
|
|
69
|
-
const result: CommandResult = {
|
|
70
|
-
success: false,
|
|
71
|
-
error: error.message,
|
|
72
|
-
command: error.command,
|
|
73
|
-
executionTimeMs: error.timeoutMs,
|
|
74
|
-
};
|
|
75
|
-
return Effect.succeed(result);
|
|
76
|
-
},
|
|
77
|
-
}),
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
yield* Console.log(formatOutput(result, format));
|
|
81
|
-
}),
|
|
173
|
+
({ cmd, dryRun, env, format, profile }) => runK8sCommand(cmd, { dryRun, env, format, profile }),
|
|
82
174
|
).pipe(
|
|
83
175
|
Command.withDescription(
|
|
84
176
|
`Kubernetes CLI Tool for Coding Agents
|
|
@@ -130,9 +222,169 @@ RELATED TOOLS:
|
|
|
130
222
|
),
|
|
131
223
|
);
|
|
132
224
|
|
|
225
|
+
const podsCommand = Command.make(
|
|
226
|
+
"pods",
|
|
227
|
+
{
|
|
228
|
+
...commonFlags,
|
|
229
|
+
namespace: Flag.string("namespace").pipe(
|
|
230
|
+
Flag.withDescription("Namespace to query"),
|
|
231
|
+
Flag.optional,
|
|
232
|
+
),
|
|
233
|
+
label: Flag.string("label").pipe(
|
|
234
|
+
Flag.withDescription("Label selector (key=value)"),
|
|
235
|
+
Flag.optional,
|
|
236
|
+
),
|
|
237
|
+
wide: Flag.boolean("wide").pipe(
|
|
238
|
+
Flag.withDescription("Show additional pod information"),
|
|
239
|
+
Flag.withDefault(false),
|
|
240
|
+
),
|
|
241
|
+
},
|
|
242
|
+
({ dryRun, env, format, label, namespace, profile, wide }) =>
|
|
243
|
+
Effect.gen(function* () {
|
|
244
|
+
const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
|
|
245
|
+
const command = buildKubectlCommand("get pods", [
|
|
246
|
+
Option.match(label, {
|
|
247
|
+
onNone: () => "",
|
|
248
|
+
onSome: (value) => `-l ${value}`,
|
|
249
|
+
}),
|
|
250
|
+
resolvedNamespace ? `-n ${resolvedNamespace}` : "",
|
|
251
|
+
wide ? "-o wide" : "",
|
|
252
|
+
]);
|
|
253
|
+
return yield* runK8sCommand(command, { dryRun, env, format, profile });
|
|
254
|
+
}),
|
|
255
|
+
).pipe(Command.withDescription("List pods (get pods) with optional namespace/label/wide output"));
|
|
256
|
+
|
|
257
|
+
const logsCommand = Command.make(
|
|
258
|
+
"logs",
|
|
259
|
+
{
|
|
260
|
+
...commonFlags,
|
|
261
|
+
pod: Flag.string("pod").pipe(Flag.withDescription("Pod name")),
|
|
262
|
+
namespace: Flag.string("namespace").pipe(
|
|
263
|
+
Flag.withDescription("Namespace containing the pod"),
|
|
264
|
+
Flag.optional,
|
|
265
|
+
),
|
|
266
|
+
container: Flag.string("container").pipe(
|
|
267
|
+
Flag.withDescription("Container name (for multi-container pods)"),
|
|
268
|
+
Flag.optional,
|
|
269
|
+
),
|
|
270
|
+
tail: Flag.integer("tail").pipe(Flag.withDescription("Show last N log lines"), Flag.optional),
|
|
271
|
+
follow: Flag.boolean("follow").pipe(
|
|
272
|
+
Flag.withAlias("f"),
|
|
273
|
+
Flag.withDescription("Stream logs in real time"),
|
|
274
|
+
Flag.withDefault(false),
|
|
275
|
+
),
|
|
276
|
+
},
|
|
277
|
+
({ container, dryRun, env, follow, format, namespace, pod, profile, tail }) =>
|
|
278
|
+
Effect.gen(function* () {
|
|
279
|
+
const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
|
|
280
|
+
const command = buildKubectlCommand(`logs ${pod}`, [
|
|
281
|
+
resolvedNamespace ? `-n ${resolvedNamespace}` : "",
|
|
282
|
+
Option.match(container, {
|
|
283
|
+
onNone: () => "",
|
|
284
|
+
onSome: (value) => `-c ${value}`,
|
|
285
|
+
}),
|
|
286
|
+
Option.match(tail, {
|
|
287
|
+
onNone: () => "",
|
|
288
|
+
onSome: (value) => `--tail=${value}`,
|
|
289
|
+
}),
|
|
290
|
+
follow ? "-f" : "",
|
|
291
|
+
]);
|
|
292
|
+
return yield* runK8sCommand(command, { dryRun, env, format, profile });
|
|
293
|
+
}),
|
|
294
|
+
).pipe(Command.withDescription("Fetch pod logs with tail/follow/container selectors"));
|
|
295
|
+
|
|
296
|
+
const describeCommand = Command.make(
|
|
297
|
+
"describe",
|
|
298
|
+
{
|
|
299
|
+
...commonFlags,
|
|
300
|
+
resource: Flag.string("resource").pipe(
|
|
301
|
+
Flag.withDescription("Resource type (pod, deploy, svc, etc.)"),
|
|
302
|
+
),
|
|
303
|
+
name: Flag.string("name").pipe(Flag.withDescription("Resource name")),
|
|
304
|
+
namespace: Flag.string("namespace").pipe(
|
|
305
|
+
Flag.withDescription("Namespace containing the resource"),
|
|
306
|
+
Flag.optional,
|
|
307
|
+
),
|
|
308
|
+
},
|
|
309
|
+
({ dryRun, env, format, name, namespace, profile, resource }) =>
|
|
310
|
+
Effect.gen(function* () {
|
|
311
|
+
const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
|
|
312
|
+
const command = buildKubectlCommand(`describe ${resource} ${name}`, [
|
|
313
|
+
resolvedNamespace ? `-n ${resolvedNamespace}` : "",
|
|
314
|
+
]);
|
|
315
|
+
return yield* runK8sCommand(command, { dryRun, env, format, profile });
|
|
316
|
+
}),
|
|
317
|
+
).pipe(Command.withDescription("Describe a Kubernetes resource by type and name"));
|
|
318
|
+
|
|
319
|
+
const execCommand = Command.make(
|
|
320
|
+
"exec",
|
|
321
|
+
{
|
|
322
|
+
...commonFlags,
|
|
323
|
+
pod: Flag.string("pod").pipe(Flag.withDescription("Pod name")),
|
|
324
|
+
execCmd: Flag.string("exec-cmd").pipe(
|
|
325
|
+
Flag.withDescription("Command to run inside the pod; wrap in quotes for spaces"),
|
|
326
|
+
),
|
|
327
|
+
namespace: Flag.string("namespace").pipe(
|
|
328
|
+
Flag.withDescription("Namespace containing the pod"),
|
|
329
|
+
Flag.optional,
|
|
330
|
+
),
|
|
331
|
+
container: Flag.string("container").pipe(
|
|
332
|
+
Flag.withDescription("Container name (for multi-container pods)"),
|
|
333
|
+
Flag.optional,
|
|
334
|
+
),
|
|
335
|
+
},
|
|
336
|
+
({ container, dryRun, env, execCmd, format, namespace, pod, profile }) =>
|
|
337
|
+
Effect.gen(function* () {
|
|
338
|
+
const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
|
|
339
|
+
const command = buildKubectlCommand(`exec ${pod}`, [
|
|
340
|
+
resolvedNamespace ? `-n ${resolvedNamespace}` : "",
|
|
341
|
+
Option.match(container, {
|
|
342
|
+
onNone: () => "",
|
|
343
|
+
onSome: (value) => `-c ${value}`,
|
|
344
|
+
}),
|
|
345
|
+
`-- ${execCmd}`,
|
|
346
|
+
]);
|
|
347
|
+
return yield* runK8sCommand(command, { dryRun, env, format, profile });
|
|
348
|
+
}),
|
|
349
|
+
).pipe(Command.withDescription("Execute a command in a pod (kubectl exec <pod> -- <cmd>)"));
|
|
350
|
+
|
|
351
|
+
const topCommand = Command.make(
|
|
352
|
+
"top",
|
|
353
|
+
{
|
|
354
|
+
...commonFlags,
|
|
355
|
+
namespace: Flag.string("namespace").pipe(
|
|
356
|
+
Flag.withDescription("Namespace to inspect"),
|
|
357
|
+
Flag.optional,
|
|
358
|
+
),
|
|
359
|
+
sortBy: Flag.choice("sort-by", ["cpu", "memory"] as const).pipe(
|
|
360
|
+
Flag.withDescription("Sort metrics output when supported by kubectl"),
|
|
361
|
+
Flag.optional,
|
|
362
|
+
),
|
|
363
|
+
},
|
|
364
|
+
({ dryRun, env, format, namespace, profile, sortBy }) =>
|
|
365
|
+
Effect.gen(function* () {
|
|
366
|
+
const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
|
|
367
|
+
const command = buildKubectlCommand("top pod", [
|
|
368
|
+
resolvedNamespace ? `-n ${resolvedNamespace}` : "",
|
|
369
|
+
Option.match(sortBy, {
|
|
370
|
+
onNone: () => "",
|
|
371
|
+
onSome: (value) => `--sort-by=${value}`,
|
|
372
|
+
}),
|
|
373
|
+
]);
|
|
374
|
+
return yield* runK8sCommand(command, { dryRun, env, format, profile });
|
|
375
|
+
}),
|
|
376
|
+
).pipe(Command.withDescription("Show pod CPU/memory usage (kubectl top pod)"));
|
|
377
|
+
|
|
133
378
|
const mainCommand = Command.make("k8s-tool", {}).pipe(
|
|
134
379
|
Command.withDescription("Kubernetes CLI Tool for Coding Agents"),
|
|
135
|
-
Command.withSubcommands([
|
|
380
|
+
Command.withSubcommands([
|
|
381
|
+
kubectlCommand,
|
|
382
|
+
podsCommand,
|
|
383
|
+
logsCommand,
|
|
384
|
+
describeCommand,
|
|
385
|
+
execCommand,
|
|
386
|
+
topCommand,
|
|
387
|
+
]),
|
|
136
388
|
);
|
|
137
389
|
|
|
138
390
|
const cli = Command.run(mainCommand, {
|
package/src/k8s-tool/service.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { Effect, Layer, Option, Ref, ServiceMap, Stream } from "effect";
|
|
|
4
4
|
import type { CommandResult, Environment } from "./types";
|
|
5
5
|
|
|
6
6
|
import { K8sCommandError, K8sContextError, K8sTimeoutError } from "./errors";
|
|
7
|
-
import { ConfigService, getToolConfig } from "
|
|
8
|
-
import type { K8sConfig } from "
|
|
7
|
+
import { ConfigService, getToolConfig } from "#src/config";
|
|
8
|
+
import type { K8sConfig } from "#src/config";
|
|
9
9
|
|
|
10
10
|
export class K8sService extends ServiceMap.Service<
|
|
11
11
|
K8sService,
|
package/src/k8s-tool/types.ts
CHANGED
package/src/logs-tool/errors.ts
CHANGED
|
@@ -5,16 +5,25 @@ export class LogsNotFoundError extends Schema.TaggedErrorClass<LogsNotFoundError
|
|
|
5
5
|
{
|
|
6
6
|
message: Schema.String,
|
|
7
7
|
path: Schema.String,
|
|
8
|
+
hint: Schema.optionalKey(Schema.String),
|
|
9
|
+
nextCommand: Schema.optionalKey(Schema.String),
|
|
10
|
+
retryable: Schema.optionalKey(Schema.Boolean),
|
|
8
11
|
},
|
|
9
12
|
) {}
|
|
10
13
|
|
|
11
14
|
export class LogsReadError extends Schema.TaggedErrorClass<LogsReadError>()("LogsReadError", {
|
|
12
15
|
message: Schema.String,
|
|
13
16
|
source: Schema.String,
|
|
17
|
+
hint: Schema.optionalKey(Schema.String),
|
|
18
|
+
nextCommand: Schema.optionalKey(Schema.String),
|
|
19
|
+
retryable: Schema.optionalKey(Schema.Boolean),
|
|
14
20
|
}) {}
|
|
15
21
|
|
|
16
22
|
export class LogsConfigError extends Schema.TaggedErrorClass<LogsConfigError>()("LogsConfigError", {
|
|
17
23
|
message: Schema.String,
|
|
24
|
+
hint: Schema.optionalKey(Schema.String),
|
|
25
|
+
nextCommand: Schema.optionalKey(Schema.String),
|
|
26
|
+
retryable: Schema.optionalKey(Schema.Boolean),
|
|
18
27
|
}) {}
|
|
19
28
|
|
|
20
29
|
export class LogsTimeoutError extends Schema.TaggedErrorClass<LogsTimeoutError>()(
|
|
@@ -23,6 +32,9 @@ export class LogsTimeoutError extends Schema.TaggedErrorClass<LogsTimeoutError>(
|
|
|
23
32
|
message: Schema.String,
|
|
24
33
|
source: Schema.String,
|
|
25
34
|
timeoutMs: Schema.Number,
|
|
35
|
+
hint: Schema.optionalKey(Schema.String),
|
|
36
|
+
nextCommand: Schema.optionalKey(Schema.String),
|
|
37
|
+
retryable: Schema.optionalKey(Schema.Boolean),
|
|
26
38
|
},
|
|
27
39
|
) {}
|
|
28
40
|
|
package/src/logs-tool/index.ts
CHANGED
|
@@ -20,8 +20,9 @@ import { Console, Effect, Layer, Option, Result } from "effect";
|
|
|
20
20
|
|
|
21
21
|
import type { Environment, LogResult, ReadOptions } from "./types";
|
|
22
22
|
|
|
23
|
-
import { formatOption, formatOutput, renderCauseToStderr, VERSION } from "
|
|
24
|
-
import {
|
|
23
|
+
import { formatOption, formatOutput, renderCauseToStderr, VERSION } from "#src/shared";
|
|
24
|
+
import { ConfigService, ConfigServiceLayer, getDefaultEnvironment } from "#src/config";
|
|
25
|
+
import { LogsConfigError, LogsNotFoundError, LogsReadError, LogsTimeoutError } from "./errors";
|
|
25
26
|
import { LogsService, LogsServiceLayer } from "./service";
|
|
26
27
|
|
|
27
28
|
const profileOption = Flag.optional(
|
|
@@ -32,6 +33,36 @@ const profileOption = Flag.optional(
|
|
|
32
33
|
),
|
|
33
34
|
);
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Resolve environment from explicit --env flag, config defaultEnvironment, or fail with hint.
|
|
38
|
+
*/
|
|
39
|
+
const resolveEnv = (envOption: Option.Option<string>) =>
|
|
40
|
+
Effect.gen(function* () {
|
|
41
|
+
const explicit = Option.getOrUndefined(envOption);
|
|
42
|
+
if (explicit) return explicit;
|
|
43
|
+
|
|
44
|
+
const config = yield* ConfigService;
|
|
45
|
+
const defaultEnv = getDefaultEnvironment(config);
|
|
46
|
+
|
|
47
|
+
if (defaultEnv === "prod") {
|
|
48
|
+
return yield* new LogsConfigError({
|
|
49
|
+
message:
|
|
50
|
+
"Implicit prod access blocked. Config defaultEnvironment is 'prod' but --env was not passed explicitly.",
|
|
51
|
+
hint: "Pass --env prod explicitly to confirm production access, or change defaultEnvironment to a non-prod value.",
|
|
52
|
+
nextCommand: "agent-tools-logs list --env prod",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (defaultEnv) return defaultEnv;
|
|
57
|
+
|
|
58
|
+
return yield* new LogsConfigError({
|
|
59
|
+
message:
|
|
60
|
+
"No environment specified. Use --env <name> or set defaultEnvironment in agent-tools.json5.",
|
|
61
|
+
hint: 'Set defaultEnvironment in agent-tools.json5 (e.g. defaultEnvironment: "local") or pass --env explicitly.',
|
|
62
|
+
nextCommand: "agent-tools-logs list --env local",
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
35
66
|
const buildSource = (
|
|
36
67
|
mode: "list" | "read",
|
|
37
68
|
env: Environment,
|
|
@@ -56,20 +87,23 @@ const buildSource = (
|
|
|
56
87
|
const listCommand = Command.make(
|
|
57
88
|
"list",
|
|
58
89
|
{
|
|
59
|
-
env: Flag.
|
|
60
|
-
Flag.withDescription(
|
|
90
|
+
env: Flag.optional(Flag.string("env")).pipe(
|
|
91
|
+
Flag.withDescription(
|
|
92
|
+
"Target environment (e.g. local, test, prod). Falls back to defaultEnvironment in config.",
|
|
93
|
+
),
|
|
61
94
|
),
|
|
62
95
|
format: formatOption,
|
|
63
96
|
profile: profileOption,
|
|
64
97
|
},
|
|
65
98
|
({ env, format, profile }) =>
|
|
66
99
|
Effect.gen(function* () {
|
|
100
|
+
const resolvedEnv = yield* resolveEnv(env);
|
|
67
101
|
const logsService = yield* LogsService;
|
|
68
102
|
const startTime = Date.now();
|
|
69
103
|
const profileName = Option.getOrUndefined(profile);
|
|
70
104
|
|
|
71
105
|
const result = yield* logsService
|
|
72
|
-
.listLogs(
|
|
106
|
+
.listLogs(resolvedEnv as Environment, profileName)
|
|
73
107
|
.pipe(Effect.result);
|
|
74
108
|
const executionTimeMs = Date.now() - startTime;
|
|
75
109
|
|
|
@@ -79,11 +113,22 @@ const listCommand = Command.make(
|
|
|
79
113
|
error: error.message,
|
|
80
114
|
source: error instanceof LogsNotFoundError ? error.path : undefined,
|
|
81
115
|
executionTimeMs,
|
|
116
|
+
hint:
|
|
117
|
+
error.hint ??
|
|
118
|
+
(error instanceof LogsReadError && error.source === "config"
|
|
119
|
+
? "Add a 'logs' section to agent-tools.json5 with localDir and remotePath."
|
|
120
|
+
: undefined),
|
|
121
|
+
nextCommand:
|
|
122
|
+
error.nextCommand ??
|
|
123
|
+
(error instanceof LogsReadError && error.source === "config"
|
|
124
|
+
? "agent-tools-logs list --env local"
|
|
125
|
+
: undefined),
|
|
126
|
+
retryable: error.retryable ?? (error instanceof LogsTimeoutError ? true : undefined),
|
|
82
127
|
}),
|
|
83
128
|
onSuccess: (data) => ({
|
|
84
129
|
success: true,
|
|
85
130
|
data,
|
|
86
|
-
source: buildSource("list",
|
|
131
|
+
source: buildSource("list", resolvedEnv as Environment, undefined, null),
|
|
87
132
|
executionTimeMs,
|
|
88
133
|
}),
|
|
89
134
|
});
|
|
@@ -95,8 +140,10 @@ const listCommand = Command.make(
|
|
|
95
140
|
const readCommand = Command.make(
|
|
96
141
|
"read",
|
|
97
142
|
{
|
|
98
|
-
env: Flag.
|
|
99
|
-
Flag.withDescription(
|
|
143
|
+
env: Flag.optional(Flag.string("env")).pipe(
|
|
144
|
+
Flag.withDescription(
|
|
145
|
+
"Target environment (e.g. local, test, prod). Falls back to defaultEnvironment in config.",
|
|
146
|
+
),
|
|
100
147
|
),
|
|
101
148
|
file: Flag.string("file").pipe(
|
|
102
149
|
Flag.withDescription("Specific log file to read"),
|
|
@@ -119,6 +166,7 @@ const readCommand = Command.make(
|
|
|
119
166
|
},
|
|
120
167
|
({ env, file, format, grep, pretty, profile, tail }) =>
|
|
121
168
|
Effect.gen(function* () {
|
|
169
|
+
const resolvedEnv = yield* resolveEnv(env);
|
|
122
170
|
const logsService = yield* LogsService;
|
|
123
171
|
const startTime = Date.now();
|
|
124
172
|
const profileName = Option.getOrUndefined(profile);
|
|
@@ -131,7 +179,7 @@ const readCommand = Command.make(
|
|
|
131
179
|
};
|
|
132
180
|
|
|
133
181
|
const result = yield* logsService
|
|
134
|
-
.readLogs(
|
|
182
|
+
.readLogs(resolvedEnv as Environment, readOptions, profileName)
|
|
135
183
|
.pipe(Effect.result);
|
|
136
184
|
const executionTimeMs = Date.now() - startTime;
|
|
137
185
|
|
|
@@ -141,11 +189,22 @@ const readCommand = Command.make(
|
|
|
141
189
|
error: error.message,
|
|
142
190
|
source: error instanceof LogsNotFoundError ? error.path : undefined,
|
|
143
191
|
executionTimeMs,
|
|
192
|
+
hint:
|
|
193
|
+
error.hint ??
|
|
194
|
+
(error instanceof LogsReadError && error.source === "config"
|
|
195
|
+
? "Add a 'logs' section to agent-tools.json5 with localDir and remotePath."
|
|
196
|
+
: undefined),
|
|
197
|
+
nextCommand:
|
|
198
|
+
error.nextCommand ??
|
|
199
|
+
(error instanceof LogsReadError && error.source === "config"
|
|
200
|
+
? "agent-tools-logs read --env local --file app.log"
|
|
201
|
+
: undefined),
|
|
202
|
+
retryable: error.retryable ?? (error instanceof LogsTimeoutError ? true : undefined),
|
|
144
203
|
}),
|
|
145
204
|
onSuccess: (data) => ({
|
|
146
205
|
success: true,
|
|
147
206
|
data,
|
|
148
|
-
source: buildSource("read",
|
|
207
|
+
source: buildSource("read", resolvedEnv as Environment, undefined, readOptions),
|
|
149
208
|
executionTimeMs,
|
|
150
209
|
}),
|
|
151
210
|
});
|
|
@@ -167,7 +226,10 @@ export const run = Command.runWith(mainCommand, {
|
|
|
167
226
|
version: VERSION,
|
|
168
227
|
});
|
|
169
228
|
|
|
170
|
-
const MainLayer = LogsServiceLayer.pipe(
|
|
229
|
+
const MainLayer = LogsServiceLayer.pipe(
|
|
230
|
+
Layer.provideMerge(ConfigServiceLayer),
|
|
231
|
+
Layer.provideMerge(BunServices.layer),
|
|
232
|
+
);
|
|
171
233
|
|
|
172
234
|
const program = cli.pipe(Effect.provide(MainLayer), Effect.tapCause(renderCauseToStderr));
|
|
173
235
|
|
package/src/logs-tool/service.ts
CHANGED
|
@@ -3,10 +3,10 @@ import { Effect, Layer, Result, ServiceMap, Stream } from "effect";
|
|
|
3
3
|
|
|
4
4
|
import type { Environment, LogFile, ReadOptions } from "./types";
|
|
5
5
|
|
|
6
|
-
import { K8sCommandError } from "
|
|
7
|
-
import { K8sService, K8sServiceLayer } from "
|
|
8
|
-
import { ConfigService, ConfigServiceLayer, getToolConfig } from "
|
|
9
|
-
import type { LogsConfig } from "
|
|
6
|
+
import { K8sCommandError } from "#src/k8s-tool/errors";
|
|
7
|
+
import { K8sService, K8sServiceLayer } from "#src/k8s-tool/service";
|
|
8
|
+
import { ConfigService, ConfigServiceLayer, getToolConfig } from "#src/config/loader";
|
|
9
|
+
import type { LogsConfig } from "#src/config/types";
|
|
10
10
|
import { LogsNotFoundError, LogsReadError, type LogsError } from "./errors";
|
|
11
11
|
|
|
12
12
|
export const parseLogFiles = (output: string): LogFile[] => {
|
package/src/logs-tool/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Environment, OutputFormat } from "
|
|
1
|
+
import type { Environment, OutputFormat } from "#src/shared";
|
|
2
2
|
export type { Environment, OutputFormat };
|
|
3
3
|
|
|
4
4
|
export type LogFile = {
|
|
@@ -20,6 +20,9 @@ export type LogResult = {
|
|
|
20
20
|
error?: string;
|
|
21
21
|
source?: string;
|
|
22
22
|
executionTimeMs: number;
|
|
23
|
+
hint?: string;
|
|
24
|
+
nextCommand?: string;
|
|
25
|
+
retryable?: boolean;
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
export type ParsedArgs =
|
|
@@ -2,7 +2,7 @@ import { Effect, Layer, ServiceMap } from "effect";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
|
-
import { loadConfig } from "
|
|
5
|
+
import { loadConfig } from "#src/config/loader";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Resolves the OpenCode storage base path from config or default.
|
|
@@ -13,7 +13,7 @@ import { Console, Effect, Layer, Result } from "effect";
|
|
|
13
13
|
|
|
14
14
|
import type { MessageSummary, SessionResult } from "./types";
|
|
15
15
|
|
|
16
|
-
import { formatOption, formatOutput, VERSION } from "
|
|
16
|
+
import { formatOption, formatOutput, VERSION } from "#src/shared";
|
|
17
17
|
import { ResolvedPaths, ResolvedPathsLayer } from "./config";
|
|
18
18
|
import { SessionStorageNotFoundError } from "./errors";
|
|
19
19
|
import { formatDate, SessionService, SessionServiceLayer, truncate } from "./service";
|