kubeagent 0.1.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/LICENSE +72 -0
- package/README.md +154 -0
- package/dist/auth.d.ts +23 -0
- package/dist/auth.js +162 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +447 -0
- package/dist/config.d.ts +50 -0
- package/dist/config.js +79 -0
- package/dist/debug.d.ts +10 -0
- package/dist/debug.js +18 -0
- package/dist/diagnoser/index.d.ts +17 -0
- package/dist/diagnoser/index.js +251 -0
- package/dist/diagnoser/tools.d.ts +119 -0
- package/dist/diagnoser/tools.js +108 -0
- package/dist/kb/loader.d.ts +1 -0
- package/dist/kb/loader.js +41 -0
- package/dist/kb/writer.d.ts +11 -0
- package/dist/kb/writer.js +36 -0
- package/dist/kubectl-config.d.ts +7 -0
- package/dist/kubectl-config.js +47 -0
- package/dist/kubectl.d.ts +13 -0
- package/dist/kubectl.js +57 -0
- package/dist/monitor/checks.d.ts +71 -0
- package/dist/monitor/checks.js +167 -0
- package/dist/monitor/index.d.ts +7 -0
- package/dist/monitor/index.js +126 -0
- package/dist/monitor/types.d.ts +11 -0
- package/dist/monitor/types.js +1 -0
- package/dist/notify/index.d.ts +5 -0
- package/dist/notify/index.js +40 -0
- package/dist/notify/setup.d.ts +4 -0
- package/dist/notify/setup.js +88 -0
- package/dist/notify/slack.d.ts +4 -0
- package/dist/notify/slack.js +76 -0
- package/dist/notify/telegram.d.ts +8 -0
- package/dist/notify/telegram.js +63 -0
- package/dist/notify/webhook.d.ts +3 -0
- package/dist/notify/webhook.js +49 -0
- package/dist/onboard/cluster-scan.d.ts +42 -0
- package/dist/onboard/cluster-scan.js +103 -0
- package/dist/onboard/code-scan.d.ts +9 -0
- package/dist/onboard/code-scan.js +114 -0
- package/dist/onboard/index.d.ts +1 -0
- package/dist/onboard/index.js +328 -0
- package/dist/onboard/interview.d.ts +12 -0
- package/dist/onboard/interview.js +71 -0
- package/dist/onboard/project-matcher.d.ts +25 -0
- package/dist/onboard/project-matcher.js +149 -0
- package/dist/orchestrator.d.ts +3 -0
- package/dist/orchestrator.js +222 -0
- package/dist/proxy-client.d.ts +15 -0
- package/dist/proxy-client.js +72 -0
- package/dist/render.d.ts +5 -0
- package/dist/render.js +143 -0
- package/dist/verifier.d.ts +9 -0
- package/dist/verifier.js +17 -0
- package/package.json +39 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Issue } from "../monitor/types.js";
|
|
2
|
+
import type { TelegramChannel } from "../config.js";
|
|
3
|
+
export declare function sendTelegramQuestion(channel: TelegramChannel, question: string, choices: string[] | undefined, clusterContext?: string): Promise<void>;
|
|
4
|
+
export declare function sendTelegram(issues: Issue[], channel: TelegramChannel, clusterContext?: string): Promise<void>;
|
|
5
|
+
export declare function testTelegramCredentials(botToken: string, chatId: string): Promise<{
|
|
6
|
+
ok: boolean;
|
|
7
|
+
error?: string;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const TELEGRAM_API = "https://api.telegram.org";
|
|
2
|
+
function formatTelegramMessage(issues, clusterContext) {
|
|
3
|
+
const ctx = clusterContext ? ` — \`${clusterContext}\`` : "";
|
|
4
|
+
const header = `⚠ *KubeAgent Alert*${ctx}`;
|
|
5
|
+
const lines = issues.map((i) => {
|
|
6
|
+
const icon = i.severity === "critical" ? "🔴" : "🟡";
|
|
7
|
+
const ns = i.namespace ? ` \`${i.namespace}\`` : "";
|
|
8
|
+
return `${icon} ${i.message}${ns}`;
|
|
9
|
+
});
|
|
10
|
+
const critical = issues.filter((i) => i.severity === "critical").length;
|
|
11
|
+
const warning = issues.filter((i) => i.severity === "warning").length;
|
|
12
|
+
const summary = [
|
|
13
|
+
critical ? `${critical} critical` : "",
|
|
14
|
+
warning ? `${warning} warning` : "",
|
|
15
|
+
].filter(Boolean).join(", ");
|
|
16
|
+
return [header, summary, "", ...lines].join("\n");
|
|
17
|
+
}
|
|
18
|
+
async function postToTelegram(channel, text) {
|
|
19
|
+
const url = `${TELEGRAM_API}/bot${channel.bot_token}/sendMessage`;
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(url, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: { "Content-Type": "application/json" },
|
|
24
|
+
body: JSON.stringify({ chat_id: channel.chat_id, text, parse_mode: "Markdown" }),
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
const body = await res.json().catch(() => ({}));
|
|
28
|
+
console.error(`Telegram error: ${body.description ?? res.statusText}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
console.error(`Telegram error: ${err.message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function sendTelegramQuestion(channel, question, choices, clusterContext) {
|
|
36
|
+
const ctx = clusterContext ? ` — \`${clusterContext}\`` : "";
|
|
37
|
+
const choiceLines = choices?.map((c, i) => ` ${i + 1}. ${c}`) ?? [];
|
|
38
|
+
const body = choiceLines.length
|
|
39
|
+
? `${question}\n\n${choiceLines.join("\n")}\n\n_Answer via terminal._`
|
|
40
|
+
: `${question}\n\n_Answer required via terminal._`;
|
|
41
|
+
await postToTelegram(channel, `❓ *KubeAgent needs input*${ctx}\n\n${body}`);
|
|
42
|
+
}
|
|
43
|
+
export async function sendTelegram(issues, channel, clusterContext) {
|
|
44
|
+
await postToTelegram(channel, formatTelegramMessage(issues, clusterContext));
|
|
45
|
+
}
|
|
46
|
+
export async function testTelegramCredentials(botToken, chatId) {
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(`${TELEGRAM_API}/bot${botToken}/sendMessage`, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "Content-Type": "application/json" },
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
chat_id: chatId,
|
|
53
|
+
text: "✅ *KubeAgent* connected successfully\\! You'll receive cluster alerts here\\.",
|
|
54
|
+
parse_mode: "MarkdownV2",
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
const body = await res.json();
|
|
58
|
+
return body.ok ? { ok: true } : { ok: false, error: body.description };
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
return { ok: false, error: err.message };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
function formatMessage(issues) {
|
|
2
|
+
const header = `**KubeAgent Alert** — ${issues.length} issue(s) detected\n`;
|
|
3
|
+
const body = issues
|
|
4
|
+
.map((i) => {
|
|
5
|
+
const icon = i.severity === "critical" ? "\u{1F534}" : "\u{1F7E1}";
|
|
6
|
+
return `${icon} **${i.kind}**: ${i.message}`;
|
|
7
|
+
})
|
|
8
|
+
.join("\n");
|
|
9
|
+
return header + body;
|
|
10
|
+
}
|
|
11
|
+
export async function sendWebhook(issues, config) {
|
|
12
|
+
const message = formatMessage(issues);
|
|
13
|
+
let payload;
|
|
14
|
+
switch (config.type) {
|
|
15
|
+
case "discord":
|
|
16
|
+
payload = { content: message };
|
|
17
|
+
break;
|
|
18
|
+
case "slack":
|
|
19
|
+
case "mattermost":
|
|
20
|
+
default:
|
|
21
|
+
payload = { text: message };
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
// Validate URL to prevent SSRF
|
|
25
|
+
try {
|
|
26
|
+
const url = new URL(config.url);
|
|
27
|
+
if (!["https:", "http:"].includes(url.protocol)) {
|
|
28
|
+
console.error(`Webhook error: unsupported protocol ${url.protocol}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
console.error(`Webhook error: invalid URL ${config.url}`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch(config.url, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: { "Content-Type": "application/json" },
|
|
40
|
+
body: JSON.stringify(payload),
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
console.error(`Webhook failed: ${response.status} ${response.statusText}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
console.error(`Webhook error: ${err.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type KubectlOptions } from "../kubectl.js";
|
|
2
|
+
export interface NodeInfo {
|
|
3
|
+
name: string;
|
|
4
|
+
roles: string[];
|
|
5
|
+
version: string;
|
|
6
|
+
os: string;
|
|
7
|
+
}
|
|
8
|
+
export interface DeploymentInfo {
|
|
9
|
+
name: string;
|
|
10
|
+
namespace: string;
|
|
11
|
+
replicas: number;
|
|
12
|
+
image: string;
|
|
13
|
+
}
|
|
14
|
+
export interface StatefulSetInfo {
|
|
15
|
+
name: string;
|
|
16
|
+
namespace: string;
|
|
17
|
+
replicas: number;
|
|
18
|
+
image: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ServiceInfo {
|
|
21
|
+
name: string;
|
|
22
|
+
namespace: string;
|
|
23
|
+
type: string;
|
|
24
|
+
ports: string;
|
|
25
|
+
}
|
|
26
|
+
export interface IngressInfo {
|
|
27
|
+
name: string;
|
|
28
|
+
namespace: string;
|
|
29
|
+
hosts: string[];
|
|
30
|
+
tls: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface ClusterInfo {
|
|
33
|
+
context: string;
|
|
34
|
+
nodes: NodeInfo[];
|
|
35
|
+
namespaces: string[];
|
|
36
|
+
deployments: DeploymentInfo[];
|
|
37
|
+
statefulsets: StatefulSetInfo[];
|
|
38
|
+
services: ServiceInfo[];
|
|
39
|
+
ingresses: IngressInfo[];
|
|
40
|
+
}
|
|
41
|
+
export declare function scanCluster(options: KubectlOptions): Promise<ClusterInfo>;
|
|
42
|
+
export declare function formatClusterMarkdown(info: ClusterInfo): string;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { kubectlJson } from "../kubectl.js";
|
|
2
|
+
export async function scanCluster(options) {
|
|
3
|
+
const context = options.context ?? "default";
|
|
4
|
+
// Nodes
|
|
5
|
+
const nodesRaw = (await kubectlJson(["get", "nodes"], options));
|
|
6
|
+
const nodes = nodesRaw.items.map((n) => ({
|
|
7
|
+
name: n.metadata.name,
|
|
8
|
+
roles: Object.keys(n.metadata.labels)
|
|
9
|
+
.filter((l) => l.startsWith("node-role.kubernetes.io/"))
|
|
10
|
+
.map((l) => l.replace("node-role.kubernetes.io/", "")),
|
|
11
|
+
version: n.status.nodeInfo.kubeletVersion,
|
|
12
|
+
os: n.status.nodeInfo.osImage,
|
|
13
|
+
}));
|
|
14
|
+
// Namespaces
|
|
15
|
+
const nsRaw = (await kubectlJson(["get", "namespaces"], options));
|
|
16
|
+
const namespaces = nsRaw.items.map((n) => n.metadata.name);
|
|
17
|
+
// Deployments
|
|
18
|
+
const deplRaw = (await kubectlJson(["get", "deployments", "--all-namespaces"], options));
|
|
19
|
+
const deployments = deplRaw.items.map((d) => ({
|
|
20
|
+
name: d.metadata.name,
|
|
21
|
+
namespace: d.metadata.namespace,
|
|
22
|
+
replicas: d.spec.replicas,
|
|
23
|
+
image: d.spec.template.spec.containers[0]?.image ?? "unknown",
|
|
24
|
+
}));
|
|
25
|
+
// StatefulSets
|
|
26
|
+
const ssRaw = (await kubectlJson(["get", "statefulsets", "--all-namespaces"], options));
|
|
27
|
+
const statefulsets = ssRaw.items.map((s) => ({
|
|
28
|
+
name: s.metadata.name,
|
|
29
|
+
namespace: s.metadata.namespace,
|
|
30
|
+
replicas: s.spec.replicas,
|
|
31
|
+
image: s.spec.template.spec.containers[0]?.image ?? "unknown",
|
|
32
|
+
}));
|
|
33
|
+
// Services
|
|
34
|
+
const svcRaw = (await kubectlJson(["get", "services", "--all-namespaces"], options));
|
|
35
|
+
const services = svcRaw.items.map((s) => ({
|
|
36
|
+
name: s.metadata.name,
|
|
37
|
+
namespace: s.metadata.namespace,
|
|
38
|
+
type: s.spec.type,
|
|
39
|
+
ports: (s.spec.ports ?? []).map((p) => `${p.port}/${p.protocol}`).join(", "),
|
|
40
|
+
}));
|
|
41
|
+
// Ingresses
|
|
42
|
+
const ingRaw = (await kubectlJson(["get", "ingresses", "--all-namespaces"], options));
|
|
43
|
+
const ingresses = ingRaw.items.map((i) => ({
|
|
44
|
+
name: i.metadata.name,
|
|
45
|
+
namespace: i.metadata.namespace,
|
|
46
|
+
hosts: (i.spec.rules ?? []).map((r) => r.host),
|
|
47
|
+
tls: (i.spec.tls?.length ?? 0) > 0,
|
|
48
|
+
}));
|
|
49
|
+
return { context, nodes, namespaces, deployments, statefulsets, services, ingresses };
|
|
50
|
+
}
|
|
51
|
+
export function formatClusterMarkdown(info) {
|
|
52
|
+
const lines = [];
|
|
53
|
+
lines.push(`# Cluster: ${info.context}`, "");
|
|
54
|
+
// Nodes
|
|
55
|
+
lines.push("## Nodes", "");
|
|
56
|
+
lines.push("| Name | Roles | Version | OS |");
|
|
57
|
+
lines.push("|------|-------|---------|-----|");
|
|
58
|
+
for (const n of info.nodes) {
|
|
59
|
+
lines.push(`| ${n.name} | ${n.roles.join(", ")} | ${n.version} | ${n.os} |`);
|
|
60
|
+
}
|
|
61
|
+
lines.push("");
|
|
62
|
+
// Namespaces
|
|
63
|
+
lines.push("## Namespaces", "");
|
|
64
|
+
lines.push(info.namespaces.map((n) => `- \`${n}\``).join("\n"));
|
|
65
|
+
lines.push("");
|
|
66
|
+
// Deployments
|
|
67
|
+
lines.push("## Deployments", "");
|
|
68
|
+
lines.push("| Name | Namespace | Replicas | Image |");
|
|
69
|
+
lines.push("|------|-----------|----------|-------|");
|
|
70
|
+
for (const d of info.deployments) {
|
|
71
|
+
lines.push(`| ${d.name} | ${d.namespace} | ${d.replicas} | \`${d.image}\` |`);
|
|
72
|
+
}
|
|
73
|
+
lines.push("");
|
|
74
|
+
// StatefulSets
|
|
75
|
+
if (info.statefulsets.length > 0) {
|
|
76
|
+
lines.push("## StatefulSets", "");
|
|
77
|
+
lines.push("| Name | Namespace | Replicas | Image |");
|
|
78
|
+
lines.push("|------|-----------|----------|-------|");
|
|
79
|
+
for (const s of info.statefulsets) {
|
|
80
|
+
lines.push(`| ${s.name} | ${s.namespace} | ${s.replicas} | \`${s.image}\` |`);
|
|
81
|
+
}
|
|
82
|
+
lines.push("");
|
|
83
|
+
}
|
|
84
|
+
// Services
|
|
85
|
+
lines.push("## Services", "");
|
|
86
|
+
lines.push("| Name | Namespace | Type | Ports |");
|
|
87
|
+
lines.push("|------|-----------|------|-------|");
|
|
88
|
+
for (const s of info.services) {
|
|
89
|
+
lines.push(`| ${s.name} | ${s.namespace} | ${s.type} | ${s.ports} |`);
|
|
90
|
+
}
|
|
91
|
+
lines.push("");
|
|
92
|
+
// Ingresses
|
|
93
|
+
if (info.ingresses.length > 0) {
|
|
94
|
+
lines.push("## Ingresses", "");
|
|
95
|
+
lines.push("| Name | Namespace | Hosts | TLS |");
|
|
96
|
+
lines.push("|------|-----------|-------|-----|");
|
|
97
|
+
for (const i of info.ingresses) {
|
|
98
|
+
lines.push(`| ${i.name} | ${i.namespace} | ${i.hosts.join(", ")} | ${i.tls ? "Yes" : "No"} |`);
|
|
99
|
+
}
|
|
100
|
+
lines.push("");
|
|
101
|
+
}
|
|
102
|
+
return lines.join("\n");
|
|
103
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface TechStack {
|
|
2
|
+
language: string;
|
|
3
|
+
framework: string;
|
|
4
|
+
runtimeVersion: string;
|
|
5
|
+
dependencies: string[];
|
|
6
|
+
buildTool: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function detectTechStack(codePath: string): TechStack;
|
|
9
|
+
export declare function formatProjectMarkdown(projectName: string, codePath: string, stack: TechStack, extraNotes?: string): string;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export function detectTechStack(codePath) {
|
|
4
|
+
const stack = {
|
|
5
|
+
language: "Unknown",
|
|
6
|
+
framework: "Unknown",
|
|
7
|
+
runtimeVersion: "Unknown",
|
|
8
|
+
dependencies: [],
|
|
9
|
+
buildTool: "Unknown",
|
|
10
|
+
};
|
|
11
|
+
// Check composer.json (PHP)
|
|
12
|
+
const composerPath = join(codePath, "composer.json");
|
|
13
|
+
if (existsSync(composerPath)) {
|
|
14
|
+
try {
|
|
15
|
+
const composer = JSON.parse(readFileSync(composerPath, "utf-8"));
|
|
16
|
+
stack.language = "PHP";
|
|
17
|
+
stack.runtimeVersion = composer.require?.php ?? "Unknown";
|
|
18
|
+
const deps = { ...composer.require, ...composer["require-dev"] };
|
|
19
|
+
stack.dependencies = Object.keys(deps).slice(0, 20);
|
|
20
|
+
if (deps["laravel/framework"])
|
|
21
|
+
stack.framework = "Laravel";
|
|
22
|
+
else if (deps["symfony/framework-bundle"])
|
|
23
|
+
stack.framework = "Symfony";
|
|
24
|
+
else if (deps["yiisoft/yii"])
|
|
25
|
+
stack.framework = "Yii";
|
|
26
|
+
stack.buildTool = "Composer";
|
|
27
|
+
}
|
|
28
|
+
catch { /* ignore parse errors */ }
|
|
29
|
+
return stack;
|
|
30
|
+
}
|
|
31
|
+
// Check package.json (Node.js)
|
|
32
|
+
const packagePath = join(codePath, "package.json");
|
|
33
|
+
if (existsSync(packagePath)) {
|
|
34
|
+
try {
|
|
35
|
+
const pkg = JSON.parse(readFileSync(packagePath, "utf-8"));
|
|
36
|
+
stack.language = "TypeScript/JavaScript";
|
|
37
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
38
|
+
stack.dependencies = Object.keys(deps).slice(0, 20);
|
|
39
|
+
if (deps.next)
|
|
40
|
+
stack.framework = "Next.js";
|
|
41
|
+
else if (deps.nuxt)
|
|
42
|
+
stack.framework = "Nuxt";
|
|
43
|
+
else if (deps.react)
|
|
44
|
+
stack.framework = "React";
|
|
45
|
+
else if (deps.vue)
|
|
46
|
+
stack.framework = "Vue";
|
|
47
|
+
else if (deps.express)
|
|
48
|
+
stack.framework = "Express";
|
|
49
|
+
if (existsSync(join(codePath, "tsconfig.json"))) {
|
|
50
|
+
stack.language = "TypeScript";
|
|
51
|
+
}
|
|
52
|
+
stack.runtimeVersion = pkg.engines?.node ?? "Unknown";
|
|
53
|
+
stack.buildTool = deps.vite ? "Vite" : deps.webpack ? "Webpack" : "npm";
|
|
54
|
+
}
|
|
55
|
+
catch { /* ignore */ }
|
|
56
|
+
return stack;
|
|
57
|
+
}
|
|
58
|
+
// Check go.mod (Go)
|
|
59
|
+
const goModPath = join(codePath, "go.mod");
|
|
60
|
+
if (existsSync(goModPath)) {
|
|
61
|
+
const goMod = readFileSync(goModPath, "utf-8");
|
|
62
|
+
stack.language = "Go";
|
|
63
|
+
stack.buildTool = "go";
|
|
64
|
+
const goVersionMatch = goMod.match(/^go\s+(\S+)/m);
|
|
65
|
+
if (goVersionMatch)
|
|
66
|
+
stack.runtimeVersion = goVersionMatch[1];
|
|
67
|
+
const moduleMatch = goMod.match(/^module\s+(\S+)/m);
|
|
68
|
+
if (moduleMatch)
|
|
69
|
+
stack.dependencies.push(moduleMatch[1]);
|
|
70
|
+
const requireMatches = goMod.matchAll(/^\s+(\S+)\s+\S+/gm);
|
|
71
|
+
for (const m of requireMatches) {
|
|
72
|
+
if (m[1] && !m[1].startsWith("//")) {
|
|
73
|
+
stack.dependencies.push(m[1]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
stack.dependencies = stack.dependencies.slice(0, 20);
|
|
77
|
+
stack.framework = "stdlib";
|
|
78
|
+
return stack;
|
|
79
|
+
}
|
|
80
|
+
return stack;
|
|
81
|
+
}
|
|
82
|
+
export function formatProjectMarkdown(projectName, codePath, stack, extraNotes) {
|
|
83
|
+
const lines = [];
|
|
84
|
+
lines.push(`# ${projectName}`, "");
|
|
85
|
+
lines.push(`**Code path:** \`${codePath}\``);
|
|
86
|
+
lines.push(`**Language:** ${stack.language}`);
|
|
87
|
+
lines.push(`**Framework:** ${stack.framework}`);
|
|
88
|
+
lines.push(`**Runtime version:** ${stack.runtimeVersion}`);
|
|
89
|
+
lines.push(`**Build tool:** ${stack.buildTool}`);
|
|
90
|
+
lines.push("");
|
|
91
|
+
if (stack.dependencies.length > 0) {
|
|
92
|
+
lines.push("## Key Dependencies", "");
|
|
93
|
+
for (const dep of stack.dependencies) {
|
|
94
|
+
lines.push(`- \`${dep}\``);
|
|
95
|
+
}
|
|
96
|
+
lines.push("");
|
|
97
|
+
}
|
|
98
|
+
// Check for Dockerfile
|
|
99
|
+
if (existsSync(join(codePath, "Dockerfile"))) {
|
|
100
|
+
const dockerfile = readFileSync(join(codePath, "Dockerfile"), "utf-8");
|
|
101
|
+
const fromMatch = dockerfile.match(/^FROM\s+(\S+)/m);
|
|
102
|
+
if (fromMatch) {
|
|
103
|
+
lines.push(`## Docker`, "");
|
|
104
|
+
lines.push(`Base image: \`${fromMatch[1]}\``);
|
|
105
|
+
lines.push("");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (extraNotes) {
|
|
109
|
+
lines.push("## Notes", "");
|
|
110
|
+
lines.push(extraNotes);
|
|
111
|
+
lines.push("");
|
|
112
|
+
}
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function onboard(): Promise<void>;
|