Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
1 change: 1 addition & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 # 卸载
```

Expand Down
6 changes: 6 additions & 0 deletions docs/guide/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions docs/zh/guide/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
54 changes: 49 additions & 5 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('<name>', '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) {
Expand Down
65 changes: 63 additions & 2 deletions src/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
});
});
32 changes: 31 additions & 1 deletion src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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,
};
Loading