From 6da2556e9f6f1dd9cce8bc2965e3007020fee11d Mon Sep 17 00:00:00 2001 From: jackwener Date: Sun, 22 Mar 2026 18:39:58 +0800 Subject: [PATCH] chore: prepare v1.2 release --- docs/zh/guide/plugins.md | 107 +++++++++++++++++++++++++++++++++++++++ package-lock.json | 4 +- package.json | 2 +- src/discovery.ts | 7 +-- src/execution.ts | 3 +- 5 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 docs/zh/guide/plugins.md diff --git a/docs/zh/guide/plugins.md b/docs/zh/guide/plugins.md new file mode 100644 index 00000000..2046a951 --- /dev/null +++ b/docs/zh/guide/plugins.md @@ -0,0 +1,107 @@ +# 插件 + +OpenCLI 支持社区贡献的 plugins。你可以从 GitHub 安装第三方 adapters,它们会和内置 commands 一起在启动时自动发现。 + +## 安装插件 + +```bash +# 安装插件 +opencli plugin install github:ByteYue/opencli-plugin-github-trending + +# 列出已安装插件 +opencli plugin list + +# 使用插件(本质上就是普通 command) +opencli github-trending today + +# 卸载插件 +opencli plugin uninstall github-trending +``` + +## 插件目录结构 + +Plugins 存放在 `~/.opencli/plugins//`。每个子目录都会在启动时扫描 `.yaml`、`.ts`、`.js` 命令文件,格式与内置 adapters 相同。 + +## 安装来源 + +```bash +opencli plugin install github:user/repo +opencli plugin install https://github.com/user/repo +``` + +如果仓库名带 `opencli-plugin-` 前缀,本地目录会自动去掉这个前缀。例如 `opencli-plugin-hot-digest` 会变成 `hot-digest`。 + +## YAML plugin 示例 + +```text +my-plugin/ + hot.yaml +``` + +```yaml +site: my-plugin +name: hot +description: Example plugin command +strategy: public +browser: false + +pipeline: + - evaluate: | + () => [{ title: 'hello', url: 'https://example.com' }] + +columns: [title, url] +``` + +## TypeScript plugin 示例 + +```text +my-plugin/ + index.ts + package.json +``` + +```json +{ + "name": "opencli-plugin-my-plugin", + "type": "module" +} +``` + +```ts +import { cli, Strategy } from '@jackwener/opencli/registry'; + +cli({ + site: 'my-plugin', + name: 'hot', + description: 'Example TS plugin command', + strategy: Strategy.PUBLIC, + browser: false, + columns: ['title', 'url'], + func: async () => [{ title: 'hello', url: 'https://example.com' }], +}); +``` + +运行 `opencli plugin install` 时,TS plugins 会自动完成基础设置: + +1. 安装 plugin 自身依赖 +2. 补齐 TypeScript 运行环境 +3. 将宿主 `@jackwener/opencli` 链接到 plugin 的 `node_modules/`,保证 `@jackwener/opencli/registry` 指向当前宿主版本 + +## 示例 plugins + +- `opencli-plugin-github-trending`:GitHub Trending 仓库 +- `opencli-plugin-hot-digest`:多平台热点聚合(zhihu、weibo、bilibili、v2ex、stackoverflow、reddit、linux-do) +- `opencli-plugin-juejin`:稀土掘金热榜、分类和文章流 + +## 排查问题 + +### TS plugin import 报错 + +如果看到 `Cannot find module '@jackwener/opencli/registry'`,通常是宿主 symlink 失效。重新安装 plugin 即可: + +```bash +opencli plugin uninstall my-plugin +opencli plugin install github:user/opencli-plugin-my-plugin +``` + +安装或卸载 plugin 后,建议重新打开一个终端,确保启动时重新发现命令。 diff --git a/package-lock.json b/package-lock.json index eac3796f..801d819b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jackwener/opencli", - "version": "1.1.1", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jackwener/opencli", - "version": "1.1.1", + "version": "1.2.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 3734124e..a9ee6e58 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@jackwener/opencli", - "version": "1.1.1", + "version": "1.2.0", "publishConfig": { "access": "public" }, diff --git a/src/discovery.ts b/src/discovery.ts index e4039add..c4e98042 100644 --- a/src/discovery.ts +++ b/src/discovery.ts @@ -11,6 +11,7 @@ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; +import { pathToFileURL } from 'node:url'; import yaml from 'js-yaml'; import { type CliCommand, type InternalCliCommand, type Arg, Strategy, registerCommand } from './registry.js'; import { log } from './logger.js'; @@ -156,7 +157,7 @@ async function discoverClisFromFs(dir: string): Promise { ) { if (!(await isCliModule(filePath))) continue; promises.push( - import(`file://${filePath}`).catch((err) => { + import(pathToFileURL(filePath).href).catch((err) => { log.warn(`Failed to load module ${filePath}: ${getErrorMessage(err)}`); }) ); @@ -244,7 +245,7 @@ async function discoverPluginDir(dir: string, site: string): Promise { } else if (file.endsWith('.js') && !file.endsWith('.d.js')) { if (!(await isCliModule(filePath))) continue; promises.push( - import(`file://${filePath}`).catch((err) => { + import(pathToFileURL(filePath).href).catch((err) => { log.warn(`Plugin ${site}/${file}: ${getErrorMessage(err)}`); }) ); @@ -256,7 +257,7 @@ async function discoverPluginDir(dir: string, site: string): Promise { if (fileSet.has(jsFile)) continue; if (!(await isCliModule(filePath))) continue; promises.push( - import(`file://${filePath}`).catch((err) => { + import(pathToFileURL(filePath).href).catch((err) => { log.warn(`Plugin ${site}/${file}: ${getErrorMessage(err)}`); }) ); diff --git a/src/execution.ts b/src/execution.ts index 7119cd12..3363fa7e 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -11,6 +11,7 @@ import { type CliCommand, type InternalCliCommand, type Arg, Strategy, getRegistry, fullName } from './registry.js'; import type { IPage } from './types.js'; +import { pathToFileURL } from 'node:url'; import { executePipeline } from './pipeline/index.js'; import { AdapterLoadError } from './errors.js'; import { shouldUseBrowserSession } from './capabilityRouting.js'; @@ -86,7 +87,7 @@ async function runCommand( const modulePath = internal._modulePath; if (!_loadedModules.has(modulePath)) { try { - await import(`file://${modulePath}`); + await import(pathToFileURL(modulePath).href); _loadedModules.add(modulePath); } catch (err) { throw new AdapterLoadError(