kubeagent 0.1.25 → 0.1.27
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/dist/cli.js
CHANGED
|
@@ -56,8 +56,9 @@ program
|
|
|
56
56
|
program
|
|
57
57
|
.command("onboard")
|
|
58
58
|
.description("Scan cluster + codebases, interview user, generate knowledge base")
|
|
59
|
-
.
|
|
60
|
-
|
|
59
|
+
.option("-t, --timeout <seconds>", "Max seconds to wait for cluster scan (default: 120)", "120")
|
|
60
|
+
.action(async (opts) => {
|
|
61
|
+
await onboard({ timeout: parseInt(opts.timeout, 10) });
|
|
61
62
|
});
|
|
62
63
|
program
|
|
63
64
|
.command("watch")
|
|
@@ -38,5 +38,5 @@ export interface ClusterInfo {
|
|
|
38
38
|
services: ServiceInfo[];
|
|
39
39
|
ingresses: IngressInfo[];
|
|
40
40
|
}
|
|
41
|
-
export declare function scanCluster(options: KubectlOptions): Promise<ClusterInfo>;
|
|
41
|
+
export declare function scanCluster(options: KubectlOptions, onProgress?: (step: string) => void): Promise<ClusterInfo>;
|
|
42
42
|
export declare function formatClusterMarkdown(info: ClusterInfo): string;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { kubectlJson } from "../kubectl.js";
|
|
2
|
-
export async function scanCluster(options) {
|
|
2
|
+
export async function scanCluster(options, onProgress) {
|
|
3
3
|
const context = options.context ?? "default";
|
|
4
4
|
// Nodes
|
|
5
|
+
onProgress?.("nodes");
|
|
5
6
|
const nodesRaw = (await kubectlJson(["get", "nodes"], options));
|
|
6
7
|
const nodes = nodesRaw.items.map((n) => ({
|
|
7
8
|
name: n.metadata.name,
|
|
@@ -12,9 +13,11 @@ export async function scanCluster(options) {
|
|
|
12
13
|
os: n.status.nodeInfo.osImage,
|
|
13
14
|
}));
|
|
14
15
|
// Namespaces
|
|
16
|
+
onProgress?.("namespaces");
|
|
15
17
|
const nsRaw = (await kubectlJson(["get", "namespaces"], options));
|
|
16
18
|
const namespaces = nsRaw.items.map((n) => n.metadata.name);
|
|
17
19
|
// Deployments
|
|
20
|
+
onProgress?.("deployments");
|
|
18
21
|
const deplRaw = (await kubectlJson(["get", "deployments", "--all-namespaces"], options));
|
|
19
22
|
const deployments = deplRaw.items.map((d) => ({
|
|
20
23
|
name: d.metadata.name,
|
|
@@ -23,6 +26,7 @@ export async function scanCluster(options) {
|
|
|
23
26
|
image: d.spec.template.spec.containers[0]?.image ?? "unknown",
|
|
24
27
|
}));
|
|
25
28
|
// StatefulSets
|
|
29
|
+
onProgress?.("statefulsets");
|
|
26
30
|
const ssRaw = (await kubectlJson(["get", "statefulsets", "--all-namespaces"], options));
|
|
27
31
|
const statefulsets = ssRaw.items.map((s) => ({
|
|
28
32
|
name: s.metadata.name,
|
|
@@ -31,6 +35,7 @@ export async function scanCluster(options) {
|
|
|
31
35
|
image: s.spec.template.spec.containers[0]?.image ?? "unknown",
|
|
32
36
|
}));
|
|
33
37
|
// Services
|
|
38
|
+
onProgress?.("services");
|
|
34
39
|
const svcRaw = (await kubectlJson(["get", "services", "--all-namespaces"], options));
|
|
35
40
|
const services = svcRaw.items.map((s) => ({
|
|
36
41
|
name: s.metadata.name,
|
|
@@ -39,6 +44,7 @@ export async function scanCluster(options) {
|
|
|
39
44
|
ports: (s.spec.ports ?? []).map((p) => `${p.port}/${p.protocol}`).join(", "),
|
|
40
45
|
}));
|
|
41
46
|
// Ingresses
|
|
47
|
+
onProgress?.("ingresses");
|
|
42
48
|
const ingRaw = (await kubectlJson(["get", "ingresses", "--all-namespaces"], options));
|
|
43
49
|
const ingresses = ingRaw.items.map((i) => ({
|
|
44
50
|
name: i.metadata.name,
|
package/dist/onboard/index.d.ts
CHANGED
package/dist/onboard/index.js
CHANGED
|
@@ -56,8 +56,9 @@ async function configureSafeActions(current) {
|
|
|
56
56
|
console.log(chalk.dim(`Safe actions set to: ${result.length > 0 ? result.join(", ") : "(none — all require approval)"}`));
|
|
57
57
|
return result;
|
|
58
58
|
}
|
|
59
|
-
export async function onboard() {
|
|
59
|
+
export async function onboard(opts = {}) {
|
|
60
60
|
console.log(chalk.bold("\nKubeAgent Onboarding\n"));
|
|
61
|
+
const timeoutMs = (opts.timeout ?? 120) * 1000;
|
|
61
62
|
// Step 1: Pick cluster context from list
|
|
62
63
|
const context = await pickContext();
|
|
63
64
|
const kubectlOpts = context ? { context } : {};
|
|
@@ -66,8 +67,16 @@ export async function onboard() {
|
|
|
66
67
|
const spinner = ora("Scanning cluster...").start();
|
|
67
68
|
let clusterInfo;
|
|
68
69
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
const scanPromise = scanCluster(kubectlOpts, (step) => {
|
|
71
|
+
spinner.text = `Scanning cluster — ${step}...`;
|
|
72
|
+
});
|
|
73
|
+
let timeoutId;
|
|
74
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
75
|
+
timeoutId = setTimeout(() => reject(new Error(`Cluster scan timed out after ${opts.timeout ?? 120}s`)), timeoutMs);
|
|
76
|
+
});
|
|
77
|
+
clusterInfo = await Promise.race([scanPromise, timeoutPromise]);
|
|
78
|
+
clearTimeout(timeoutId);
|
|
79
|
+
spinner.succeed(`Found ${clusterInfo.nodes.length} nodes, ${clusterInfo.deployments.length} deployments across ${clusterInfo.namespaces.length} namespaces`);
|
|
71
80
|
}
|
|
72
81
|
catch (err) {
|
|
73
82
|
spinner.fail(`Cluster scan failed: ${err.message}`);
|
|
@@ -96,8 +105,12 @@ export async function onboard() {
|
|
|
96
105
|
const notes = await runInterview({ cluster: clusterInfo, projects });
|
|
97
106
|
const kbDir = join(configDir(), "clusters", contextName);
|
|
98
107
|
ensureKbDir(kbDir);
|
|
108
|
+
const totalItems = projects.length + 1;
|
|
109
|
+
const earlyKbSpinner = ora(`Building knowledge base [1/${totalItems}]: cluster...`).start();
|
|
99
110
|
writeClusterKb(kbDir, formatClusterMarkdown(clusterInfo));
|
|
100
|
-
for (
|
|
111
|
+
for (let i = 0; i < projects.length; i++) {
|
|
112
|
+
const p = projects[i];
|
|
113
|
+
earlyKbSpinner.text = `Building knowledge base [${i + 2}/${totalItems}]: ${p.name}...`;
|
|
101
114
|
const mapping = projectMappings.find((m) => m.name === p.name);
|
|
102
115
|
const mappingNote = mapping ? `**Deployment:** ${mapping.deployment} (${mapping.namespace})\n**Kind:** ${mapping.kind}` : undefined;
|
|
103
116
|
writeProjectKb(kbDir, p.name, formatProjectMarkdown(p.name, p.path, p.stack, mappingNote));
|
|
@@ -105,6 +118,7 @@ export async function onboard() {
|
|
|
105
118
|
if (notes.size > 0) {
|
|
106
119
|
writeProjectKb(kbDir, "_notes", `# Onboarding Notes\n\n${Array.from(notes.entries()).map(([q, a]) => `**${q}**\n${a}`).join("\n\n")}\n`);
|
|
107
120
|
}
|
|
121
|
+
earlyKbSpinner.succeed(`Knowledge base written (${totalItems} file${totalItems !== 1 ? "s" : ""})`);
|
|
108
122
|
const currentSafeActions = existingConfig.remediation?.safe_actions ?? DEFAULT_SAFE_ACTIONS;
|
|
109
123
|
const safeActions = await configureSafeActions(currentSafeActions);
|
|
110
124
|
const existingChannels = existingConfig.notifications?.channels ?? [];
|
|
@@ -258,9 +272,13 @@ export async function onboard() {
|
|
|
258
272
|
// Step 6: Write knowledge base
|
|
259
273
|
const kbDir = join(configDir(), "clusters", contextName);
|
|
260
274
|
ensureKbDir(kbDir);
|
|
275
|
+
const totalKbItems = projects.length + 1; // +1 for cluster
|
|
276
|
+
const kbSpinner = ora(`Building knowledge base [1/${totalKbItems}]: cluster...`).start();
|
|
261
277
|
const clusterMd = formatClusterMarkdown(clusterInfo);
|
|
262
278
|
writeClusterKb(kbDir, clusterMd);
|
|
263
|
-
for (
|
|
279
|
+
for (let i = 0; i < projects.length; i++) {
|
|
280
|
+
const p = projects[i];
|
|
281
|
+
kbSpinner.text = `Building knowledge base [${i + 2}/${totalKbItems}]: ${p.name}...`;
|
|
264
282
|
const interviewNotes = Array.from(notes.entries())
|
|
265
283
|
.filter(([q]) => q.toLowerCase().includes(p.name.toLowerCase()))
|
|
266
284
|
.map(([q, a]) => `**${q}**\n${a}`)
|
|
@@ -280,6 +298,7 @@ export async function onboard() {
|
|
|
280
298
|
.join("\n\n");
|
|
281
299
|
writeProjectKb(kbDir, "_notes", `# Onboarding Notes\n\n${generalNotes}\n`);
|
|
282
300
|
}
|
|
301
|
+
kbSpinner.succeed(`Knowledge base written (${totalKbItems} file${totalKbItems !== 1 ? "s" : ""})`);
|
|
283
302
|
// Step 6b: Configure safe actions
|
|
284
303
|
const currentSafeActions = existingConfig.remediation?.safe_actions ?? DEFAULT_SAFE_ACTIONS;
|
|
285
304
|
const safeActions = await configureSafeActions(currentSafeActions);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import readline from "node:readline";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
3
4
|
import { loadAuth } from "../auth.js";
|
|
4
5
|
import { proxyRequest } from "../proxy-client.js";
|
|
5
6
|
async function ask(question) {
|
|
@@ -29,6 +30,7 @@ export async function runInterview(context) {
|
|
|
29
30
|
`Projects: ${context.projects.map((p) => `${p.name} (${p.stack.language}/${p.stack.framework})`).join(", ")}`,
|
|
30
31
|
].join("\n");
|
|
31
32
|
let response;
|
|
33
|
+
const interviewSpinner = ora("Preparing interview questions...").start();
|
|
32
34
|
try {
|
|
33
35
|
response = await createMessage({
|
|
34
36
|
model: "claude-haiku-4-5-20251001",
|
|
@@ -40,8 +42,10 @@ export async function runInterview(context) {
|
|
|
40
42
|
},
|
|
41
43
|
],
|
|
42
44
|
});
|
|
45
|
+
interviewSpinner.succeed("Interview questions ready");
|
|
43
46
|
}
|
|
44
47
|
catch (err) {
|
|
48
|
+
interviewSpinner.stop();
|
|
45
49
|
const msg = err.message;
|
|
46
50
|
if (msg === "no_auth") {
|
|
47
51
|
console.log(chalk.yellow("Skipping AI interview — run 'kubeagent login' to enable."));
|
package/dist/orchestrator.js
CHANGED
|
@@ -97,15 +97,20 @@ export async function handleIssues(issues, config, clusterContext, noInteractive
|
|
|
97
97
|
const kbDir = join(configDir(), "clusters", clusterContext ?? "default");
|
|
98
98
|
const dateStr = new Date().toISOString().slice(0, 10);
|
|
99
99
|
// ── Issues detected ──────────────────────────────────────
|
|
100
|
-
const
|
|
101
|
-
const
|
|
100
|
+
const SEVERITY_RANK = { critical: 0, warning: 1, info: 2 };
|
|
101
|
+
const sortBySeverity = (a, b) => (SEVERITY_RANK[a.severity] ?? 3) - (SEVERITY_RANK[b.severity] ?? 3);
|
|
102
|
+
const sortedIssues = [...issues].sort(sortBySeverity);
|
|
103
|
+
const critCount = sortedIssues.filter((i) => i.severity === "critical").length;
|
|
104
|
+
const warnCount = sortedIssues.filter((i) => i.severity === "warning").length;
|
|
105
|
+
const infoCount = sortedIssues.filter((i) => i.severity === "info").length;
|
|
102
106
|
const parts = [
|
|
103
|
-
critCount ? chalk.red(`${critCount}
|
|
104
|
-
warnCount ? chalk.yellow(`${warnCount}
|
|
107
|
+
critCount ? chalk.red(`${critCount} CRITICAL`) : "",
|
|
108
|
+
warnCount ? chalk.yellow(`${warnCount} WARNING`) : "",
|
|
109
|
+
infoCount ? chalk.cyan(`${infoCount} INFO`) : "",
|
|
105
110
|
].filter(Boolean).join(chalk.dim(", "));
|
|
106
|
-
sectionHeader(`${
|
|
107
|
-
const gaveUpIssues =
|
|
108
|
-
const actionableIssues =
|
|
111
|
+
sectionHeader(`${sortedIssues.length} issue${sortedIssues.length !== 1 ? "s" : ""} detected ${parts}`, chalk.red.bold);
|
|
112
|
+
const gaveUpIssues = sortedIssues.filter((i) => getOrCreate(issueKey(i)).gaveUp);
|
|
113
|
+
const actionableIssues = sortedIssues.filter((i) => !getOrCreate(issueKey(i)).gaveUp);
|
|
109
114
|
for (const issue of actionableIssues) {
|
|
110
115
|
printIssue(issue);
|
|
111
116
|
}
|