fix(plugin): add Instance.provide context and validate cache

- Wrap CLI commands in Instance.provide for proper Config access
- Validate registry cache file against schema before use
- Prevents crash when Config.update() is called without Instance context

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Artur Do Lago
2026-01-11 14:02:37 +01:00
parent 16f52f9b2f
commit 75f723b021
5 changed files with 155 additions and 124 deletions
+58 -52
View File
@@ -2,6 +2,7 @@ import { cmd } from "../cmd"
import { fetchRegistry, getPlugin } from "../../../plugin/registry"
import { getInstalled } from "../../../plugin/manager"
import { UI } from "../../ui"
import { Instance } from "../../../project/instance"
export const InfoCommand = cmd({
command: "info <name>",
@@ -21,62 +22,67 @@ export const InfoCommand = cmd({
async handler(args) {
const { name, json } = args
try {
const registry = await fetchRegistry()
const plugin = getPlugin(registry, name)
const installed = await getInstalled(name)
await Instance.provide({
directory: process.cwd(),
async fn() {
try {
const registry = await fetchRegistry()
const plugin = getPlugin(registry, name)
const installed = await getInstalled(name)
if (!plugin && !installed) {
UI.error(`Plugin "${name}" not found`)
UI.println(UI.Style.TEXT_DIM + "Search for plugins with: agent-core plugin search" + UI.Style.TEXT_NORMAL)
process.exit(1)
}
if (!plugin && !installed) {
UI.error(`Plugin "${name}" not found`)
UI.println(UI.Style.TEXT_DIM + "Search for plugins with: agent-core plugin search" + UI.Style.TEXT_NORMAL)
process.exit(1)
}
const info = {
...plugin,
installed: !!installed,
installedVersion: installed?.version,
}
const info = {
...plugin,
installed: !!installed,
installedVersion: installed?.version,
}
if (json) {
console.log(JSON.stringify(info, null, 2))
return
}
if (json) {
console.log(JSON.stringify(info, null, 2))
return
}
// Display info
if (plugin) {
UI.println(UI.Style.TEXT_HIGHLIGHT_BOLD + plugin.displayName + UI.Style.TEXT_NORMAL)
UI.println(UI.Style.TEXT_DIM + plugin.name + UI.Style.TEXT_NORMAL)
UI.empty()
UI.println(plugin.description)
UI.empty()
UI.println(UI.Style.TEXT_NORMAL_BOLD + "Package: " + UI.Style.TEXT_NORMAL + `${plugin.npm}@${plugin.version}`)
UI.println(UI.Style.TEXT_NORMAL_BOLD + "Category: " + UI.Style.TEXT_NORMAL + plugin.category)
UI.println(UI.Style.TEXT_NORMAL_BOLD + "Tags: " + UI.Style.TEXT_NORMAL + plugin.tags.join(", "))
UI.println(UI.Style.TEXT_NORMAL_BOLD + "Capabilities:" + UI.Style.TEXT_NORMAL + " " + plugin.capabilities.join(", "))
UI.println(UI.Style.TEXT_NORMAL_BOLD + "Author: " + UI.Style.TEXT_NORMAL + plugin.author)
if (plugin.homepage) {
UI.println(UI.Style.TEXT_NORMAL_BOLD + "Homepage: " + UI.Style.TEXT_INFO + plugin.homepage + UI.Style.TEXT_NORMAL)
// Display info
if (plugin) {
UI.println(UI.Style.TEXT_HIGHLIGHT_BOLD + plugin.displayName + UI.Style.TEXT_NORMAL)
UI.println(UI.Style.TEXT_DIM + plugin.name + UI.Style.TEXT_NORMAL)
UI.empty()
UI.println(plugin.description)
UI.empty()
UI.println(UI.Style.TEXT_NORMAL_BOLD + "Package: " + UI.Style.TEXT_NORMAL + `${plugin.npm}@${plugin.version}`)
UI.println(UI.Style.TEXT_NORMAL_BOLD + "Category: " + UI.Style.TEXT_NORMAL + plugin.category)
UI.println(UI.Style.TEXT_NORMAL_BOLD + "Tags: " + UI.Style.TEXT_NORMAL + plugin.tags.join(", "))
UI.println(UI.Style.TEXT_NORMAL_BOLD + "Capabilities:" + UI.Style.TEXT_NORMAL + " " + plugin.capabilities.join(", "))
UI.println(UI.Style.TEXT_NORMAL_BOLD + "Author: " + UI.Style.TEXT_NORMAL + plugin.author)
if (plugin.homepage) {
UI.println(UI.Style.TEXT_NORMAL_BOLD + "Homepage: " + UI.Style.TEXT_INFO + plugin.homepage + UI.Style.TEXT_NORMAL)
}
UI.empty()
if (installed) {
UI.println(UI.Style.TEXT_SUCCESS + "✓ Installed" + UI.Style.TEXT_NORMAL + UI.Style.TEXT_DIM + ` (${installed.spec})` + UI.Style.TEXT_NORMAL)
} else {
UI.println(UI.Style.TEXT_DIM + "Not installed" + UI.Style.TEXT_NORMAL)
UI.println(UI.Style.TEXT_DIM + `Install with: agent-core plugin install ${plugin.name}` + UI.Style.TEXT_NORMAL)
}
} else if (installed) {
// Plugin not in registry but is installed
UI.println(UI.Style.TEXT_HIGHLIGHT_BOLD + installed.name + UI.Style.TEXT_NORMAL)
UI.empty()
UI.println(UI.Style.TEXT_NORMAL_BOLD + "Package:" + UI.Style.TEXT_NORMAL + ` ${installed.spec}`)
UI.empty()
UI.println(UI.Style.TEXT_WARNING + "⚠" + UI.Style.TEXT_NORMAL + UI.Style.TEXT_DIM + " This plugin is not in the official registry" + UI.Style.TEXT_NORMAL)
}
} catch (error) {
UI.error(`Failed to get plugin info: ${error instanceof Error ? error.message : error}`)
process.exit(1)
}
UI.empty()
if (installed) {
UI.println(UI.Style.TEXT_SUCCESS + "✓ Installed" + UI.Style.TEXT_NORMAL + UI.Style.TEXT_DIM + ` (${installed.spec})` + UI.Style.TEXT_NORMAL)
} else {
UI.println(UI.Style.TEXT_DIM + "Not installed" + UI.Style.TEXT_NORMAL)
UI.println(UI.Style.TEXT_DIM + `Install with: agent-core plugin install ${plugin.name}` + UI.Style.TEXT_NORMAL)
}
} else if (installed) {
// Plugin not in registry but is installed
UI.println(UI.Style.TEXT_HIGHLIGHT_BOLD + installed.name + UI.Style.TEXT_NORMAL)
UI.empty()
UI.println(UI.Style.TEXT_NORMAL_BOLD + "Package:" + UI.Style.TEXT_NORMAL + ` ${installed.spec}`)
UI.empty()
UI.println(UI.Style.TEXT_WARNING + "⚠" + UI.Style.TEXT_NORMAL + UI.Style.TEXT_DIM + " This plugin is not in the official registry" + UI.Style.TEXT_NORMAL)
}
} catch (error) {
UI.error(`Failed to get plugin info: ${error instanceof Error ? error.message : error}`)
process.exit(1)
}
},
})
},
})
@@ -1,6 +1,7 @@
import { cmd } from "../cmd"
import { installPlugin, isInstalled } from "../../../plugin/manager"
import { UI } from "../../ui"
import { Instance } from "../../../project/instance"
export const InstallCommand = cmd({
command: "install <name>",
@@ -22,32 +23,37 @@ export const InstallCommand = cmd({
async handler(args) {
const { name, force } = args
try {
// Check if already installed
if (!force && (await isInstalled(name))) {
UI.println(UI.Style.TEXT_WARNING + `Plugin "${name}" is already installed` + UI.Style.TEXT_NORMAL)
UI.println(UI.Style.TEXT_DIM + "Use --force to reinstall" + UI.Style.TEXT_NORMAL)
return
}
await Instance.provide({
directory: process.cwd(),
async fn() {
try {
// Check if already installed
if (!force && (await isInstalled(name))) {
UI.println(UI.Style.TEXT_WARNING + `Plugin "${name}" is already installed` + UI.Style.TEXT_NORMAL)
UI.println(UI.Style.TEXT_DIM + "Use --force to reinstall" + UI.Style.TEXT_NORMAL)
return
}
UI.println(UI.Style.TEXT_DIM + `Installing ${name}...` + UI.Style.TEXT_NORMAL)
UI.println(UI.Style.TEXT_DIM + `Installing ${name}...` + UI.Style.TEXT_NORMAL)
const result = await installPlugin(name)
const result = await installPlugin(name)
if (result.success) {
UI.println(UI.Style.TEXT_SUCCESS + "✓" + UI.Style.TEXT_NORMAL + " " + result.message)
if (result.plugin) {
UI.empty()
UI.println(UI.Style.TEXT_DIM + `Capabilities: ${result.plugin.capabilities.join(", ")}` + UI.Style.TEXT_NORMAL)
UI.println(UI.Style.TEXT_DIM + "Restart agent-core for changes to take effect" + UI.Style.TEXT_NORMAL)
if (result.success) {
UI.println(UI.Style.TEXT_SUCCESS + "✓" + UI.Style.TEXT_NORMAL + " " + result.message)
if (result.plugin) {
UI.empty()
UI.println(UI.Style.TEXT_DIM + `Capabilities: ${result.plugin.capabilities.join(", ")}` + UI.Style.TEXT_NORMAL)
UI.println(UI.Style.TEXT_DIM + "Restart agent-core for changes to take effect" + UI.Style.TEXT_NORMAL)
}
} else {
UI.println(UI.Style.TEXT_DANGER + "✗" + UI.Style.TEXT_NORMAL + " " + result.message)
process.exit(1)
}
} catch (error) {
UI.error(`Install failed: ${error instanceof Error ? error.message : error}`)
process.exit(1)
}
} else {
UI.println(UI.Style.TEXT_DANGER + "✗" + UI.Style.TEXT_NORMAL + " " + result.message)
process.exit(1)
}
} catch (error) {
UI.error(`Install failed: ${error instanceof Error ? error.message : error}`)
process.exit(1)
}
},
})
},
})
+34 -28
View File
@@ -1,6 +1,7 @@
import { cmd } from "../cmd"
import { listInstalled } from "../../../plugin/manager"
import { UI } from "../../ui"
import { Instance } from "../../../project/instance"
export const ListCommand = cmd({
command: "list",
@@ -13,40 +14,45 @@ export const ListCommand = cmd({
default: false,
}),
async handler(args) {
try {
const installed = await listInstalled()
await Instance.provide({
directory: process.cwd(),
async fn() {
try {
const installed = await listInstalled()
if (args.json) {
console.log(JSON.stringify(installed, null, 2))
return
}
if (args.json) {
console.log(JSON.stringify(installed, null, 2))
return
}
if (installed.length === 0) {
UI.println(UI.Style.TEXT_WARNING + "No plugins installed" + UI.Style.TEXT_NORMAL)
UI.println(UI.Style.TEXT_DIM + "Search for plugins with: agent-core plugin search" + UI.Style.TEXT_NORMAL)
return
}
if (installed.length === 0) {
UI.println(UI.Style.TEXT_WARNING + "No plugins installed" + UI.Style.TEXT_NORMAL)
UI.println(UI.Style.TEXT_DIM + "Search for plugins with: agent-core plugin search" + UI.Style.TEXT_NORMAL)
return
}
UI.println(UI.Style.TEXT_NORMAL_BOLD + `Installed plugins (${installed.length}):` + UI.Style.TEXT_NORMAL)
UI.empty()
UI.println(UI.Style.TEXT_NORMAL_BOLD + `Installed plugins (${installed.length}):` + UI.Style.TEXT_NORMAL)
UI.empty()
for (const plugin of installed) {
const name = plugin.registryInfo?.displayName ?? plugin.name
const badge = plugin.fromRegistry
? UI.Style.TEXT_SUCCESS + "✓" + UI.Style.TEXT_NORMAL
: UI.Style.TEXT_WARNING + "?" + UI.Style.TEXT_NORMAL
for (const plugin of installed) {
const name = plugin.registryInfo?.displayName ?? plugin.name
const badge = plugin.fromRegistry
? UI.Style.TEXT_SUCCESS + "✓" + UI.Style.TEXT_NORMAL
: UI.Style.TEXT_WARNING + "?" + UI.Style.TEXT_NORMAL
UI.println(`${badge} ${UI.Style.TEXT_HIGHLIGHT_BOLD}${name}${UI.Style.TEXT_NORMAL}`)
UI.println(UI.Style.TEXT_DIM + ` ${plugin.spec}` + UI.Style.TEXT_NORMAL)
UI.println(`${badge} ${UI.Style.TEXT_HIGHLIGHT_BOLD}${name}${UI.Style.TEXT_NORMAL}`)
UI.println(UI.Style.TEXT_DIM + ` ${plugin.spec}` + UI.Style.TEXT_NORMAL)
if (plugin.registryInfo) {
UI.println(UI.Style.TEXT_DIM + ` ${plugin.registryInfo.description}` + UI.Style.TEXT_NORMAL)
if (plugin.registryInfo) {
UI.println(UI.Style.TEXT_DIM + ` ${plugin.registryInfo.description}` + UI.Style.TEXT_NORMAL)
}
UI.empty()
}
} catch (error) {
UI.error(`Failed to list plugins: ${error instanceof Error ? error.message : error}`)
process.exit(1)
}
UI.empty()
}
} catch (error) {
UI.error(`Failed to list plugins: ${error instanceof Error ? error.message : error}`)
process.exit(1)
}
},
})
},
})
@@ -1,6 +1,7 @@
import { cmd } from "../cmd"
import { removePlugin, isInstalled } from "../../../plugin/manager"
import { UI } from "../../ui"
import { Instance } from "../../../project/instance"
export const RemoveCommand = cmd({
command: "remove <name>",
@@ -15,28 +16,33 @@ export const RemoveCommand = cmd({
async handler(args) {
const { name } = args
try {
// Check if installed
if (!(await isInstalled(name))) {
UI.println(UI.Style.TEXT_WARNING + `Plugin "${name}" is not installed` + UI.Style.TEXT_NORMAL)
UI.println(UI.Style.TEXT_DIM + "Use 'agent-core plugin list' to see installed plugins" + UI.Style.TEXT_NORMAL)
return
}
await Instance.provide({
directory: process.cwd(),
async fn() {
try {
// Check if installed
if (!(await isInstalled(name))) {
UI.println(UI.Style.TEXT_WARNING + `Plugin "${name}" is not installed` + UI.Style.TEXT_NORMAL)
UI.println(UI.Style.TEXT_DIM + "Use 'agent-core plugin list' to see installed plugins" + UI.Style.TEXT_NORMAL)
return
}
UI.println(UI.Style.TEXT_DIM + `Removing ${name}...` + UI.Style.TEXT_NORMAL)
UI.println(UI.Style.TEXT_DIM + `Removing ${name}...` + UI.Style.TEXT_NORMAL)
const result = await removePlugin(name)
const result = await removePlugin(name)
if (result.success) {
UI.println(UI.Style.TEXT_SUCCESS + "✓" + UI.Style.TEXT_NORMAL + " " + result.message)
UI.println(UI.Style.TEXT_DIM + "Restart agent-core for changes to take effect" + UI.Style.TEXT_NORMAL)
} else {
UI.println(UI.Style.TEXT_DANGER + "✗" + UI.Style.TEXT_NORMAL + " " + result.message)
process.exit(1)
}
} catch (error) {
UI.error(`Remove failed: ${error instanceof Error ? error.message : error}`)
process.exit(1)
}
if (result.success) {
UI.println(UI.Style.TEXT_SUCCESS + "✓" + UI.Style.TEXT_NORMAL + " " + result.message)
UI.println(UI.Style.TEXT_DIM + "Restart agent-core for changes to take effect" + UI.Style.TEXT_NORMAL)
} else {
UI.println(UI.Style.TEXT_DANGER + "✗" + UI.Style.TEXT_NORMAL + " " + result.message)
process.exit(1)
}
} catch (error) {
UI.error(`Remove failed: ${error instanceof Error ? error.message : error}`)
process.exit(1)
}
},
})
},
})
+8 -1
View File
@@ -47,7 +47,14 @@ async function readCacheFile(): Promise<CachedRegistry | null> {
try {
const cachePath = await getCachePath()
const content = await fs.readFile(cachePath, "utf-8")
return JSON.parse(content)
const parsed = JSON.parse(content)
// Validate cached data has expected structure
if (!parsed.data || typeof parsed.timestamp !== "number") {
return null
}
// Validate the registry data itself
RegistrySchema.parse(parsed.data)
return parsed as CachedRegistry
} catch {
return null
}