Welcome back! In the previous chapter, CLI Command Interface, we learned how the "dashboard" of our application accepts commands like install or disable.
But there is a catch. When a user types:
tengu plugin disable my-plugin
...how does the system know which "my-plugin" they are talking about? Is it the one installed globally for the user? Or the one specific to the current project? Is it from the official marketplace or a private one?
This chapter explains how we translate a simple string into a concrete plugin identity.
Think of your phone's contact list.
+1-555-0199.The Central Use Case: A user wants to uninstall a plugin. They type:
tengu plugin uninstall super-logger
Our system needs to:
super-logger is a nickname (name) or a full ID.super-logger is already defined there.super-logger@official-marketplace.There are two ways to refer to a plugin:
super-logger. This is ambiguous. It's like saying "John".super-logger@marketplace. This is specific. It's like saying "john.doe@email.com".We store settings in three places (Scopes). When looking for a plugin, we search in a specific order:
.claude/claude.json - just for you).claude/settings.json - shared with the team)~/.config/claude/config.json - global)Most Specific Wins: If "super-logger" is defined in your Local settings, we stop looking there. We assume that's the one you mean, even if another exists globally.
Sometimes, a plugin might be removed (delisted) from the marketplace. It no longer exists online, but you still have it installed. We keep a permanent record in a file called installed_plugins_v2.json. If we can't find a plugin in the marketplace, we check here.
Let's see how we handle uninstallPluginOp. We don't just blindly delete files; we first figure out exactly who we are dealing with.
Here is the high-level logic (Conceptual):
// Input: "super-logger"
const found = findPluginInSettings("super-logger");
if (found) {
// We found it in the settings!
// It resolves to "super-logger@official"
return uninstall("super-logger@official");
} else {
// Not in settings? Check the "Lost & Found" record.
const record = resolveDelistedPluginId("super-logger");
return uninstall(record.fullId);
}
Let's look at how this works under the hood.
Before looking at the code, let's visualize the decision process.
In pluginOperations.ts, we use a helper function called findPluginInSettings. It implements the "Most Specific Wins" search strategy.
function findPluginInSettings(plugin: string): { pluginId: string, scope: InstallableScope } | null {
const searchOrder: InstallableScope[] = ['local', 'project', 'user']
// 1. Loop through scopes from most specific to least specific
for (const scope of searchOrder) {
const enabledPlugins = getSettingsForSource(scopeToSettingSource(scope))?.enabledPlugins
if (!enabledPlugins) continue
// 2. Check every key in this setting file
for (const key of Object.keys(enabledPlugins)) {
// Does "my-plugin" match "my-plugin@marketplace"?
if (key === plugin || key.startsWith(`${plugin}@`)) {
return { pluginId: key, scope } // Found it!
}
}
}
return null // Not found in any settings
}
Explanation:
local, then project, then user.pluginId) and where we found it (scope).
If a plugin is delisted, it won't appear in the marketplace API anymore. However, the user still has it installed. We need resolveDelistedPluginId to find these "ghost" plugins.
function resolveDelistedPluginId(plugin: string) {
const { name } = parsePluginIdentifier(plugin)
const installedData = loadInstalledPluginsV2()
// 1. Try exact match (e.g., input was "my-plugin@old-market")
if (installedData.plugins[plugin]?.length) {
return { pluginId: plugin, pluginName: name }
}
// 2. Try searching by name (e.g., input was "my-plugin")
const matchingKey = Object.keys(installedData.plugins).find(key => {
const { name: keyName } = parsePluginIdentifier(key)
// Return true if names match and it's actually installed
return keyName === name && (installedData.plugins[key]?.length ?? 0) > 0
})
return matchingKey ? { pluginId: matchingKey, pluginName: name } : null
}
Explanation:
loadInstalledPluginsV2() loads our permanent record of what is physically on the disk.
Finally, let's see how uninstallPluginOp uses these tools.
export async function uninstallPluginOp(plugin: string, scope: InstallableScope = 'user') {
// 1. Try to find the plugin in our loaded list/settings
const foundPlugin = findPluginByIdentifier(plugin, allPlugins)
let pluginId: string
if (foundPlugin) {
// We found it! Resolve the full ID from settings.
pluginId = foundPlugin.name // (Simplified for tutorial)
} else {
// 2. Not found? It might be delisted. Check the V2 records.
const resolved = resolveDelistedPluginId(plugin)
if (!resolved) {
return { success: false, message: `Plugin "${plugin}" not found` }
}
pluginId = resolved.pluginId
}
// ... proceed to remove files ...
}
Explanation:
resolveDelistedPluginId).In this chapter, we learned that a plugin name is just a starting point. To safely operate on plugins, the system must:
Now that we know which plugin we are talking about and where it lives in the settings, we need to decide exactly which configuration file takes precedence when the code actually runs.
Next, we will explore the rules of the hierarchy in detail: Scope Resolution Strategy
Generated by Code IQ