(options: {
json?: boolean
available?: boolean
cowork?: boolean
})
| 157 | |
| 158 | // plugin list (lines 5217–5416) |
| 159 | export async function pluginListHandler(options: { |
| 160 | json?: boolean |
| 161 | available?: boolean |
| 162 | cowork?: boolean |
| 163 | }): Promise<void> { |
| 164 | if (options.cowork) setUseCoworkPlugins(true) |
| 165 | logEvent('tengu_plugin_list_command', {}) |
| 166 | |
| 167 | const installedData = loadInstalledPluginsV2() |
| 168 | const { getPluginEditableScopes } = await import( |
| 169 | '../../utils/plugins/pluginStartupCheck.js' |
| 170 | ) |
| 171 | const enabledPlugins = getPluginEditableScopes() |
| 172 | |
| 173 | const pluginIds = Object.keys(installedData.plugins) |
| 174 | |
| 175 | // Load all plugins once. The JSON and human paths both need: |
| 176 | // - loadErrors (to show load failures per plugin) |
| 177 | // - inline plugins (session-only via --plugin-dir, source='name@inline') |
| 178 | // which are NOT in installedData.plugins (V2 bookkeeping) — they must |
| 179 | // be surfaced separately or `plugin list` silently ignores --plugin-dir. |
| 180 | const { |
| 181 | enabled: loadedEnabled, |
| 182 | disabled: loadedDisabled, |
| 183 | errors: loadErrors, |
| 184 | } = await loadAllPlugins() |
| 185 | const allLoadedPlugins = [...loadedEnabled, ...loadedDisabled] |
| 186 | const inlinePlugins = allLoadedPlugins.filter(p => |
| 187 | p.source.endsWith('@inline'), |
| 188 | ) |
| 189 | // Path-level inline failures (dir doesn't exist, parse error before |
| 190 | // manifest is read) use source='inline[N]'. Plugin-level errors after |
| 191 | // manifest read use source='name@inline'. Collect both for the session |
| 192 | // section — these are otherwise invisible since they have no pluginId. |
| 193 | const inlineLoadErrors = loadErrors.filter( |
| 194 | e => e.source.endsWith('@inline') || e.source.startsWith('inline['), |
| 195 | ) |
| 196 | |
| 197 | if (options.json) { |
| 198 | // Create a map of plugin source to loaded plugin for quick lookup |
| 199 | const loadedPluginMap = new Map(allLoadedPlugins.map(p => [p.source, p])) |
| 200 | |
| 201 | const plugins: Array<{ |
| 202 | id: string |
| 203 | version: string |
| 204 | scope: string |
| 205 | enabled: boolean |
| 206 | installPath: string |
| 207 | installedAt?: string |
| 208 | lastUpdated?: string |
| 209 | projectPath?: string |
| 210 | mcpServers?: Record<string, unknown> |
| 211 | errors?: string[] |
| 212 | }> = [] |
| 213 | |
| 214 | for (const pluginId of pluginIds.sort()) { |
| 215 | const installations = installedData.plugins[pluginId] |
| 216 | if (!installations || installations.length === 0) continue |
no test coverage detected