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