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 };