@prajwolkc/stk 0.1.1 → 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 +112 -5
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +220 -0
- package/dist/commands/health.js +29 -4
- package/dist/commands/logs.js +202 -97
- package/dist/index.js +3 -1
- package/dist/lib/output.d.ts +7 -0
- package/dist/lib/output.js +14 -0
- package/dist/lib/plugins.d.ts +40 -0
- package/dist/lib/plugins.js +65 -0
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.js +385 -0
- package/dist/services/aws.js +39 -10
- package/dist/services/database.js +25 -6
- package/dist/services/registry.d.ts +1 -0
- package/dist/services/registry.js +16 -2
- package/package.json +10 -4
package/dist/commands/logs.js
CHANGED
|
@@ -1,142 +1,246 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import ora from "ora";
|
|
4
|
+
import { loadConfig, enabledServices } from "../lib/config.js";
|
|
4
5
|
export const logsCommand = new Command("logs")
|
|
5
|
-
.description("Tail
|
|
6
|
+
.description("Tail logs from your deploy providers")
|
|
6
7
|
.option("-n, --lines <count>", "number of recent lines to show", "50")
|
|
7
8
|
.option("-f, --follow", "keep streaming new logs")
|
|
9
|
+
.option("-p, --provider <name>", "specific provider (railway, vercel, fly, render)")
|
|
8
10
|
.action(async (opts) => {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
const config = loadConfig();
|
|
12
|
+
const enabled = enabledServices(config);
|
|
13
|
+
const limit = parseInt(opts.lines, 10);
|
|
14
|
+
// Build list of available log providers
|
|
15
|
+
const providers = [];
|
|
16
|
+
if ((opts.provider === "railway" || (!opts.provider && enabled.includes("railway"))) &&
|
|
17
|
+
process.env.RAILWAY_API_TOKEN) {
|
|
18
|
+
providers.push(railwayProvider());
|
|
19
|
+
}
|
|
20
|
+
if ((opts.provider === "vercel" || (!opts.provider && enabled.includes("vercel"))) &&
|
|
21
|
+
process.env.VERCEL_TOKEN) {
|
|
22
|
+
providers.push(vercelProvider());
|
|
23
|
+
}
|
|
24
|
+
if ((opts.provider === "fly" || (!opts.provider && enabled.includes("fly"))) &&
|
|
25
|
+
process.env.FLY_API_TOKEN) {
|
|
26
|
+
providers.push(flyProvider());
|
|
27
|
+
}
|
|
28
|
+
if ((opts.provider === "render" || (!opts.provider && enabled.includes("render"))) &&
|
|
29
|
+
process.env.RENDER_API_KEY) {
|
|
30
|
+
providers.push(renderProvider());
|
|
31
|
+
}
|
|
32
|
+
if (providers.length === 0) {
|
|
33
|
+
console.log(chalk.red("\n No log providers available."));
|
|
34
|
+
console.log(chalk.dim(" Configure railway, vercel, fly, or render in stk.config.json and set their tokens.\n"));
|
|
12
35
|
process.exitCode = 1;
|
|
13
36
|
return;
|
|
14
37
|
}
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
if (!deploymentId) {
|
|
19
|
-
// Try to get latest deployment
|
|
20
|
-
const spinner = ora("Fetching latest deployment...").start();
|
|
38
|
+
// Fetch logs from all providers
|
|
39
|
+
for (const provider of providers) {
|
|
40
|
+
const spinner = ora(`Fetching ${provider.name} logs...`).start();
|
|
21
41
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
await followLogs(token, latestDeploymentId);
|
|
28
|
-
}
|
|
42
|
+
const logs = await provider.fetch(limit);
|
|
43
|
+
spinner.stop();
|
|
44
|
+
if (logs.length === 0) {
|
|
45
|
+
console.log(chalk.dim(`\n ${provider.name}: no logs found\n`));
|
|
46
|
+
continue;
|
|
29
47
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
48
|
+
console.log();
|
|
49
|
+
console.log(chalk.bold(` ${provider.name} Logs`));
|
|
50
|
+
console.log(chalk.dim(" ─────────────────────────────────────────"));
|
|
51
|
+
for (const log of logs) {
|
|
52
|
+
printLogLine(log, provider.name);
|
|
33
53
|
}
|
|
34
54
|
}
|
|
35
55
|
catch (err) {
|
|
36
|
-
spinner.fail(err.message);
|
|
56
|
+
spinner.fail(`${provider.name}: ${err.message}`);
|
|
37
57
|
}
|
|
38
|
-
return;
|
|
39
58
|
}
|
|
40
|
-
|
|
41
|
-
if (opts.follow) {
|
|
42
|
-
|
|
59
|
+
// Follow mode — stream from first available provider
|
|
60
|
+
if (opts.follow && providers[0]) {
|
|
61
|
+
console.log(chalk.dim("\n --- streaming (Ctrl+C to stop) ---\n"));
|
|
62
|
+
const provider = providers[0];
|
|
63
|
+
if (provider.follow) {
|
|
64
|
+
await provider.follow((log) => printLogLine(log, provider.name));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
console.log(chalk.dim(` ${provider.name} does not support streaming`));
|
|
68
|
+
}
|
|
43
69
|
}
|
|
70
|
+
console.log();
|
|
44
71
|
});
|
|
45
|
-
|
|
72
|
+
// --- Railway ---
|
|
73
|
+
function railwayProvider() {
|
|
74
|
+
const token = process.env.RAILWAY_API_TOKEN;
|
|
75
|
+
const projectId = process.env.RAILWAY_PROJECT_ID;
|
|
76
|
+
const serviceId = process.env.RAILWAY_SERVICE_ID;
|
|
77
|
+
return {
|
|
78
|
+
name: "Railway",
|
|
79
|
+
async fetch(limit) {
|
|
80
|
+
const deploymentId = await getLatestRailwayDeployment(token, projectId, serviceId);
|
|
81
|
+
if (!deploymentId)
|
|
82
|
+
return [];
|
|
83
|
+
const res = await fetch("https://backboard.railway.com/graphql/v2", {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
Authorization: `Bearer ${token}`,
|
|
87
|
+
"Content-Type": "application/json",
|
|
88
|
+
},
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
query: `{ deploymentLogs(deploymentId: "${deploymentId}", limit: ${limit}) { timestamp message severity } }`,
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
const data = (await res.json());
|
|
94
|
+
return data.data?.deploymentLogs ?? [];
|
|
95
|
+
},
|
|
96
|
+
async follow(onLog) {
|
|
97
|
+
const deploymentId = await getLatestRailwayDeployment(token, projectId, serviceId);
|
|
98
|
+
if (!deploymentId)
|
|
99
|
+
return;
|
|
100
|
+
let lastTimestamp = new Date().toISOString();
|
|
101
|
+
while (true) {
|
|
102
|
+
await sleep(3000);
|
|
103
|
+
try {
|
|
104
|
+
const res = await fetch("https://backboard.railway.com/graphql/v2", {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: {
|
|
107
|
+
Authorization: `Bearer ${token}`,
|
|
108
|
+
"Content-Type": "application/json",
|
|
109
|
+
},
|
|
110
|
+
body: JSON.stringify({
|
|
111
|
+
query: `{ deploymentLogs(deploymentId: "${deploymentId}", limit: 20) { timestamp message severity } }`,
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
const data = (await res.json());
|
|
115
|
+
for (const log of data.data?.deploymentLogs ?? []) {
|
|
116
|
+
if (log.timestamp > lastTimestamp) {
|
|
117
|
+
onLog(log);
|
|
118
|
+
lastTimestamp = log.timestamp;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch { /* retry */ }
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async function getLatestRailwayDeployment(token, projectId, serviceId) {
|
|
46
128
|
if (!projectId)
|
|
47
129
|
return null;
|
|
48
|
-
const serviceFilter = serviceId
|
|
49
|
-
? `serviceId: "${serviceId}",`
|
|
50
|
-
: "";
|
|
130
|
+
const serviceFilter = serviceId ? `serviceId: "${serviceId}",` : "";
|
|
51
131
|
const res = await fetch("https://backboard.railway.com/graphql/v2", {
|
|
52
132
|
method: "POST",
|
|
53
|
-
headers: {
|
|
54
|
-
Authorization: `Bearer ${token}`,
|
|
55
|
-
"Content-Type": "application/json",
|
|
56
|
-
},
|
|
133
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
57
134
|
body: JSON.stringify({
|
|
58
|
-
query: `{
|
|
59
|
-
deployments(
|
|
60
|
-
first: 1,
|
|
61
|
-
input: {
|
|
62
|
-
projectId: "${projectId}",
|
|
63
|
-
${serviceFilter}
|
|
64
|
-
}
|
|
65
|
-
) {
|
|
66
|
-
edges {
|
|
67
|
-
node { id status }
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}`,
|
|
135
|
+
query: `{ deployments(first: 1, input: { projectId: "${projectId}", ${serviceFilter} }) { edges { node { id } } } }`,
|
|
71
136
|
}),
|
|
72
137
|
});
|
|
73
138
|
const data = (await res.json());
|
|
74
139
|
return data.data?.deployments?.edges?.[0]?.node?.id ?? null;
|
|
75
140
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
141
|
+
// --- Vercel ---
|
|
142
|
+
function vercelProvider() {
|
|
143
|
+
const token = process.env.VERCEL_TOKEN;
|
|
144
|
+
return {
|
|
145
|
+
name: "Vercel",
|
|
146
|
+
async fetch(limit) {
|
|
147
|
+
// Get latest deployment, then its build logs
|
|
148
|
+
const depRes = await fetch("https://api.vercel.com/v6/deployments?limit=1", {
|
|
149
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
150
|
+
});
|
|
151
|
+
const depData = (await depRes.json());
|
|
152
|
+
const dep = depData.deployments?.[0];
|
|
153
|
+
if (!dep)
|
|
154
|
+
return [];
|
|
155
|
+
const logRes = await fetch(`https://api.vercel.com/v2/deployments/${dep.uid}/events`, { headers: { Authorization: `Bearer ${token}` } });
|
|
156
|
+
const events = (await logRes.json());
|
|
157
|
+
if (!Array.isArray(events))
|
|
158
|
+
return [];
|
|
159
|
+
return events
|
|
160
|
+
.filter((e) => e.type === "stdout" || e.type === "stderr")
|
|
161
|
+
.slice(-limit)
|
|
162
|
+
.map((e) => ({
|
|
163
|
+
timestamp: new Date(e.created).toISOString(),
|
|
164
|
+
message: e.payload?.text ?? e.text ?? "",
|
|
165
|
+
severity: e.type === "stderr" ? "ERROR" : "INFO",
|
|
166
|
+
}));
|
|
82
167
|
},
|
|
83
|
-
|
|
84
|
-
query: `{
|
|
85
|
-
deploymentLogs(deploymentId: "${deploymentId}", limit: ${limit}) {
|
|
86
|
-
timestamp
|
|
87
|
-
message
|
|
88
|
-
severity
|
|
89
|
-
}
|
|
90
|
-
}`,
|
|
91
|
-
}),
|
|
92
|
-
});
|
|
93
|
-
const data = (await res.json());
|
|
94
|
-
const logs = data.data?.deploymentLogs ?? [];
|
|
95
|
-
if (logs.length === 0) {
|
|
96
|
-
console.log(chalk.dim(" No logs found"));
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
for (const log of logs) {
|
|
100
|
-
printLogLine(log);
|
|
101
|
-
}
|
|
168
|
+
};
|
|
102
169
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
170
|
+
// --- Fly.io ---
|
|
171
|
+
function flyProvider() {
|
|
172
|
+
const token = process.env.FLY_API_TOKEN;
|
|
173
|
+
const app = process.env.FLY_APP_NAME;
|
|
174
|
+
return {
|
|
175
|
+
name: "Fly.io",
|
|
176
|
+
async fetch(limit) {
|
|
177
|
+
if (!app) {
|
|
178
|
+
throw new Error("FLY_APP_NAME not set");
|
|
179
|
+
}
|
|
180
|
+
const res = await fetch("https://api.fly.io/graphql", {
|
|
110
181
|
method: "POST",
|
|
111
182
|
headers: {
|
|
112
183
|
Authorization: `Bearer ${token}`,
|
|
113
184
|
"Content-Type": "application/json",
|
|
114
185
|
},
|
|
115
186
|
body: JSON.stringify({
|
|
116
|
-
query: `{
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
message
|
|
120
|
-
severity
|
|
187
|
+
query: `query {
|
|
188
|
+
app(name: "${app}") {
|
|
189
|
+
currentRelease { status createdAt }
|
|
121
190
|
}
|
|
122
191
|
}`,
|
|
123
192
|
}),
|
|
124
193
|
});
|
|
125
194
|
const data = (await res.json());
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
195
|
+
const release = data.data?.app?.currentRelease;
|
|
196
|
+
if (!release)
|
|
197
|
+
return [];
|
|
198
|
+
// Fly doesn't have a simple log fetch API via GraphQL
|
|
199
|
+
// Return release info as a log line
|
|
200
|
+
return [
|
|
201
|
+
{
|
|
202
|
+
timestamp: release.createdAt,
|
|
203
|
+
message: `Current release: ${release.status}`,
|
|
204
|
+
severity: "INFO",
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
// --- Render ---
|
|
211
|
+
function renderProvider() {
|
|
212
|
+
const token = process.env.RENDER_API_KEY;
|
|
213
|
+
return {
|
|
214
|
+
name: "Render",
|
|
215
|
+
async fetch(logLimit) {
|
|
216
|
+
// Get first service
|
|
217
|
+
const svcRes = await fetch("https://api.render.com/v1/services?limit=1", {
|
|
218
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
219
|
+
});
|
|
220
|
+
const services = (await svcRes.json());
|
|
221
|
+
const svc = services[0]?.service;
|
|
222
|
+
if (!svc)
|
|
223
|
+
return [];
|
|
224
|
+
// Get deploys for the service
|
|
225
|
+
const depRes = await fetch(`https://api.render.com/v1/services/${svc.id}/deploys?limit=1`, { headers: { Authorization: `Bearer ${token}` } });
|
|
226
|
+
const deploys = (await depRes.json());
|
|
227
|
+
const deploy = deploys[0]?.deploy;
|
|
228
|
+
if (!deploy)
|
|
229
|
+
return [];
|
|
230
|
+
// Get logs for the deploy
|
|
231
|
+
const logRes = await fetch(`https://api.render.com/v1/services/${svc.id}/deploys/${deploy.id}/logs`, { headers: { Authorization: `Bearer ${token}` } });
|
|
232
|
+
const logs = (await logRes.json());
|
|
233
|
+
if (!Array.isArray(logs))
|
|
234
|
+
return [];
|
|
235
|
+
return logs.slice(-logLimit).map((l) => ({
|
|
236
|
+
timestamp: l.timestamp ?? new Date().toISOString(),
|
|
237
|
+
message: l.message ?? l.text ?? String(l),
|
|
238
|
+
severity: "INFO",
|
|
239
|
+
}));
|
|
240
|
+
},
|
|
241
|
+
};
|
|
138
242
|
}
|
|
139
|
-
function printLogLine(log) {
|
|
243
|
+
function printLogLine(log, provider) {
|
|
140
244
|
const time = chalk.dim(new Date(log.timestamp).toLocaleTimeString());
|
|
141
245
|
const severity = log.severity?.toUpperCase() ?? "INFO";
|
|
142
246
|
const sevColor = severity === "ERROR"
|
|
@@ -144,7 +248,8 @@ function printLogLine(log) {
|
|
|
144
248
|
: severity === "WARN"
|
|
145
249
|
? chalk.yellow
|
|
146
250
|
: chalk.dim;
|
|
147
|
-
|
|
251
|
+
const tag = chalk.dim(`[${provider.toLowerCase()}]`);
|
|
252
|
+
console.log(` ${time} ${tag} ${sevColor(severity.padEnd(5))} ${log.message}`);
|
|
148
253
|
}
|
|
149
254
|
function sleep(ms) {
|
|
150
255
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
package/dist/index.js
CHANGED
|
@@ -7,11 +7,12 @@ import { deployCommand } from "./commands/deploy.js";
|
|
|
7
7
|
import { envCommand } from "./commands/env.js";
|
|
8
8
|
import { logsCommand } from "./commands/logs.js";
|
|
9
9
|
import { todoCommand } from "./commands/todo.js";
|
|
10
|
+
import { doctorCommand } from "./commands/doctor.js";
|
|
10
11
|
const program = new Command();
|
|
11
12
|
program
|
|
12
13
|
.name("stk")
|
|
13
14
|
.description("One CLI to deploy, monitor, and debug your entire stack.")
|
|
14
|
-
.version("0.
|
|
15
|
+
.version("0.2.0");
|
|
15
16
|
program.addCommand(initCommand);
|
|
16
17
|
program.addCommand(statusCommand);
|
|
17
18
|
program.addCommand(healthCommand);
|
|
@@ -19,4 +20,5 @@ program.addCommand(deployCommand);
|
|
|
19
20
|
program.addCommand(envCommand);
|
|
20
21
|
program.addCommand(logsCommand);
|
|
21
22
|
program.addCommand(todoCommand);
|
|
23
|
+
program.addCommand(doctorCommand);
|
|
22
24
|
program.parse();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared output helper. When --json is passed, commands collect data
|
|
3
|
+
* and call jsonOutput() instead of console.log with chalk.
|
|
4
|
+
*/
|
|
5
|
+
export declare function setJsonMode(enabled: boolean): void;
|
|
6
|
+
export declare function isJsonMode(): boolean;
|
|
7
|
+
export declare function jsonOutput(data: unknown): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared output helper. When --json is passed, commands collect data
|
|
3
|
+
* and call jsonOutput() instead of console.log with chalk.
|
|
4
|
+
*/
|
|
5
|
+
let jsonMode = false;
|
|
6
|
+
export function setJsonMode(enabled) {
|
|
7
|
+
jsonMode = enabled;
|
|
8
|
+
}
|
|
9
|
+
export function isJsonMode() {
|
|
10
|
+
return jsonMode;
|
|
11
|
+
}
|
|
12
|
+
export function jsonOutput(data) {
|
|
13
|
+
console.log(JSON.stringify(data, null, 2));
|
|
14
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { CheckResult } from "../services/checker.js";
|
|
2
|
+
export interface StkPlugin {
|
|
3
|
+
name: string;
|
|
4
|
+
version?: string;
|
|
5
|
+
services?: Record<string, PluginService>;
|
|
6
|
+
}
|
|
7
|
+
export interface PluginService {
|
|
8
|
+
name: string;
|
|
9
|
+
envVars: string[];
|
|
10
|
+
healthCheck: () => Promise<CheckResult>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Load plugins from .stk/plugins/ directory.
|
|
14
|
+
*
|
|
15
|
+
* Each plugin is a .js or .mjs file that exports a StkPlugin:
|
|
16
|
+
*
|
|
17
|
+
* ```js
|
|
18
|
+
* // .stk/plugins/my-service.mjs
|
|
19
|
+
* export default {
|
|
20
|
+
* name: "my-plugin",
|
|
21
|
+
* services: {
|
|
22
|
+
* myservice: {
|
|
23
|
+
* name: "My Service",
|
|
24
|
+
* envVars: ["MY_SERVICE_TOKEN"],
|
|
25
|
+
* healthCheck: async () => {
|
|
26
|
+
* const token = process.env.MY_SERVICE_TOKEN;
|
|
27
|
+
* if (!token) return { name: "My Service", status: "skipped", detail: "MY_SERVICE_TOKEN not set" };
|
|
28
|
+
* // ... check logic
|
|
29
|
+
* return { name: "My Service", status: "healthy", detail: "connected" };
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
* };
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare function loadPlugins(): Promise<StkPlugin[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Collect all plugin health checkers into a flat record.
|
|
39
|
+
*/
|
|
40
|
+
export declare function getPluginCheckers(): Promise<Record<string, () => Promise<CheckResult>>>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "fs";
|
|
2
|
+
import { resolve, join } from "path";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
const PLUGIN_DIR = ".stk/plugins";
|
|
5
|
+
/**
|
|
6
|
+
* Load plugins from .stk/plugins/ directory.
|
|
7
|
+
*
|
|
8
|
+
* Each plugin is a .js or .mjs file that exports a StkPlugin:
|
|
9
|
+
*
|
|
10
|
+
* ```js
|
|
11
|
+
* // .stk/plugins/my-service.mjs
|
|
12
|
+
* export default {
|
|
13
|
+
* name: "my-plugin",
|
|
14
|
+
* services: {
|
|
15
|
+
* myservice: {
|
|
16
|
+
* name: "My Service",
|
|
17
|
+
* envVars: ["MY_SERVICE_TOKEN"],
|
|
18
|
+
* healthCheck: async () => {
|
|
19
|
+
* const token = process.env.MY_SERVICE_TOKEN;
|
|
20
|
+
* if (!token) return { name: "My Service", status: "skipped", detail: "MY_SERVICE_TOKEN not set" };
|
|
21
|
+
* // ... check logic
|
|
22
|
+
* return { name: "My Service", status: "healthy", detail: "connected" };
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
* };
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export async function loadPlugins() {
|
|
30
|
+
const pluginDir = resolve(process.cwd(), PLUGIN_DIR);
|
|
31
|
+
if (!existsSync(pluginDir))
|
|
32
|
+
return [];
|
|
33
|
+
const plugins = [];
|
|
34
|
+
const files = readdirSync(pluginDir).filter((f) => f.endsWith(".js") || f.endsWith(".mjs"));
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
try {
|
|
37
|
+
const filePath = join(pluginDir, file);
|
|
38
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
39
|
+
const mod = await import(fileUrl);
|
|
40
|
+
const plugin = mod.default ?? mod;
|
|
41
|
+
if (plugin.name && plugin.services) {
|
|
42
|
+
plugins.push(plugin);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Skip invalid plugins silently
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return plugins;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Collect all plugin health checkers into a flat record.
|
|
53
|
+
*/
|
|
54
|
+
export async function getPluginCheckers() {
|
|
55
|
+
const plugins = await loadPlugins();
|
|
56
|
+
const checkers = {};
|
|
57
|
+
for (const plugin of plugins) {
|
|
58
|
+
if (!plugin.services)
|
|
59
|
+
continue;
|
|
60
|
+
for (const [key, svc] of Object.entries(plugin.services)) {
|
|
61
|
+
checkers[key] = svc.healthCheck;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return checkers;
|
|
65
|
+
}
|