From 970d4c4d1a25314b3cfa1aeaf368387a031df595 Mon Sep 17 00:00:00 2001 From: jackwener Date: Tue, 24 Mar 2026 21:53:07 +0800 Subject: [PATCH] review: scope plugin update-all and sync docs --- README.md | 1 + README.zh-CN.md | 1 + docs/guide/plugins.md | 6 ++++ docs/zh/guide/plugins.md | 6 ++++ src/cli.ts | 54 +++++++++++++++++++++++++++++---- src/plugin.test.ts | 65 ++++++++++++++++++++++++++++++++++++++-- src/plugin.ts | 32 +++++++++++++++++++- 7 files changed, 157 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 227530a0..7c9b5522 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,7 @@ Extend OpenCLI with community-contributed adapters. Plugins use the same YAML/TS opencli plugin install github:user/opencli-plugin-my-tool # Install opencli plugin list # List installed opencli plugin update my-tool # Update to latest +opencli plugin update --all # Update all installed plugins opencli plugin uninstall my-tool # Remove ``` diff --git a/README.zh-CN.md b/README.zh-CN.md index d3b01b96..451cd228 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -292,6 +292,7 @@ opencli bilibili hot -v # 详细模式:展示管线执行步骤调试 opencli plugin install github:user/opencli-plugin-my-tool # 安装 opencli plugin list # 查看已安装 opencli plugin update my-tool # 更新到最新 +opencli plugin update --all # 更新全部已安装插件 opencli plugin uninstall my-tool # 卸载 ``` diff --git a/docs/guide/plugins.md b/docs/guide/plugins.md index febd0275..5569be52 100644 --- a/docs/guide/plugins.md +++ b/docs/guide/plugins.md @@ -11,6 +11,12 @@ opencli plugin install github:ByteYue/opencli-plugin-github-trending # List installed plugins opencli plugin list +# Update one plugin +opencli plugin update github-trending + +# Update all installed plugins +opencli plugin update --all + # Use the plugin (it's just a regular command) opencli github-trending repos --limit 10 diff --git a/docs/zh/guide/plugins.md b/docs/zh/guide/plugins.md index 2046a951..8f71734d 100644 --- a/docs/zh/guide/plugins.md +++ b/docs/zh/guide/plugins.md @@ -11,6 +11,12 @@ opencli plugin install github:ByteYue/opencli-plugin-github-trending # 列出已安装插件 opencli plugin list +# 更新单个插件 +opencli plugin update github-trending + +# 更新全部已安装插件 +opencli plugin update --all + # 使用插件(本质上就是普通 command) opencli github-trending today diff --git a/src/cli.ts b/src/cli.ts index 295140ca..7de29e70 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -283,13 +283,57 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void { pluginCmd .command('update') - .description('Update a plugin to the latest version') - .argument('', 'Plugin name') - .action(async (name: string) => { - const { updatePlugin } = await import('./plugin.js'); + .description('Update a plugin (or all plugins) to the latest version') + .argument('[name]', 'Plugin name (required unless --all is passed)') + .option('--all', 'Update all installed plugins') + .action(async (name: string | undefined, opts: { all?: boolean }) => { + if (!name && !opts.all) { + console.error(chalk.red('Error: Please specify a plugin name or use the --all flag.')); + process.exitCode = 1; + return; + } + if (name && opts.all) { + console.error(chalk.red('Error: Cannot specify both a plugin name and --all.')); + process.exitCode = 1; + return; + } + + const { updatePlugin, updateAllPlugins } = await import('./plugin.js'); const { discoverPlugins } = await import('./discovery.js'); + if (opts.all) { + const results = updateAllPlugins(); + if (results.length > 0) { + await discoverPlugins(); + } + + let hasErrors = false; + console.log(chalk.bold(' Update Results:')); + for (const result of results) { + if (result.success) { + console.log(` ${chalk.green('✓')} ${result.name}`); + continue; + } + hasErrors = true; + console.log(` ${chalk.red('✗')} ${result.name} — ${chalk.dim(result.error)}`); + } + + if (results.length === 0) { + console.log(chalk.dim(' No plugins installed.')); + return; + } + + console.log(); + if (hasErrors) { + console.error(chalk.red('Completed with some errors.')); + process.exitCode = 1; + } else { + console.log(chalk.green('✅ All plugins updated successfully.')); + } + return; + } + try { - updatePlugin(name); + updatePlugin(name!); await discoverPlugins(); console.log(chalk.green(`✅ Plugin "${name}" updated successfully.`)); } catch (err: any) { diff --git a/src/plugin.test.ts b/src/plugin.test.ts index fd08a17a..048f22ba 100644 --- a/src/plugin.test.ts +++ b/src/plugin.test.ts @@ -2,11 +2,20 @@ * Tests for plugin management: install, uninstall, list. */ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import * as fs from 'node:fs'; import * as path from 'node:path'; import { PLUGINS_DIR } from './discovery.js'; -import { listPlugins, uninstallPlugin, updatePlugin, _parseSource, _validatePluginStructure } from './plugin.js'; +import * as pluginModule from './plugin.js'; + +const { + listPlugins, + uninstallPlugin, + updatePlugin, + _parseSource, + _updateAllPlugins, + _validatePluginStructure, +} = pluginModule; describe('parseSource', () => { it('parses github:user/repo format', () => { @@ -151,3 +160,55 @@ describe('updatePlugin', () => { expect(() => updatePlugin('__nonexistent__')).toThrow('not installed'); }); }); + +vi.mock('node:child_process', () => { + return { + execFileSync: vi.fn((_cmd, _args, opts) => { + if (opts && opts.cwd && String(opts.cwd).endsWith('plugin-b')) { + throw new Error('Network error'); + } + return ''; + }), + execSync: vi.fn(() => ''), + }; +}); + +describe('updateAllPlugins', () => { + const testDirA = path.join(PLUGINS_DIR, 'plugin-a'); + const testDirB = path.join(PLUGINS_DIR, 'plugin-b'); + const testDirC = path.join(PLUGINS_DIR, 'plugin-c'); + + beforeEach(() => { + fs.mkdirSync(testDirA, { recursive: true }); + fs.mkdirSync(testDirB, { recursive: true }); + fs.mkdirSync(testDirC, { recursive: true }); + fs.writeFileSync(path.join(testDirA, 'cmd.yaml'), 'site: a'); + fs.writeFileSync(path.join(testDirB, 'cmd.yaml'), 'site: b'); + fs.writeFileSync(path.join(testDirC, 'cmd.yaml'), 'site: c'); + }); + + afterEach(() => { + try { fs.rmSync(testDirA, { recursive: true }); } catch {} + try { fs.rmSync(testDirB, { recursive: true }); } catch {} + try { fs.rmSync(testDirC, { recursive: true }); } catch {} + vi.clearAllMocks(); + }); + + it('collects successes and failures without throwing', () => { + const results = _updateAllPlugins(); + + const resA = results.find(r => r.name === 'plugin-a'); + const resB = results.find(r => r.name === 'plugin-b'); + const resC = results.find(r => r.name === 'plugin-c'); + + expect(resA).toBeDefined(); + expect(resA!.success).toBe(true); + + expect(resB).toBeDefined(); + expect(resB!.success).toBe(false); + expect(resB!.error).toContain('Network error'); + + expect(resC).toBeDefined(); + expect(resC!.success).toBe(true); + }); +}); diff --git a/src/plugin.ts b/src/plugin.ts index e1020bc4..109d9ce7 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -9,6 +9,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { execSync, execFileSync } from 'node:child_process'; import { PLUGINS_DIR } from './discovery.js'; +import { getErrorMessage } from './errors.js'; import { log } from './logger.js'; export interface PluginInfo { @@ -174,6 +175,31 @@ export function updatePlugin(name: string): void { postInstallLifecycle(targetDir); } +export interface UpdateResult { + name: string; + success: boolean; + error?: string; +} + +/** + * Update all installed plugins. + * Continues even if individual plugin updates fail. + */ +export function updateAllPlugins(): UpdateResult[] { + return listPlugins().map((plugin): UpdateResult => { + try { + updatePlugin(plugin.name); + return { name: plugin.name, success: true }; + } catch (err) { + return { + name: plugin.name, + success: false, + error: getErrorMessage(err), + }; + } + }); +} + /** * List all installed plugins. */ @@ -334,4 +360,8 @@ function transpilePluginTs(pluginDir: string): void { } } -export { parseSource as _parseSource, validatePluginStructure as _validatePluginStructure }; +export { + parseSource as _parseSource, + updateAllPlugins as _updateAllPlugins, + validatePluginStructure as _validatePluginStructure, +};