140 lines
5.1 KiB
JavaScript
140 lines
5.1 KiB
JavaScript
const fs = require("fs");
|
|
const path = require("path");
|
|
const { compareVersions } = require("./tool_loader");
|
|
|
|
class ToolManager {
|
|
constructor(options = {}) {
|
|
this.repoClient = options.repoClient;
|
|
this.installer = options.installer;
|
|
this.loader = options.loader;
|
|
}
|
|
|
|
async list({ force = false } = {}) {
|
|
const remoteResult = await this.repoClient.discover({ force });
|
|
const localRows = this.installer.scanLocal();
|
|
const remoteMap = new Map(remoteResult.tools.map((metadata) => [metadata.tool_id, metadata]));
|
|
const localMap = new Map(localRows.map((entry) => [entry.tool_id, entry]));
|
|
const ids = [...new Set([...remoteMap.keys(), ...localMap.keys()])].sort();
|
|
return {
|
|
repository: remoteResult.repository,
|
|
branch: remoteResult.branch,
|
|
checked_at: remoteResult.checked_at,
|
|
cached: remoteResult.cached,
|
|
stale: remoteResult.stale,
|
|
error: remoteResult.error || null,
|
|
tools: ids.map((toolId) => {
|
|
const remote = remoteMap.get(toolId) || null;
|
|
const local = localMap.get(toolId) || null;
|
|
const metadata = local?.metadata || remote;
|
|
const installed = Boolean(local);
|
|
const remoteMissing = installed && !remote;
|
|
const enabled = installed && this.loader.isEnabled(toolId);
|
|
const runtime = this.loader.status(toolId);
|
|
const updateAvailable = Boolean(
|
|
installed && remote && local.valid && !remote.remote_invalid &&
|
|
compareVersions(remote.version, local.metadata.version) > 0
|
|
);
|
|
const dependencies = local?.valid ? this.loader.inspectDependencies(local.metadata, local.dir) : { blocking: [], optional: [] };
|
|
return {
|
|
...metadata,
|
|
installed,
|
|
enabled,
|
|
local_version: installed ? local.metadata.version : null,
|
|
remote_version: remote?.version || null,
|
|
update_available: updateAvailable,
|
|
update_enabled: installed && Boolean(remote) && !remote?.remote_invalid,
|
|
remote_missing: remoteMissing,
|
|
local_only: remoteMissing,
|
|
local_valid: local?.valid !== false,
|
|
local_error: local?.error || null,
|
|
remote_invalid: remote?.remote_invalid === true,
|
|
remote_error: remote?.remote_error || null,
|
|
runtime_state: runtime.state,
|
|
runtime_message: runtime.message,
|
|
dependency_status: dependencies,
|
|
primary_type: metadata?.tool_type || "general",
|
|
primary_scope: displayScope(metadata?.scope)
|
|
};
|
|
})
|
|
};
|
|
}
|
|
|
|
async enable(toolId) {
|
|
let installed = this.installer.local(toolId);
|
|
if (!installed) {
|
|
const remote = await this.repoClient.discover();
|
|
if (!remote.tools.some((entry) => entry.tool_id === toolId && !entry.remote_invalid)) {
|
|
throw new Error("Remote AI tool plugin is unavailable.");
|
|
}
|
|
await this.installer.install(toolId);
|
|
installed = this.installer.local(toolId);
|
|
}
|
|
return this.loader.enable(toolId);
|
|
}
|
|
|
|
async disable(toolId) {
|
|
return this.loader.disable(toolId);
|
|
}
|
|
|
|
async update(toolId) {
|
|
const local = this.installer.local(toolId);
|
|
if (!local) throw new Error("Install the AI tool plugin before updating it.");
|
|
const remote = await this.repoClient.discover({ force: true });
|
|
if (!remote.tools.some((entry) => entry.tool_id === toolId && !entry.remote_invalid)) {
|
|
throw new Error("This installed tool is missing from the configured repository.");
|
|
}
|
|
const wasEnabled = this.loader.isEnabled(toolId);
|
|
if (wasEnabled) await this.loader.disable(toolId, { persist: false });
|
|
try {
|
|
const result = await this.installer.update(toolId);
|
|
if (wasEnabled) await this.loader.enable(toolId, { persist: false });
|
|
this.loader.setEnabled(toolId, wasEnabled);
|
|
return result;
|
|
} catch (error) {
|
|
if (wasEnabled && this.installer.local(toolId)) {
|
|
try { await this.loader.enable(toolId, { persist: false }); } catch {}
|
|
this.loader.setEnabled(toolId, true);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async delete(toolId) {
|
|
await this.loader.disable(toolId);
|
|
const deleted = this.installer.delete(toolId);
|
|
this.loader.setEnabled(toolId, false);
|
|
return { deleted };
|
|
}
|
|
|
|
async readme(toolId) {
|
|
const local = this.installer.local(toolId);
|
|
if (local) {
|
|
const file = path.join(local.dir, "readme.md");
|
|
if (!fs.existsSync(file)) throw new Error("Installed tool readme.md is missing.");
|
|
return { markdown: fs.readFileSync(file, "utf8"), source: "local" };
|
|
}
|
|
return { markdown: await this.repoClient.readReadme(toolId), source: "remote" };
|
|
}
|
|
|
|
async loadEnabled() {
|
|
return this.loader.loadEnabled();
|
|
}
|
|
|
|
async stopAll() {
|
|
return this.loader.stopAll();
|
|
}
|
|
|
|
resolveAsset(toolId, relative) {
|
|
return this.loader.resolveAsset(toolId, relative);
|
|
}
|
|
}
|
|
|
|
function displayScope(scope) {
|
|
if (typeof scope === "string") return scope;
|
|
if (Array.isArray(scope)) return scope.join(", ");
|
|
if (scope && typeof scope === "object") return scope.label || scope.required_role || JSON.stringify(scope);
|
|
return "unspecified";
|
|
}
|
|
|
|
module.exports = { ToolManager, displayScope };
|