Skip to content

Commit 93b9db5

Browse files
ByteYuejackwener
andauthored
review: scope plugin update-all and sync docs (#368)
Co-authored-by: jackwener <jakevingoo@gmail.com>
1 parent ba5f133 commit 93b9db5

7 files changed

Lines changed: 157 additions & 8 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ Extend OpenCLI with community-contributed adapters. Plugins use the same YAML/TS
290290
opencli plugin install github:user/opencli-plugin-my-tool # Install
291291
opencli plugin list # List installed
292292
opencli plugin update my-tool # Update to latest
293+
opencli plugin update --all # Update all installed plugins
293294
opencli plugin uninstall my-tool # Remove
294295
```
295296

README.zh-CN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ opencli bilibili hot -v # 详细模式:展示管线执行步骤调试
292292
opencli plugin install github:user/opencli-plugin-my-tool # 安装
293293
opencli plugin list # 查看已安装
294294
opencli plugin update my-tool # 更新到最新
295+
opencli plugin update --all # 更新全部已安装插件
295296
opencli plugin uninstall my-tool # 卸载
296297
```
297298

docs/guide/plugins.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ opencli plugin install github:ByteYue/opencli-plugin-github-trending
1111
# List installed plugins
1212
opencli plugin list
1313

14+
# Update one plugin
15+
opencli plugin update github-trending
16+
17+
# Update all installed plugins
18+
opencli plugin update --all
19+
1420
# Use the plugin (it's just a regular command)
1521
opencli github-trending repos --limit 10
1622

docs/zh/guide/plugins.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ opencli plugin install github:ByteYue/opencli-plugin-github-trending
1111
# 列出已安装插件
1212
opencli plugin list
1313

14+
# 更新单个插件
15+
opencli plugin update github-trending
16+
17+
# 更新全部已安装插件
18+
opencli plugin update --all
19+
1420
# 使用插件(本质上就是普通 command)
1521
opencli github-trending today
1622

src/cli.ts

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -283,13 +283,57 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
283283

284284
pluginCmd
285285
.command('update')
286-
.description('Update a plugin to the latest version')
287-
.argument('<name>', 'Plugin name')
288-
.action(async (name: string) => {
289-
const { updatePlugin } = await import('./plugin.js');
286+
.description('Update a plugin (or all plugins) to the latest version')
287+
.argument('[name]', 'Plugin name (required unless --all is passed)')
288+
.option('--all', 'Update all installed plugins')
289+
.action(async (name: string | undefined, opts: { all?: boolean }) => {
290+
if (!name && !opts.all) {
291+
console.error(chalk.red('Error: Please specify a plugin name or use the --all flag.'));
292+
process.exitCode = 1;
293+
return;
294+
}
295+
if (name && opts.all) {
296+
console.error(chalk.red('Error: Cannot specify both a plugin name and --all.'));
297+
process.exitCode = 1;
298+
return;
299+
}
300+
301+
const { updatePlugin, updateAllPlugins } = await import('./plugin.js');
290302
const { discoverPlugins } = await import('./discovery.js');
303+
if (opts.all) {
304+
const results = updateAllPlugins();
305+
if (results.length > 0) {
306+
await discoverPlugins();
307+
}
308+
309+
let hasErrors = false;
310+
console.log(chalk.bold(' Update Results:'));
311+
for (const result of results) {
312+
if (result.success) {
313+
console.log(` ${chalk.green('✓')} ${result.name}`);
314+
continue;
315+
}
316+
hasErrors = true;
317+
console.log(` ${chalk.red('✗')} ${result.name}${chalk.dim(result.error)}`);
318+
}
319+
320+
if (results.length === 0) {
321+
console.log(chalk.dim(' No plugins installed.'));
322+
return;
323+
}
324+
325+
console.log();
326+
if (hasErrors) {
327+
console.error(chalk.red('Completed with some errors.'));
328+
process.exitCode = 1;
329+
} else {
330+
console.log(chalk.green('✅ All plugins updated successfully.'));
331+
}
332+
return;
333+
}
334+
291335
try {
292-
updatePlugin(name);
336+
updatePlugin(name!);
293337
await discoverPlugins();
294338
console.log(chalk.green(`✅ Plugin "${name}" updated successfully.`));
295339
} catch (err: any) {

src/plugin.test.ts

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,20 @@
22
* Tests for plugin management: install, uninstall, list.
33
*/
44

5-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
5+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
66
import * as fs from 'node:fs';
77
import * as path from 'node:path';
88
import { PLUGINS_DIR } from './discovery.js';
9-
import { listPlugins, uninstallPlugin, updatePlugin, _parseSource, _validatePluginStructure } from './plugin.js';
9+
import * as pluginModule from './plugin.js';
10+
11+
const {
12+
listPlugins,
13+
uninstallPlugin,
14+
updatePlugin,
15+
_parseSource,
16+
_updateAllPlugins,
17+
_validatePluginStructure,
18+
} = pluginModule;
1019

1120
describe('parseSource', () => {
1221
it('parses github:user/repo format', () => {
@@ -151,3 +160,55 @@ describe('updatePlugin', () => {
151160
expect(() => updatePlugin('__nonexistent__')).toThrow('not installed');
152161
});
153162
});
163+
164+
vi.mock('node:child_process', () => {
165+
return {
166+
execFileSync: vi.fn((_cmd, _args, opts) => {
167+
if (opts && opts.cwd && String(opts.cwd).endsWith('plugin-b')) {
168+
throw new Error('Network error');
169+
}
170+
return '';
171+
}),
172+
execSync: vi.fn(() => ''),
173+
};
174+
});
175+
176+
describe('updateAllPlugins', () => {
177+
const testDirA = path.join(PLUGINS_DIR, 'plugin-a');
178+
const testDirB = path.join(PLUGINS_DIR, 'plugin-b');
179+
const testDirC = path.join(PLUGINS_DIR, 'plugin-c');
180+
181+
beforeEach(() => {
182+
fs.mkdirSync(testDirA, { recursive: true });
183+
fs.mkdirSync(testDirB, { recursive: true });
184+
fs.mkdirSync(testDirC, { recursive: true });
185+
fs.writeFileSync(path.join(testDirA, 'cmd.yaml'), 'site: a');
186+
fs.writeFileSync(path.join(testDirB, 'cmd.yaml'), 'site: b');
187+
fs.writeFileSync(path.join(testDirC, 'cmd.yaml'), 'site: c');
188+
});
189+
190+
afterEach(() => {
191+
try { fs.rmSync(testDirA, { recursive: true }); } catch {}
192+
try { fs.rmSync(testDirB, { recursive: true }); } catch {}
193+
try { fs.rmSync(testDirC, { recursive: true }); } catch {}
194+
vi.clearAllMocks();
195+
});
196+
197+
it('collects successes and failures without throwing', () => {
198+
const results = _updateAllPlugins();
199+
200+
const resA = results.find(r => r.name === 'plugin-a');
201+
const resB = results.find(r => r.name === 'plugin-b');
202+
const resC = results.find(r => r.name === 'plugin-c');
203+
204+
expect(resA).toBeDefined();
205+
expect(resA!.success).toBe(true);
206+
207+
expect(resB).toBeDefined();
208+
expect(resB!.success).toBe(false);
209+
expect(resB!.error).toContain('Network error');
210+
211+
expect(resC).toBeDefined();
212+
expect(resC!.success).toBe(true);
213+
});
214+
});

src/plugin.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as fs from 'node:fs';
99
import * as path from 'node:path';
1010
import { execSync, execFileSync } from 'node:child_process';
1111
import { PLUGINS_DIR } from './discovery.js';
12+
import { getErrorMessage } from './errors.js';
1213
import { log } from './logger.js';
1314

1415
export interface PluginInfo {
@@ -174,6 +175,31 @@ export function updatePlugin(name: string): void {
174175
postInstallLifecycle(targetDir);
175176
}
176177

178+
export interface UpdateResult {
179+
name: string;
180+
success: boolean;
181+
error?: string;
182+
}
183+
184+
/**
185+
* Update all installed plugins.
186+
* Continues even if individual plugin updates fail.
187+
*/
188+
export function updateAllPlugins(): UpdateResult[] {
189+
return listPlugins().map((plugin): UpdateResult => {
190+
try {
191+
updatePlugin(plugin.name);
192+
return { name: plugin.name, success: true };
193+
} catch (err) {
194+
return {
195+
name: plugin.name,
196+
success: false,
197+
error: getErrorMessage(err),
198+
};
199+
}
200+
});
201+
}
202+
177203
/**
178204
* List all installed plugins.
179205
*/
@@ -334,4 +360,8 @@ function transpilePluginTs(pluginDir: string): void {
334360
}
335361
}
336362

337-
export { parseSource as _parseSource, validatePluginStructure as _validatePluginStructure };
363+
export {
364+
parseSource as _parseSource,
365+
updateAllPlugins as _updateAllPlugins,
366+
validatePluginStructure as _validatePluginStructure,
367+
};

0 commit comments

Comments
 (0)