Spaces:
Runtime error
Runtime error
File size: 4,372 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | /**
* Plugin doctor β diagnostic tool for plugin health checks.
*
* Runs 5 checks: directory exists, manifest valid, entry point exists,
* can spawn, DB status matches filesystem.
*
* @module plugins/doctor
*/
import { stat } from "fs/promises";
import { join } from "path";
import { safeValidateManifest } from "./manifest";
import { getPluginByName } from "../db/plugins";
import { readFile } from "fs/promises";
import { logger } from "../../../open-sse/utils/logger.ts";
const log = logger("PLUGIN_DOCTOR");
export interface DoctorCheck {
name: string;
status: "pass" | "fail" | "warn";
message?: string;
}
export interface DoctorResult {
pluginName: string;
checks: DoctorCheck[];
overall: "healthy" | "degraded" | "unhealthy";
}
/**
* Run diagnostic checks on a plugin.
*/
export async function runPluginDoctor(pluginDir: string, pluginName: string): Promise<DoctorResult> {
const checks: DoctorCheck[] = [];
// Check 1: directory_exists
try {
const dirStat = await stat(pluginDir);
checks.push({
name: "directory_exists",
status: dirStat.isDirectory() ? "pass" : "fail",
message: dirStat.isDirectory() ? undefined : "Path is not a directory",
});
} catch {
checks.push({ name: "directory_exists", status: "fail", message: "Directory not found" });
}
// Check 2: manifest_valid
const manifestPath = join(pluginDir, "plugin.json");
try {
const raw = await readFile(manifestPath, "utf-8");
const parsed = JSON.parse(raw);
const result = safeValidateManifest(parsed);
checks.push({
name: "manifest_valid",
status: result.success ? "pass" : "fail",
message: result.success ? undefined : (result as { success: false; errors: string[] }).errors.join("; "),
});
} catch (err: unknown) {
checks.push({
name: "manifest_valid",
status: "fail",
message: `Cannot read manifest: ${err instanceof Error ? err.message : String(err)}`,
});
}
// Check 3: entry_point_exists
try {
const raw = await readFile(manifestPath, "utf-8");
const parsed = JSON.parse(raw);
const result = safeValidateManifest(parsed);
if (result.success) {
const entryPoint = join(pluginDir, result.data.main);
try {
await stat(entryPoint);
checks.push({ name: "entry_point_exists", status: "pass" });
} catch {
checks.push({ name: "entry_point_exists", status: "fail", message: `Entry point not found: ${result.data.main}` });
}
} else {
checks.push({ name: "entry_point_exists", status: "warn", message: "Skipped β manifest invalid" });
}
} catch {
checks.push({ name: "entry_point_exists", status: "warn", message: "Skipped β manifest unreadable" });
}
// Check 4: can_spawn (simplified β check entry point is .js/.mjs)
try {
const raw = await readFile(manifestPath, "utf-8");
const parsed = JSON.parse(raw);
const result = safeValidateManifest(parsed);
if (result.success) {
const main = result.data.main;
const ext = main.split(".").pop();
checks.push({
name: "can_spawn",
status: ext === "js" || ext === "mjs" ? "pass" : "warn",
message: ext === "js" || ext === "mjs" ? undefined : `Unexpected extension: .${ext}`,
});
} else {
checks.push({ name: "can_spawn", status: "warn", message: "Skipped β manifest invalid" });
}
} catch {
checks.push({ name: "can_spawn", status: "warn", message: "Skipped β manifest unreadable" });
}
// Check 5: db_status_correct
const dbRow = getPluginByName(pluginName);
if (dbRow) {
checks.push({
name: "db_status_correct",
status: "pass",
message: `Status: ${dbRow.status}`,
});
} else {
checks.push({
name: "db_status_correct",
status: "warn",
message: "Plugin not in database",
});
}
// Overall
const failCount = checks.filter((c) => c.status === "fail").length;
const warnCount = checks.filter((c) => c.status === "warn").length;
let overall: DoctorResult["overall"];
if (failCount === 0 && warnCount === 0) {
overall = "healthy";
} else if (failCount === 0) {
overall = "degraded";
} else {
overall = "unhealthy";
}
log.info("doctor.result", { pluginName, overall, failCount, warnCount });
return { pluginName, checks, overall };
}
|