@blogic-cz/agent-tools 1.5.3 → 1.5.4
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 +4 -4
- package/dist/audit-tool/main.d.ts +3 -0
- package/dist/audit-tool/main.d.ts.map +1 -0
- package/dist/az-tool/main.d.ts +3 -0
- package/dist/az-tool/main.d.ts.map +1 -0
- package/dist/azdo-tool/main.d.ts +3 -0
- package/dist/azdo-tool/main.d.ts.map +1 -0
- package/dist/db-tool/main.d.ts +3 -0
- package/dist/db-tool/main.d.ts.map +1 -0
- package/dist/gh-tool/main.d.ts +3 -0
- package/dist/gh-tool/main.d.ts.map +1 -0
- package/dist/gh-tool/pr/core.d.ts.map +1 -1
- package/dist/gh-tool/workflow.d.ts.map +1 -1
- package/dist/k8s-tool/main.d.ts +3 -0
- package/dist/k8s-tool/main.d.ts.map +1 -0
- package/dist/logs-tool/index.d.ts +1 -19
- package/dist/logs-tool/index.d.ts.map +1 -1
- package/dist/logs-tool/main.d.ts +21 -0
- package/dist/logs-tool/main.d.ts.map +1 -0
- package/dist/observability-tool/main.d.ts +3 -0
- package/dist/observability-tool/main.d.ts.map +1 -0
- package/dist/session-tool/index.d.ts +1 -11
- package/dist/session-tool/index.d.ts.map +1 -1
- package/dist/session-tool/main.d.ts +13 -0
- package/dist/session-tool/main.d.ts.map +1 -0
- package/dist/shared/effect-version.d.ts +4 -0
- package/dist/shared/effect-version.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/audit-tool/index.ts +3 -109
- package/src/audit-tool/main.ts +111 -0
- package/src/az-tool/index.ts +3 -151
- package/src/az-tool/main.ts +153 -0
- package/src/azdo-tool/index.ts +3 -220
- package/src/azdo-tool/main.ts +222 -0
- package/src/db-tool/index.ts +3 -260
- package/src/db-tool/main.ts +262 -0
- package/src/gh-tool/index.ts +3 -256
- package/src/gh-tool/main.ts +258 -0
- package/src/gh-tool/pr/core.ts +52 -2
- package/src/gh-tool/workflow.ts +1 -0
- package/src/k8s-tool/index.ts +3 -487
- package/src/k8s-tool/main.ts +489 -0
- package/src/logs-tool/index.ts +3 -247
- package/src/logs-tool/main.ts +249 -0
- package/src/observability-tool/index.ts +3 -34
- package/src/observability-tool/main.ts +36 -0
- package/src/session-tool/index.ts +3 -337
- package/src/session-tool/main.ts +339 -0
- package/src/shared/effect-version.ts +32 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
3
|
+
import { BunRuntime, BunServices } from "@effect/platform-bun";
|
|
4
|
+
import { Effect, Layer, Option } from "effect";
|
|
5
|
+
|
|
6
|
+
import type { CommandResult } from "./types";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
makeSchemaCommand,
|
|
10
|
+
formatOption,
|
|
11
|
+
formatOutput,
|
|
12
|
+
logText,
|
|
13
|
+
renderCauseToStderr,
|
|
14
|
+
VERSION,
|
|
15
|
+
} from "#shared";
|
|
16
|
+
import { AuditServiceLayer, withAudit } from "#shared/audit";
|
|
17
|
+
import { K8sService, K8sServiceLayer } from "./service";
|
|
18
|
+
import { ConfigService, ConfigServiceLayer, getDefaultEnvironment, getToolConfig } from "#config";
|
|
19
|
+
import type { K8sConfig } from "#config";
|
|
20
|
+
import { K8sContextError } from "./errors";
|
|
21
|
+
import { hasKubernetesConfig, selectK8sProfile } from "./profile";
|
|
22
|
+
import {
|
|
23
|
+
transformDescribe,
|
|
24
|
+
transformGenericKubectl,
|
|
25
|
+
transformLogs,
|
|
26
|
+
transformPods,
|
|
27
|
+
transformTop,
|
|
28
|
+
} from "./transformers";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve environment from explicit --env flag, config defaultEnvironment, or fail with hint.
|
|
32
|
+
* Rejects implicit prod: if defaultEnvironment is "prod" and --env was not passed, fail explicitly.
|
|
33
|
+
*/
|
|
34
|
+
const resolveEnv = (
|
|
35
|
+
envOption: Option.Option<string>,
|
|
36
|
+
config: Parameters<typeof getDefaultEnvironment>[0],
|
|
37
|
+
) =>
|
|
38
|
+
Effect.gen(function* () {
|
|
39
|
+
const explicit = Option.getOrUndefined(envOption);
|
|
40
|
+
if (explicit) return explicit;
|
|
41
|
+
|
|
42
|
+
const defaultEnv = getDefaultEnvironment(config);
|
|
43
|
+
|
|
44
|
+
if (defaultEnv === "prod") {
|
|
45
|
+
return yield* new K8sContextError({
|
|
46
|
+
message:
|
|
47
|
+
"Implicit prod access blocked. Config defaultEnvironment is 'prod' but --env was not passed explicitly.",
|
|
48
|
+
clusterId: "(prod-safety)",
|
|
49
|
+
hint: "Pass --env prod explicitly to confirm production access, or change defaultEnvironment to a non-prod value.",
|
|
50
|
+
nextCommand: 'agent-tools-k8s kubectl --env prod --cmd "get pods -n <namespace>"',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (defaultEnv) return defaultEnv;
|
|
55
|
+
|
|
56
|
+
return yield* new K8sContextError({
|
|
57
|
+
message:
|
|
58
|
+
"No environment specified. Use --env <name> or set defaultEnvironment in agent-tools.json5.",
|
|
59
|
+
clusterId: "(not specified)",
|
|
60
|
+
hint: 'Set defaultEnvironment in agent-tools.json5 (e.g. defaultEnvironment: "test") or pass --env explicitly.',
|
|
61
|
+
nextCommand: 'agent-tools-k8s kubectl --env test --cmd "get pods -n <namespace>"',
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const resolveEnvAndProfile = (
|
|
66
|
+
envOption: Option.Option<string>,
|
|
67
|
+
profileOption: Option.Option<string>,
|
|
68
|
+
config: Parameters<typeof getDefaultEnvironment>[0],
|
|
69
|
+
) =>
|
|
70
|
+
Effect.gen(function* () {
|
|
71
|
+
const resolvedEnv = yield* resolveEnv(envOption, config);
|
|
72
|
+
const profileName = selectK8sProfile(
|
|
73
|
+
config?.kubernetes,
|
|
74
|
+
Option.getOrUndefined(profileOption),
|
|
75
|
+
resolvedEnv,
|
|
76
|
+
);
|
|
77
|
+
return { env: resolvedEnv, profile: profileName };
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
type CommonK8sCommandOptions = {
|
|
81
|
+
readonly env: Option.Option<string>;
|
|
82
|
+
readonly dryRun: boolean;
|
|
83
|
+
readonly format: "toon" | "json";
|
|
84
|
+
readonly profile: Option.Option<string>;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const noKubernetesConfigResult = (): CommandResult => ({
|
|
88
|
+
success: false,
|
|
89
|
+
error: "No Kubernetes configuration found",
|
|
90
|
+
hint: "Add a 'kubernetes' section to agent-tools.json5 with clusterId and namespaces.",
|
|
91
|
+
nextCommand:
|
|
92
|
+
"echo '{ kubernetes: { default: { clusterId: \"my-cluster\" } } }' > agent-tools.json5",
|
|
93
|
+
executionTimeMs: 0,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const executeK8sCommand = (command: string, options: CommonK8sCommandOptions) =>
|
|
97
|
+
Effect.gen(function* () {
|
|
98
|
+
const config = yield* ConfigService;
|
|
99
|
+
|
|
100
|
+
// Check for a missing 'kubernetes' section before resolving --env, so that case still
|
|
101
|
+
// surfaces "add a kubernetes section" instead of an env-resolution error (e.g. prod-safety).
|
|
102
|
+
if (!hasKubernetesConfig(config?.kubernetes)) {
|
|
103
|
+
return noKubernetesConfigResult();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const { env: resolvedEnv, profile: profileName } = yield* resolveEnvAndProfile(
|
|
107
|
+
options.env,
|
|
108
|
+
options.profile,
|
|
109
|
+
config,
|
|
110
|
+
);
|
|
111
|
+
const k8sConfig = getToolConfig<K8sConfig>(config, "kubernetes", profileName);
|
|
112
|
+
|
|
113
|
+
if (!k8sConfig) {
|
|
114
|
+
return noKubernetesConfigResult();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const k8sService = yield* K8sService;
|
|
118
|
+
const result = yield* k8sService.runKubectl(command, options.dryRun, profileName).pipe(
|
|
119
|
+
Effect.catchTags({
|
|
120
|
+
K8sContextError: (error) => {
|
|
121
|
+
const errorResult: CommandResult = {
|
|
122
|
+
success: false,
|
|
123
|
+
error: error.message,
|
|
124
|
+
hint:
|
|
125
|
+
error.hint ??
|
|
126
|
+
`Verify cluster ID "${k8sConfig.clusterId}" matches a context in kubectl config. Run: kubectl config get-contexts`,
|
|
127
|
+
...(error.hint ? {} : { nextCommand: "kubectl config get-contexts" }),
|
|
128
|
+
executionTimeMs: 0,
|
|
129
|
+
};
|
|
130
|
+
return Effect.succeed(errorResult);
|
|
131
|
+
},
|
|
132
|
+
K8sCommandError: (error) => {
|
|
133
|
+
const errorResult: CommandResult = {
|
|
134
|
+
success: false,
|
|
135
|
+
error: error.message,
|
|
136
|
+
command: error.command,
|
|
137
|
+
hint:
|
|
138
|
+
error.hint ?? "Check command syntax and ensure the target namespace/resource exists.",
|
|
139
|
+
executionTimeMs: 0,
|
|
140
|
+
};
|
|
141
|
+
return Effect.succeed(errorResult);
|
|
142
|
+
},
|
|
143
|
+
K8sTimeoutError: (error) => {
|
|
144
|
+
const errorResult: CommandResult = {
|
|
145
|
+
success: false,
|
|
146
|
+
error: error.message,
|
|
147
|
+
command: error.command,
|
|
148
|
+
hint:
|
|
149
|
+
error.hint ??
|
|
150
|
+
`Command timed out after ${error.timeoutMs}ms. Consider increasing timeoutMs in config or narrowing the query.`,
|
|
151
|
+
executionTimeMs: error.timeoutMs,
|
|
152
|
+
};
|
|
153
|
+
return Effect.succeed(errorResult);
|
|
154
|
+
},
|
|
155
|
+
K8sDangerousCommandError: (error) => {
|
|
156
|
+
const errorResult: CommandResult = {
|
|
157
|
+
success: false,
|
|
158
|
+
error: error.message,
|
|
159
|
+
command: error.command,
|
|
160
|
+
hint:
|
|
161
|
+
error.hint ??
|
|
162
|
+
"AI agents can only run read-only kubectl commands. For mutating operations, use kubectl directly or ask a human operator.",
|
|
163
|
+
executionTimeMs: 0,
|
|
164
|
+
};
|
|
165
|
+
return Effect.succeed(errorResult);
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return { ...result, environment: resolvedEnv };
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const runK8sCommand = (command: string, options: CommonK8sCommandOptions) =>
|
|
174
|
+
Effect.gen(function* () {
|
|
175
|
+
const result = yield* executeK8sCommand(command, options);
|
|
176
|
+
yield* logText(formatOutput(result, options.format));
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const logTransformedResult = (
|
|
180
|
+
result: CommandResult,
|
|
181
|
+
format: CommonK8sCommandOptions["format"],
|
|
182
|
+
transform: (output: string) => string | Record<string, unknown>,
|
|
183
|
+
) =>
|
|
184
|
+
Effect.gen(function* () {
|
|
185
|
+
const transformedResult =
|
|
186
|
+
typeof result.output === "string" ? { ...result, output: transform(result.output) } : result;
|
|
187
|
+
yield* logText(formatOutput(transformedResult, format));
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const commonFlags = {
|
|
191
|
+
env: Flag.optional(Flag.String("env")).pipe(
|
|
192
|
+
Flag.withDescription(
|
|
193
|
+
"Target environment (e.g. test, prod). Falls back to defaultEnvironment in config.",
|
|
194
|
+
),
|
|
195
|
+
),
|
|
196
|
+
dryRun: Flag.Boolean("dry-run").pipe(
|
|
197
|
+
Flag.withAlias("d"),
|
|
198
|
+
Flag.withDescription("Show command without executing"),
|
|
199
|
+
Flag.withDefault(false),
|
|
200
|
+
),
|
|
201
|
+
format: formatOption,
|
|
202
|
+
profile: Flag.optional(Flag.String("profile")).pipe(
|
|
203
|
+
Flag.withDescription("Kubernetes profile name (if multiple configured)"),
|
|
204
|
+
),
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const buildKubectlCommand = (base: string, args: ReadonlyArray<string>) => {
|
|
208
|
+
const extras = args.filter((part) => part.length > 0);
|
|
209
|
+
return extras.length === 0 ? base : `${base} ${extras.join(" ")}`;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const resolveStructuredNamespace = (
|
|
213
|
+
namespaceOption: Option.Option<string>,
|
|
214
|
+
envOption: Option.Option<string>,
|
|
215
|
+
profileOption: Option.Option<string>,
|
|
216
|
+
) =>
|
|
217
|
+
Effect.gen(function* () {
|
|
218
|
+
const explicitNamespace = Option.getOrUndefined(namespaceOption);
|
|
219
|
+
if (explicitNamespace) return explicitNamespace;
|
|
220
|
+
|
|
221
|
+
const config = yield* ConfigService;
|
|
222
|
+
const { env: resolvedEnv, profile: profileName } = yield* resolveEnvAndProfile(
|
|
223
|
+
envOption,
|
|
224
|
+
profileOption,
|
|
225
|
+
config,
|
|
226
|
+
);
|
|
227
|
+
const k8sConfig = getToolConfig<K8sConfig>(config, "kubernetes", profileName);
|
|
228
|
+
|
|
229
|
+
if (!k8sConfig) return undefined;
|
|
230
|
+
|
|
231
|
+
return k8sConfig.namespaces[resolvedEnv];
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const kubectlCommand = Command.make(
|
|
235
|
+
"kubectl",
|
|
236
|
+
{
|
|
237
|
+
...commonFlags,
|
|
238
|
+
cmd: Flag.String("cmd").pipe(
|
|
239
|
+
Flag.withDescription('kubectl command (without "kubectl" prefix)'),
|
|
240
|
+
),
|
|
241
|
+
},
|
|
242
|
+
({ cmd, dryRun, env, format, profile }) =>
|
|
243
|
+
Effect.gen(function* () {
|
|
244
|
+
const result = yield* executeK8sCommand(cmd, { dryRun, env, format, profile });
|
|
245
|
+
return yield* logTransformedResult(result, format, (output) =>
|
|
246
|
+
transformGenericKubectl(output, cmd),
|
|
247
|
+
);
|
|
248
|
+
}),
|
|
249
|
+
).pipe(
|
|
250
|
+
Command.withDescription(
|
|
251
|
+
`Kubernetes CLI Tool for Coding Agents
|
|
252
|
+
|
|
253
|
+
Executes kubectl commands against the correct cluster context.
|
|
254
|
+
Parses commands into arguments and rejects shell syntax.
|
|
255
|
+
|
|
256
|
+
IMPORTANT FOR AI AGENTS:
|
|
257
|
+
Always use this tool instead of kubectl directly to ensure
|
|
258
|
+
commands run against the correct cluster context.
|
|
259
|
+
Use --format toon for LLM-optimized output (fewer tokens).
|
|
260
|
+
|
|
261
|
+
CLUSTER CONFIGURATION:
|
|
262
|
+
Cluster ID: (from agent-tools.json5 kubernetes profile)
|
|
263
|
+
Namespaces: (from agent-tools.json5 kubernetes profile)
|
|
264
|
+
(Context name is resolved dynamically from cluster ID)
|
|
265
|
+
|
|
266
|
+
WORKFLOW FOR AI AGENTS:
|
|
267
|
+
1. Use this tool for ALL kubectl operations on test/prod
|
|
268
|
+
2. Do not use pipes, chaining, substitution, or shell interpreters
|
|
269
|
+
3. Use logs-tool for remote log filtering
|
|
270
|
+
4. Use -n <namespace> for target namespace
|
|
271
|
+
|
|
272
|
+
EXAMPLES:
|
|
273
|
+
# List pods in test namespace
|
|
274
|
+
bun run src/k8s-tool kubectl --env test --cmd "get pods -n my-app-test"
|
|
275
|
+
|
|
276
|
+
# Get pod logs (use logs-tool when filtering is required)
|
|
277
|
+
bun run src/k8s-tool kubectl --env test --cmd "logs -l app=web-app -n my-app-test --tail=100"
|
|
278
|
+
|
|
279
|
+
# Check resource usage
|
|
280
|
+
bun run src/k8s-tool kubectl --env test --cmd "top pod -n my-app-test"
|
|
281
|
+
|
|
282
|
+
# Describe a pod
|
|
283
|
+
bun run src/k8s-tool kubectl --env test --cmd "describe pod web-app-xxx -n my-app-test"
|
|
284
|
+
|
|
285
|
+
# Execute an allowlisted diagnostic in a pod
|
|
286
|
+
bun run src/k8s-tool kubectl --env test --cmd "exec web-app-xxx -n my-app-test -- redis-cli INFO commandstats"
|
|
287
|
+
|
|
288
|
+
# Dry run - show command without executing
|
|
289
|
+
bun run src/k8s-tool kubectl --env test --cmd "get pods -n my-app-test" --dry-run
|
|
290
|
+
|
|
291
|
+
OUTPUT:
|
|
292
|
+
TOON: Token-efficient format for LLM agents - DEFAULT
|
|
293
|
+
JSON: { success, output?, error?, command?, executionTimeMs }
|
|
294
|
+
|
|
295
|
+
RELATED TOOLS:
|
|
296
|
+
- logs-tool: Higher-level tool for reading application logs
|
|
297
|
+
- db-tool: Database queries and schema introspection`,
|
|
298
|
+
),
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const podsCommand = Command.make(
|
|
302
|
+
"pods",
|
|
303
|
+
{
|
|
304
|
+
...commonFlags,
|
|
305
|
+
namespace: Flag.String("namespace").pipe(
|
|
306
|
+
Flag.withDescription("Namespace to query"),
|
|
307
|
+
Flag.optional,
|
|
308
|
+
),
|
|
309
|
+
label: Flag.String("label").pipe(
|
|
310
|
+
Flag.withDescription("Label selector (key=value)"),
|
|
311
|
+
Flag.optional,
|
|
312
|
+
),
|
|
313
|
+
wide: Flag.Boolean("wide").pipe(
|
|
314
|
+
Flag.withDescription("Show additional pod information"),
|
|
315
|
+
Flag.withDefault(false),
|
|
316
|
+
),
|
|
317
|
+
},
|
|
318
|
+
({ dryRun, env, format, label, namespace, profile }) =>
|
|
319
|
+
Effect.gen(function* () {
|
|
320
|
+
const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
|
|
321
|
+
const command = buildKubectlCommand("get pods -o json", [
|
|
322
|
+
Option.match(label, {
|
|
323
|
+
onNone: () => "",
|
|
324
|
+
onSome: (value) => `-l ${value}`,
|
|
325
|
+
}),
|
|
326
|
+
resolvedNamespace ? `-n ${resolvedNamespace}` : "",
|
|
327
|
+
]);
|
|
328
|
+
const result = yield* executeK8sCommand(command, { dryRun, env, format, profile });
|
|
329
|
+
return yield* logTransformedResult(result, format, transformPods);
|
|
330
|
+
}),
|
|
331
|
+
).pipe(Command.withDescription("List pods (get pods) with optional namespace/label/wide output"));
|
|
332
|
+
|
|
333
|
+
const logsCommand = Command.make(
|
|
334
|
+
"logs",
|
|
335
|
+
{
|
|
336
|
+
...commonFlags,
|
|
337
|
+
pod: Flag.String("pod").pipe(Flag.withDescription("Pod name")),
|
|
338
|
+
namespace: Flag.String("namespace").pipe(
|
|
339
|
+
Flag.withDescription("Namespace containing the pod"),
|
|
340
|
+
Flag.optional,
|
|
341
|
+
),
|
|
342
|
+
container: Flag.String("container").pipe(
|
|
343
|
+
Flag.withDescription("Container name (for multi-container pods)"),
|
|
344
|
+
Flag.optional,
|
|
345
|
+
),
|
|
346
|
+
tail: Flag.Int("tail").pipe(Flag.withDescription("Show last N log lines"), Flag.optional),
|
|
347
|
+
follow: Flag.Boolean("follow").pipe(
|
|
348
|
+
Flag.withAlias("f"),
|
|
349
|
+
Flag.withDescription("Stream logs in real time"),
|
|
350
|
+
Flag.withDefault(false),
|
|
351
|
+
),
|
|
352
|
+
},
|
|
353
|
+
({ container, dryRun, env, follow, format, namespace, pod, profile, tail }) =>
|
|
354
|
+
Effect.gen(function* () {
|
|
355
|
+
const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
|
|
356
|
+
const command = buildKubectlCommand(`logs ${pod}`, [
|
|
357
|
+
resolvedNamespace ? `-n ${resolvedNamespace}` : "",
|
|
358
|
+
Option.match(container, {
|
|
359
|
+
onNone: () => "",
|
|
360
|
+
onSome: (value) => `-c ${value}`,
|
|
361
|
+
}),
|
|
362
|
+
Option.match(tail, {
|
|
363
|
+
onNone: () => "",
|
|
364
|
+
onSome: (value) => `--tail=${value}`,
|
|
365
|
+
}),
|
|
366
|
+
follow ? "-f" : "",
|
|
367
|
+
]);
|
|
368
|
+
const result = yield* executeK8sCommand(command, { dryRun, env, format, profile });
|
|
369
|
+
return yield* logTransformedResult(result, format, transformLogs);
|
|
370
|
+
}),
|
|
371
|
+
).pipe(Command.withDescription("Fetch pod logs with tail/follow/container selectors"));
|
|
372
|
+
|
|
373
|
+
const describeCommand = Command.make(
|
|
374
|
+
"describe",
|
|
375
|
+
{
|
|
376
|
+
...commonFlags,
|
|
377
|
+
resource: Flag.String("resource").pipe(
|
|
378
|
+
Flag.withDescription("Resource type (pod, deploy, svc, etc.)"),
|
|
379
|
+
),
|
|
380
|
+
name: Flag.String("name").pipe(Flag.withDescription("Resource name")),
|
|
381
|
+
namespace: Flag.String("namespace").pipe(
|
|
382
|
+
Flag.withDescription("Namespace containing the resource"),
|
|
383
|
+
Flag.optional,
|
|
384
|
+
),
|
|
385
|
+
},
|
|
386
|
+
({ dryRun, env, format, name, namespace, profile, resource }) =>
|
|
387
|
+
Effect.gen(function* () {
|
|
388
|
+
const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
|
|
389
|
+
const command = buildKubectlCommand(`describe ${resource} ${name}`, [
|
|
390
|
+
resolvedNamespace ? `-n ${resolvedNamespace}` : "",
|
|
391
|
+
]);
|
|
392
|
+
const result = yield* executeK8sCommand(command, { dryRun, env, format, profile });
|
|
393
|
+
return yield* logTransformedResult(result, format, transformDescribe);
|
|
394
|
+
}),
|
|
395
|
+
).pipe(Command.withDescription("Describe a Kubernetes resource by type and name"));
|
|
396
|
+
|
|
397
|
+
const execCommand = Command.make(
|
|
398
|
+
"exec",
|
|
399
|
+
{
|
|
400
|
+
...commonFlags,
|
|
401
|
+
pod: Flag.String("pod").pipe(Flag.withDescription("Pod name")),
|
|
402
|
+
execCmd: Flag.String("exec-cmd").pipe(
|
|
403
|
+
Flag.withDescription("Allowlisted diagnostic: redis-cli PING/INFO or ls"),
|
|
404
|
+
),
|
|
405
|
+
namespace: Flag.String("namespace").pipe(
|
|
406
|
+
Flag.withDescription("Namespace containing the pod"),
|
|
407
|
+
Flag.optional,
|
|
408
|
+
),
|
|
409
|
+
container: Flag.String("container").pipe(
|
|
410
|
+
Flag.withDescription("Container name (for multi-container pods)"),
|
|
411
|
+
Flag.optional,
|
|
412
|
+
),
|
|
413
|
+
},
|
|
414
|
+
({ container, dryRun, env, execCmd, format, namespace, pod, profile }) =>
|
|
415
|
+
Effect.gen(function* () {
|
|
416
|
+
const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
|
|
417
|
+
const command = buildKubectlCommand(`exec ${pod}`, [
|
|
418
|
+
resolvedNamespace ? `-n ${resolvedNamespace}` : "",
|
|
419
|
+
Option.match(container, {
|
|
420
|
+
onNone: () => "",
|
|
421
|
+
onSome: (value) => `-c ${value}`,
|
|
422
|
+
}),
|
|
423
|
+
`-- ${execCmd}`,
|
|
424
|
+
]);
|
|
425
|
+
return yield* runK8sCommand(command, { dryRun, env, format, profile });
|
|
426
|
+
}),
|
|
427
|
+
).pipe(Command.withDescription("Run an allowlisted diagnostic in a pod"));
|
|
428
|
+
|
|
429
|
+
const topCommand = Command.make(
|
|
430
|
+
"top",
|
|
431
|
+
{
|
|
432
|
+
...commonFlags,
|
|
433
|
+
namespace: Flag.String("namespace").pipe(
|
|
434
|
+
Flag.withDescription("Namespace to inspect"),
|
|
435
|
+
Flag.optional,
|
|
436
|
+
),
|
|
437
|
+
sortBy: Flag.Literals("sort-by", ["cpu", "memory"] as const).pipe(
|
|
438
|
+
Flag.withDescription("Sort metrics output when supported by kubectl"),
|
|
439
|
+
Flag.optional,
|
|
440
|
+
),
|
|
441
|
+
},
|
|
442
|
+
({ dryRun, env, format, namespace, profile, sortBy }) =>
|
|
443
|
+
Effect.gen(function* () {
|
|
444
|
+
const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
|
|
445
|
+
const command = buildKubectlCommand("top pod", [
|
|
446
|
+
resolvedNamespace ? `-n ${resolvedNamespace}` : "",
|
|
447
|
+
Option.match(sortBy, {
|
|
448
|
+
onNone: () => "",
|
|
449
|
+
onSome: (value) => `--sort-by=${value}`,
|
|
450
|
+
}),
|
|
451
|
+
]);
|
|
452
|
+
const result = yield* executeK8sCommand(command, { dryRun, env, format, profile });
|
|
453
|
+
return yield* logTransformedResult(result, format, transformTop);
|
|
454
|
+
}),
|
|
455
|
+
).pipe(Command.withDescription("Show pod CPU/memory usage (kubectl top pod)"));
|
|
456
|
+
|
|
457
|
+
const commandsCommand = makeSchemaCommand(() => mainCommand);
|
|
458
|
+
|
|
459
|
+
const mainCommand = Command.make("k8s-tool", {}).pipe(
|
|
460
|
+
Command.withDescription("Kubernetes CLI Tool for Coding Agents"),
|
|
461
|
+
Command.withSubcommands([
|
|
462
|
+
kubectlCommand,
|
|
463
|
+
podsCommand,
|
|
464
|
+
logsCommand,
|
|
465
|
+
describeCommand,
|
|
466
|
+
execCommand,
|
|
467
|
+
topCommand,
|
|
468
|
+
commandsCommand,
|
|
469
|
+
]),
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
const cli = Command.run(mainCommand, {
|
|
473
|
+
version: VERSION,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const MainLayer = K8sServiceLayer.pipe(
|
|
477
|
+
Layer.provideMerge(ConfigServiceLayer),
|
|
478
|
+
Layer.provideMerge(BunServices.layer),
|
|
479
|
+
Layer.provideMerge(AuditServiceLayer),
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
const program = withAudit("k8s", cli).pipe(
|
|
483
|
+
Effect.provide(MainLayer),
|
|
484
|
+
Effect.tapCause(renderCauseToStderr),
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
BunRuntime.runMain(program, {
|
|
488
|
+
disableErrorReporting: true,
|
|
489
|
+
});
|