funcoding-cli 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -2
- package/dist/agents.js +55 -0
- package/dist/api.js +51 -0
- package/dist/auth.js +99 -0
- package/dist/cli.js +317 -0
- package/dist/config-files.js +84 -0
- package/dist/content.js +100 -0
- package/dist/credentials.js +30 -0
- package/dist/info.js +40 -0
- package/dist/init.js +90 -0
- package/dist/install.js +278 -0
- package/dist/mcp.js +265 -0
- package/dist/paths.js +18 -0
- package/dist/setup.js +78 -0
- package/dist/sync.js +86 -0
- package/dist/ui.js +65 -0
- package/package.json +41 -4
- package/skill/funcoding/SKILL.md +96 -0
package/README.md
CHANGED
|
@@ -1,3 +1,108 @@
|
|
|
1
|
-
#
|
|
1
|
+
# funcoding-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[funcoding.ai](https://funcoding.ai) 的命令行工具,主要给 Claude Code、Codex、Cursor 等 AI 编程智能体使用。智能体可以用它:
|
|
4
|
+
|
|
5
|
+
- 查找和安装 Agent Skills
|
|
6
|
+
- 配置 MCP Server
|
|
7
|
+
- 同步用户在网站上的收藏
|
|
8
|
+
- 查看 AI 热点,搜索和阅读中文文档
|
|
9
|
+
|
|
10
|
+
人也可以直接使用。需要 Node.js 18.17 及以上。
|
|
11
|
+
|
|
12
|
+
## 让智能体用起来
|
|
13
|
+
|
|
14
|
+
把这句话发给你的智能体:
|
|
15
|
+
|
|
16
|
+
> 运行 `npx --yes funcoding-cli@latest init`,然后帮我找一个处理 PDF 的 Skill 并安装。
|
|
17
|
+
|
|
18
|
+
`init` 做两件事:
|
|
19
|
+
|
|
20
|
+
1. 没有全局安装时,用 npm 全局安装本工具,之后终端里可以直接用 `funcoding`,或者简写 `fun`。加 `--no-global` 跳过这一步。
|
|
21
|
+
2. 给当前智能体装上 funcoding Skill([skill/funcoding/SKILL.md](skill/funcoding/SKILL.md))。Skill 里写明了:
|
|
22
|
+
- 什么时候用哪条命令
|
|
23
|
+
- 安装前先用 `skills info` 检查内容,并征得用户同意
|
|
24
|
+
- 缺少 MCP 密钥时向用户索取,不要编造
|
|
25
|
+
|
|
26
|
+
也可以自己全局安装:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm i -g funcoding-cli
|
|
30
|
+
fun --help
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
没有用简写 `fc`:它是 zsh 和 bash 的内建命令(编辑历史命令用),shell 会优先执行内建命令。
|
|
34
|
+
|
|
35
|
+
## 命令
|
|
36
|
+
|
|
37
|
+
格式参照 [android CLI](https://github.com/android/skills/blob/main/devtools/android-cli/SKILL.md):
|
|
38
|
+
|
|
39
|
+
- 顶层是 `init`、`update`、`info`、`help` 这几条。
|
|
40
|
+
- 其余命令按「名词 动词」分组。
|
|
41
|
+
- 每条命令都可以加 `--help`,查看它的用法、选项和位置参数,比如 `funcoding skills add --help`。
|
|
42
|
+
- 选项可以写成 `--agent=codex`,也可以写成 `--agent codex`。
|
|
43
|
+
|
|
44
|
+
| 命令 | 说明 |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `init` | 全局安装本工具,并给智能体装上 funcoding Skill |
|
|
47
|
+
| `update` | 把本工具更新到最新版本,并刷新已安装的 funcoding Skill |
|
|
48
|
+
| `info [<field>]` | 环境信息:版本、站点、是否全局安装、登录状态、本机的智能体和 Skill 目录、配置目录 |
|
|
49
|
+
| `help [<command>]` | 显示帮助 |
|
|
50
|
+
| `skills find <query>` | 查找 Skill |
|
|
51
|
+
| `skills info <id>` | 查看 Skill 的描述、文件清单和 SKILL.md 全文 |
|
|
52
|
+
| `skills add <id>...` | 安装一个或多个 Skill,也可以粘贴网站上的详情页地址 |
|
|
53
|
+
| `skills list` | 列出已安装的 Skill:默认列出全部智能体,包括用户目录和当前项目 |
|
|
54
|
+
| `skills update [<name>] [--check]` | 按原来源重新安装;`--check` 只比较远端的 SKILL.md,不安装 |
|
|
55
|
+
| `skills remove <name>...` | 删除通过 funcoding 安装的 Skill,删除前先备份;删除其他来源的 Skill 需要 `--force` |
|
|
56
|
+
| `mcp find <query>` | 查找 MCP Server |
|
|
57
|
+
| `mcp info <id>` | 查看启动方式(远程地址、npm 或 PyPI 包),以及需要的环境变量或请求头 |
|
|
58
|
+
| `mcp add <id>` | 写入智能体的 MCP 配置;必填项用 `--env=KEY=VALUE` 或 `--header=KEY=VALUE` 提供,`--via=remote\|npm\|pypi` 指定启动方式 |
|
|
59
|
+
| `mcp list` / `mcp remove <name>...` | 列出或删除 MCP 配置,修改前备份原文件 |
|
|
60
|
+
| `favorites list` | 列出网站上收藏的 Skill 和 MCP Server |
|
|
61
|
+
| `favorites sync [--dry-run]` | 安装全部收藏,已安装的跳过;需要密钥的 MCP 会列出缺少哪些值 |
|
|
62
|
+
| `auth login` / `auth logout` / `auth status` | 在浏览器中授权登录(设备码)、退出登录并使登录凭证失效、查看当前账号 |
|
|
63
|
+
| `news list` / `news daily [<YYYY-MM-DD>]` | AI 热榜,或某一天的 AI 日报 |
|
|
64
|
+
| `docs search <query> [--agent=<slug>]` | 搜索中文文档 |
|
|
65
|
+
| `docs fetch <agent>/<path>` | 读取文档的 Markdown 原文,开头附官方来源和核实日期 |
|
|
66
|
+
|
|
67
|
+
## 给智能体的约定
|
|
68
|
+
|
|
69
|
+
- **`--json`**:stdout 只输出一个 JSON 对象。
|
|
70
|
+
- 成功时为 `{ "ok": true, ... }`
|
|
71
|
+
- 失败时为 `{ "ok": false, "error": { "code", "message", "hint", ... } }`
|
|
72
|
+
- 过程提示一律写到 stderr。
|
|
73
|
+
- **不交互**:需要确认的操作通过参数表达,比如覆盖时加 `--force`,`favorites sync` 可以先加 `--dry-run` 预览。
|
|
74
|
+
- **`--agent`**:可选 `claude-code`、`codex`、`cursor`,多个用逗号分隔。`all` 表示本机检测到的全部智能体,检测方法是看用户目录下有没有对应的配置目录。不指定时,在 Claude Code 中运行(`CLAUDECODE=1`)默认装给 Claude Code,其他情况也默认 Claude Code。Codex 和 Cursor 没有文档化的环境变量,CLI 不去猜,需要显式指定。
|
|
75
|
+
- **范围**:`--project` 装到当前项目,不加时装到用户目录。`skills list`、`skills update`、`skills remove`、`mcp list`、`mcp remove` 默认同时查看两处,可以用 `--project` 或 `--user` 缩小范围。
|
|
76
|
+
|
|
77
|
+
| 退出码 | 含义 |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| 0 | 成功 |
|
|
80
|
+
| 1 | 一般错误 |
|
|
81
|
+
| 2 | 参数错误,或缺少必填项(`error.missing`) |
|
|
82
|
+
| 3 | 未找到(MCP 没有标准安装包时,`error.readme` 给出 README 地址) |
|
|
83
|
+
| 4 | 目标已存在,需要 `--force` |
|
|
84
|
+
| 5 | 未登录或登录已过期 |
|
|
85
|
+
|
|
86
|
+
## 安装位置
|
|
87
|
+
|
|
88
|
+
| `--agent` | Skill(用户目录 / 项目) | MCP(用户 / 项目) |
|
|
89
|
+
| --- | --- | --- |
|
|
90
|
+
| `claude-code` | `~/.claude/skills/<name>/` / `.claude/skills/<name>/` | 调用 `claude mcp add-json --scope user\|project`;本机没有 `claude` 命令时,项目范围直接写 `.mcp.json` |
|
|
91
|
+
| `codex` | `~/.agents/skills/<name>/` / `.agents/skills/<name>/` | `$CODEX_HOME/config.toml`(默认 `~/.codex/config.toml`)/ `.codex/config.toml`;不支持 SSE |
|
|
92
|
+
| `cursor` | `~/.cursor/skills/<name>/` / `.cursor/skills/<name>/` | `~/.cursor/mcp.json` / `.cursor/mcp.json` |
|
|
93
|
+
|
|
94
|
+
以上位置取自各智能体的官方文档(2026-10-07 核实),以官方为准。
|
|
95
|
+
|
|
96
|
+
## 安全
|
|
97
|
+
|
|
98
|
+
- Skill 会按 SKILL.md 的指示执行操作,可能运行其中的脚本,安装前请先阅读。
|
|
99
|
+
- CLI 只从 GitHub 下载 Skill 所在目录的文件,拒绝任何跳出目标目录的路径。单个 Skill 最多 200 个文件、10 MB。
|
|
100
|
+
- 改写 MCP 配置时只改动对应的那一项,其他内容原样保留。
|
|
101
|
+
- 覆盖、删除 Skill 和修改 MCP 配置前,都会把原内容备份到配置目录的 `backups/` 下。
|
|
102
|
+
|
|
103
|
+
登录凭证保存在 `~/.config/funcoding/credentials.json`(设置了 `XDG_CONFIG_HOME` 时放在它下面;Windows 是 `%APPDATA%\funcoding`),文件权限为 600。
|
|
104
|
+
|
|
105
|
+
环境变量:
|
|
106
|
+
|
|
107
|
+
- `FUNCODING_URL`:更换站点地址,本地开发时用 `http://localhost:5173`
|
|
108
|
+
- `FUNCODING_CONFIG_DIR`:更换配置目录
|
package/dist/agents.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 各智能体的 Skill 目录和 MCP 配置位置(对照官方文档核实,2026-10-07),Skill 目录和网站的 src/lib/agent-targets.ts 是同一份表:
|
|
3
|
+
* - Claude Code:Skill 在 ~/.claude/skills/<name>/、项目 .claude/skills/<name>/(code.claude.com/docs/en/skills);
|
|
4
|
+
* MCP 用 `claude mcp add-json <name> <json> --scope user|project`,项目范围写在 .mcp.json(code.claude.com/docs/en/mcp)
|
|
5
|
+
* - Codex:Skill 在 ~/.agents/skills/、项目 .agents/skills/(developers.openai.com/codex/skills);
|
|
6
|
+
* MCP 写在 $CODEX_HOME/config.toml(默认 ~/.codex/config.toml)或项目 .codex/config.toml 的 [mcp_servers.<name>],
|
|
7
|
+
* 支持 stdio 和 Streamable HTTP,不支持 SSE(developers.openai.com/codex/mcp)
|
|
8
|
+
* - Cursor:Skill 在 ~/.cursor/skills/、项目 .cursor/skills/(cursor.com/docs/skills);
|
|
9
|
+
* MCP 写在 ~/.cursor/mcp.json 或项目 .cursor/mcp.json 的 mcpServers(cursor.com/docs/mcp)
|
|
10
|
+
* 运行环境识别:只有 Claude Code 文档写明会给子进程设置 CLAUDECODE=1;Codex、Cursor 没有文档化的变量,不猜。
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { join, resolve } from 'node:path';
|
|
15
|
+
export const AGENTS = {
|
|
16
|
+
'claude-code': { label: 'Claude Code', home: '.claude', skills: ['.claude', 'skills'], invoke: (n) => `/${n}` },
|
|
17
|
+
codex: { label: 'Codex', home: '.codex', skills: ['.agents', 'skills'], invoke: (n) => `$${n}` },
|
|
18
|
+
cursor: { label: 'Cursor', home: '.cursor', skills: ['.cursor', 'skills'], invoke: (n) => `/${n}` },
|
|
19
|
+
};
|
|
20
|
+
export const AGENT_IDS = Object.keys(AGENTS);
|
|
21
|
+
export const DEFAULT_AGENT = 'claude-code';
|
|
22
|
+
export function isAgentId(s) {
|
|
23
|
+
return s in AGENTS;
|
|
24
|
+
}
|
|
25
|
+
/** 当前运行在哪个智能体里(只认文档化的环境变量) */
|
|
26
|
+
export function currentAgent({ env = process.env } = {}) {
|
|
27
|
+
return env.CLAUDECODE === '1' ? 'claude-code' : null;
|
|
28
|
+
}
|
|
29
|
+
/** Codex 的配置目录:CODEX_HOME,默认 ~/.codex */
|
|
30
|
+
export const codexHome = ({ env = process.env, home = homedir() } = {}) => env.CODEX_HOME || join(home, '.codex');
|
|
31
|
+
/** 本机装了哪些智能体:看用户目录下有没有它们的配置目录 */
|
|
32
|
+
export function installedAgents(e = {}) {
|
|
33
|
+
const home = e.home ?? homedir();
|
|
34
|
+
return AGENT_IDS.filter((a) => existsSync(a === 'codex' ? codexHome(e) : join(home, AGENTS[a].home)));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* --agent 的取值:显式指定 > 当前运行的智能体 > Claude Code;`all` 表示本机检测到的全部(一个都没检测到时退回默认)。
|
|
38
|
+
* 返回 null 表示取值不合法
|
|
39
|
+
*/
|
|
40
|
+
export function resolveAgents(value, e = {}) {
|
|
41
|
+
if (value === 'all') {
|
|
42
|
+
const found = installedAgents(e);
|
|
43
|
+
return found.length > 0 ? found : [currentAgent(e) ?? DEFAULT_AGENT];
|
|
44
|
+
}
|
|
45
|
+
if (value) {
|
|
46
|
+
const ids = value.split(',').map((s) => s.trim());
|
|
47
|
+
return ids.every(isAgentId) ? [...new Set(ids)] : null;
|
|
48
|
+
}
|
|
49
|
+
return [currentAgent(e) ?? DEFAULT_AGENT];
|
|
50
|
+
}
|
|
51
|
+
/** Skill 根目录:project 时是当前目录下的项目目录,否则是用户主目录下的 */
|
|
52
|
+
export function skillsRoot(agent, scope, e = {}) {
|
|
53
|
+
const dirs = AGENTS[agent].skills;
|
|
54
|
+
return scope === 'project' ? resolve(e.cwd ?? process.cwd(), ...dirs) : join(e.home ?? homedir(), ...dirs);
|
|
55
|
+
}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** 和 funcoding.ai 通信:地址可以用 FUNCODING_URL 改(本地开发时指向 http://localhost:5173) */
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { CliError } from './ui.js';
|
|
4
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
5
|
+
export const VERSION = pkg.version;
|
|
6
|
+
export const SITE = (process.env.FUNCODING_URL ?? 'https://funcoding.ai').replace(/\/+$/, '');
|
|
7
|
+
export const USER_AGENT = `funcoding-cli/${VERSION} (node ${process.versions.node}; ${process.platform})`;
|
|
8
|
+
const codeOf = (status) => (status === 401 ? 'auth' : status === 404 ? 'not_found' : status === 400 ? 'usage' : 'error');
|
|
9
|
+
/** 接口出错:按 HTTP 状态映射到 CLI 的错误码,body 留给需要细看的调用方(如设备码登录轮询) */
|
|
10
|
+
export class ApiError extends CliError {
|
|
11
|
+
status;
|
|
12
|
+
body;
|
|
13
|
+
constructor(message, status, body) {
|
|
14
|
+
super(codeOf(status), message, status === 401 ? '运行 funcoding auth login 重新登录' : undefined);
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.body = body;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function api(path, init = {}) {
|
|
20
|
+
const { token, headers, ...rest } = init;
|
|
21
|
+
let res;
|
|
22
|
+
try {
|
|
23
|
+
res = await fetch(`${SITE}${path}`, {
|
|
24
|
+
...rest,
|
|
25
|
+
headers: {
|
|
26
|
+
'User-Agent': USER_AGENT,
|
|
27
|
+
Accept: 'application/json',
|
|
28
|
+
...(rest.body ? { 'Content-Type': 'application/json' } : {}),
|
|
29
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
30
|
+
...headers,
|
|
31
|
+
},
|
|
32
|
+
signal: rest.signal ?? AbortSignal.timeout(30_000),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
throw new ApiError(`连不上 ${SITE}:${err.message}`, 0);
|
|
37
|
+
}
|
|
38
|
+
const text = await res.text();
|
|
39
|
+
let body = text;
|
|
40
|
+
try {
|
|
41
|
+
body = text ? JSON.parse(text) : null;
|
|
42
|
+
}
|
|
43
|
+
catch { }
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const b = body;
|
|
46
|
+
throw new ApiError(b?.error_description || b?.message || b?.error || `HTTP ${res.status}`, res.status, body);
|
|
47
|
+
}
|
|
48
|
+
return body;
|
|
49
|
+
}
|
|
50
|
+
/** 网站接口返回的完整网址(本地开发时跟随 FUNCODING_URL) */
|
|
51
|
+
export const siteUrl = (path) => new URL(path, `${SITE}/`).href;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 设备码登录(RFC 8628):CLI 申请验证码 → 用户在浏览器打开 /device/ 确认 → CLI 轮询拿到会话 token。
|
|
3
|
+
* 服务端是 better-auth 的 device-authorization 插件,client_id 固定为 funcoding-cli。
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { api, SITE } from './api.js';
|
|
7
|
+
import { clearCredentials, credentialsPath, loadCredentials, saveCredentials } from './credentials.js';
|
|
8
|
+
import { bold, cyan, dim, done, fail, green, info, warn } from './ui.js';
|
|
9
|
+
const CLIENT_ID = 'funcoding-cli';
|
|
10
|
+
const absolute = (u) => new URL(u, SITE).href;
|
|
11
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
12
|
+
/** 显示成 ABCD-EFGH,方便核对 */
|
|
13
|
+
const pretty = (code) => (code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code);
|
|
14
|
+
function openBrowser(url) {
|
|
15
|
+
const [cmd, args] = process.platform === 'darwin' ? ['open', [url]] : process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]] : ['xdg-open', [url]];
|
|
16
|
+
try {
|
|
17
|
+
spawn(cmd, args, { stdio: 'ignore', detached: true }).on('error', () => { }).unref();
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
}
|
|
21
|
+
export async function login(opts) {
|
|
22
|
+
const existing = await loadCredentials(SITE);
|
|
23
|
+
if (existing) {
|
|
24
|
+
const me = await fetchMe(existing.token).catch(() => null);
|
|
25
|
+
if (me) {
|
|
26
|
+
return done({ user: me.user, alreadyLoggedIn: true }, () => info(`已经登录:${bold(me.user.name)}${me.user.githubLogin ? dim(` (@${me.user.githubLogin})`) : ''}。要换账号先运行 ${cyan('funcoding auth logout')}。`));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const code = await api('/api/auth/device/code', { method: 'POST', body: JSON.stringify({ client_id: CLIENT_ID }) });
|
|
30
|
+
const url = absolute(code.verification_uri_complete ?? `${code.verification_uri}?user_code=${code.user_code}`);
|
|
31
|
+
info('');
|
|
32
|
+
info(` 在浏览器里确认这个验证码:${bold(pretty(code.user_code))}`);
|
|
33
|
+
info(` ${cyan(url)}`);
|
|
34
|
+
info('');
|
|
35
|
+
if (opts.browser)
|
|
36
|
+
openBrowser(url);
|
|
37
|
+
info(dim('等待浏览器里确认…(Ctrl+C 取消)'));
|
|
38
|
+
let interval = Math.max(code.interval ?? 5, 1) * 1000;
|
|
39
|
+
const deadline = Date.now() + code.expires_in * 1000;
|
|
40
|
+
while (Date.now() < deadline) {
|
|
41
|
+
await sleep(interval);
|
|
42
|
+
try {
|
|
43
|
+
const token = await api('/api/auth/device/token', {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
body: JSON.stringify({ grant_type: 'urn:ietf:params:oauth:grant-type:device_code', device_code: code.device_code, client_id: CLIENT_ID }),
|
|
46
|
+
});
|
|
47
|
+
const me = await fetchMe(token.access_token);
|
|
48
|
+
await saveCredentials({ token: token.access_token, site: SITE, user: { name: me.user.name, githubLogin: me.user.githubLogin }, savedAt: new Date().toISOString() });
|
|
49
|
+
return done({ user: me.user, credentials: credentialsPath() }, () => {
|
|
50
|
+
info(green(`✓ 已登录:${me.user.name}${me.user.githubLogin ? ` (@${me.user.githubLogin})` : ''}`));
|
|
51
|
+
info(dim(`凭证保存在 ${credentialsPath()}`));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
const e = err.body;
|
|
56
|
+
if (e?.error === 'authorization_pending')
|
|
57
|
+
continue;
|
|
58
|
+
if (e?.error === 'slow_down') {
|
|
59
|
+
interval += 5000;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (e?.error === 'access_denied')
|
|
63
|
+
fail('auth', '你在浏览器里拒绝了这次登录。');
|
|
64
|
+
if (e?.error === 'expired_token')
|
|
65
|
+
fail('auth', '验证码过期了。', '重新运行 funcoding auth login');
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
fail('auth', '验证码过期了。', '重新运行 funcoding auth login');
|
|
70
|
+
}
|
|
71
|
+
export async function logout() {
|
|
72
|
+
const c = await loadCredentials(SITE);
|
|
73
|
+
if (!c)
|
|
74
|
+
return done({ loggedOut: true, wasLoggedIn: false }, () => info('当前没有登录。'));
|
|
75
|
+
// 顺便让服务端的会话失效;失败也照样删除本地凭证
|
|
76
|
+
await api('/api/auth/sign-out', { method: 'POST', token: c.token, body: '{}' }).catch(() => warn('没能通知服务端退出,本地凭证已删除。'));
|
|
77
|
+
await clearCredentials();
|
|
78
|
+
done({ loggedOut: true, wasLoggedIn: true }, () => info(green('✓ 已退出登录')));
|
|
79
|
+
}
|
|
80
|
+
export async function fetchMe(token) {
|
|
81
|
+
return api('/api/cli/me', { token });
|
|
82
|
+
}
|
|
83
|
+
/** 需要登录的命令:没登录直接报错 */
|
|
84
|
+
export async function requireToken() {
|
|
85
|
+
const c = await loadCredentials(SITE);
|
|
86
|
+
if (!c)
|
|
87
|
+
return fail('auth', '还没有登录。', '运行 funcoding auth login');
|
|
88
|
+
return c.token;
|
|
89
|
+
}
|
|
90
|
+
export async function status() {
|
|
91
|
+
const c = await loadCredentials(SITE);
|
|
92
|
+
if (!c)
|
|
93
|
+
return fail('auth', '未登录。', '运行 funcoding auth login');
|
|
94
|
+
const me = await fetchMe(c.token);
|
|
95
|
+
done({ user: me.user, favorites: { skills: me.favorites.skills.length, mcp: me.favorites.mcp.length }, site: SITE }, () => {
|
|
96
|
+
info(`${bold(me.user.name)}${me.user.githubLogin ? dim(` (@${me.user.githubLogin})`) : ''}`);
|
|
97
|
+
info(dim(`${me.favorites.skills.length} 个收藏的 Skill,${me.favorites.mcp.length} 个收藏的 MCP · ${SITE}`));
|
|
98
|
+
});
|
|
99
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* funcoding-cli:给 AI 编程智能体用的 funcoding.ai 命令行(人也能用),命令是 funcoding,简写 fun。
|
|
4
|
+
* 命令格式参照 android CLI:顶层是 init / update / info / help,其余按「名词 动词」分组(skills add、mcp list…),
|
|
5
|
+
* 每条命令都有自己的 --help。命令表 COMMANDS 同时用来解析参数和生成帮助
|
|
6
|
+
*/
|
|
7
|
+
import { parseArgs } from 'node:util';
|
|
8
|
+
import { AGENT_IDS, resolveAgents } from './agents.js';
|
|
9
|
+
import { SITE, VERSION } from './api.js';
|
|
10
|
+
import { login, logout, status } from './auth.js';
|
|
11
|
+
import { docsFetch, docsSearch, find, newsDaily, newsList } from './content.js';
|
|
12
|
+
import { envInfo } from './info.js';
|
|
13
|
+
import { init, selfUpdate } from './init.js';
|
|
14
|
+
import { install, list, remove, skillInfo, update } from './install.js';
|
|
15
|
+
import { mcpAdd, mcpInfo, mcpList, mcpRemove } from './mcp.js';
|
|
16
|
+
import { favorites, sync } from './sync.js';
|
|
17
|
+
import { bold, CliError, dim, fail, info, reportError, setJsonMode } from './ui.js';
|
|
18
|
+
/** 帮助和提示里统一写全名;fun 是同一个命令的简写 */
|
|
19
|
+
const BIN = 'funcoding';
|
|
20
|
+
const OPTIONS = {
|
|
21
|
+
json: { type: 'boolean', flag: '--json', desc: '只向 stdout 输出一个 JSON 对象(给智能体用)' },
|
|
22
|
+
help: { type: 'boolean', short: 'h', flag: '-h, --help', desc: '显示这条命令的帮助' },
|
|
23
|
+
version: { type: 'boolean', short: 'V', flag: '-V, --version', desc: '显示版本' },
|
|
24
|
+
agent: { type: 'string', short: 'a', flag: '-a, --agent=<name>', desc: `目标智能体:${AGENT_IDS.join('、')},多个用逗号分隔,all 表示本机检测到的全部(默认:当前所在的智能体,否则 claude-code)` },
|
|
25
|
+
'scope-agent': { type: 'string', short: 'a', flag: '-a, --agent=<name>', desc: `只看这些智能体:${AGENT_IDS.join('、')},多个用逗号分隔(默认全部)` },
|
|
26
|
+
'doc-agent': { type: 'string', short: 'a', flag: '-a, --agent=<slug>', desc: '只搜这个智能体的文档,比如 claude-code、codex、gemini-cli' },
|
|
27
|
+
project: { type: 'boolean', short: 'p', flag: '-p, --project', desc: '装到当前项目(可随仓库提交),不加时装到用户目录' },
|
|
28
|
+
'only-project': { type: 'boolean', short: 'p', flag: '-p, --project', desc: '只看当前项目' },
|
|
29
|
+
user: { type: 'boolean', short: 'u', flag: '-u, --user', desc: '只看用户目录(默认用户目录和当前项目都看)' },
|
|
30
|
+
force: { type: 'boolean', short: 'f', flag: '-f, --force', desc: '目标已存在时覆盖(原内容先备份到配置目录)' },
|
|
31
|
+
env: { type: 'string', short: 'e', multiple: true, flag: '-e, --env=<KEY=VALUE>', desc: 'stdio Server 的环境变量,可重复' },
|
|
32
|
+
header: { type: 'string', multiple: true, flag: '--header=<KEY=VALUE>', desc: '远程 Server 的请求头,可重复' },
|
|
33
|
+
via: { type: 'string', flag: '--via=<kind>', desc: '指定启动方式:remote、npm 或 pypi' },
|
|
34
|
+
name: { type: 'string', flag: '--name=<name>', desc: '自定义配置名(默认取自 Server 名称)' },
|
|
35
|
+
'dry-run': { type: 'boolean', flag: '--dry-run', desc: '只列出将要执行的操作' },
|
|
36
|
+
check: { type: 'boolean', flag: '--check', desc: '只检查远端有没有更新,不安装' },
|
|
37
|
+
limit: { type: 'string', flag: '--limit=<n>', desc: '返回条数(1–30,默认 10)' },
|
|
38
|
+
'no-browser': { type: 'boolean', flag: '--no-browser', desc: '不自动打开浏览器' },
|
|
39
|
+
'no-global': { type: 'boolean', flag: '--no-global', desc: '不全局安装,只装 funcoding Skill' },
|
|
40
|
+
};
|
|
41
|
+
const GLOBAL = ['json', 'help', 'version'];
|
|
42
|
+
const str = (v) => (typeof v === 'string' ? v : undefined);
|
|
43
|
+
const limitOf = (v) => Math.min(30, Math.max(1, Number(str(v.limit)) || 10));
|
|
44
|
+
const need = (args, what, example) => {
|
|
45
|
+
if (!args[0])
|
|
46
|
+
fail('usage', `缺少${what}。`, `例如 ${example}`);
|
|
47
|
+
return args;
|
|
48
|
+
};
|
|
49
|
+
/** 写入类命令的目标:智能体 × 一个范围 */
|
|
50
|
+
function targets(v) {
|
|
51
|
+
const agents = resolveAgents(str(v.agent));
|
|
52
|
+
if (!agents)
|
|
53
|
+
return fail('usage', `不认识的智能体:${str(v.agent)}`, `可选:${AGENT_IDS.join('、')}、all`);
|
|
54
|
+
const scope = v.project ? 'project' : 'user';
|
|
55
|
+
return agents.map((agent) => ({ agent, scope }));
|
|
56
|
+
}
|
|
57
|
+
/** 读取和维护类命令的范围:默认全部智能体、两个范围 */
|
|
58
|
+
function selection(v) {
|
|
59
|
+
const agents = str(v.agent) ? resolveAgents(str(v.agent)) : AGENT_IDS;
|
|
60
|
+
if (!agents)
|
|
61
|
+
return fail('usage', `不认识的智能体:${str(v.agent)}`, `可选:${AGENT_IDS.join('、')}、all`);
|
|
62
|
+
const scopes = v.project && !v.user ? ['project'] : v.user && !v.project ? ['user'] : ['user', 'project'];
|
|
63
|
+
return { agents, scopes };
|
|
64
|
+
}
|
|
65
|
+
/** KEY=VALUE 列表转成对象 */
|
|
66
|
+
function pairs(list, flag) {
|
|
67
|
+
const out = {};
|
|
68
|
+
for (const item of Array.isArray(list) ? list : []) {
|
|
69
|
+
const i = item.indexOf('=');
|
|
70
|
+
if (i <= 0)
|
|
71
|
+
fail('usage', `${flag} 要写成 KEY=VALUE:${item}`);
|
|
72
|
+
out[item.slice(0, i)] = item.slice(i + 1);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
const COMMANDS = [
|
|
77
|
+
{
|
|
78
|
+
name: 'init',
|
|
79
|
+
summary: '初始化:全局安装本工具(之后可用 funcoding 或 fun),并给智能体装上 funcoding Skill',
|
|
80
|
+
options: ['agent', 'project', 'force', 'no-global'],
|
|
81
|
+
examples: ['npx --yes funcoding-cli@latest init', `${BIN} init --agent=all`],
|
|
82
|
+
run: ({ values: v }) => init(targets(v), !!v.force, !v['no-global']),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'update',
|
|
86
|
+
summary: '把本工具更新到最新版本,并刷新已安装的 funcoding Skill',
|
|
87
|
+
run: () => selfUpdate(),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'info',
|
|
91
|
+
summary: '环境信息:版本、站点、登录状态、本机的智能体和 Skill 目录、配置目录',
|
|
92
|
+
params: [['<field>', '只输出这一项,比如 version、user、agents;不写时输出全部']],
|
|
93
|
+
run: ({ args }) => envInfo(args[0]),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'help',
|
|
97
|
+
summary: '显示帮助',
|
|
98
|
+
params: [['<command>', '要查看帮助的命令,比如 skills add']],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'skills',
|
|
102
|
+
summary: '查找、安装和管理 Agent Skills',
|
|
103
|
+
commands: [
|
|
104
|
+
{ name: 'find', summary: '按关键词查找 Skill', params: [['<query>', '关键词']], options: ['limit'], run: ({ args, values }) => find('skill', args.join(' '), limitOf(values)) },
|
|
105
|
+
{
|
|
106
|
+
name: 'info',
|
|
107
|
+
summary: '查看 Skill 的描述、文件清单和 SKILL.md 全文(安装前检查用)',
|
|
108
|
+
params: [['<id>', 'owner/repo/skill,或网站上的详情页地址']],
|
|
109
|
+
run: ({ args }) => skillInfo(need(args, ' Skill', `${BIN} skills info anthropics/skills/pdf`)[0]),
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'add',
|
|
113
|
+
summary: '安装一个或多个 Skill',
|
|
114
|
+
params: [['<id>...', 'owner/repo/skill,或网站上的详情页地址']],
|
|
115
|
+
options: ['agent', 'project', 'force'],
|
|
116
|
+
examples: [`${BIN} skills add anthropics/skills/pdf`, `${BIN} skills add anthropics/skills/pdf --agent=codex,cursor --project`],
|
|
117
|
+
run: ({ args, values: v }) => install(need(args, '要安装的 Skill', `${BIN} skills add anthropics/skills/pdf`), targets(v), !!v.force),
|
|
118
|
+
},
|
|
119
|
+
{ name: 'list', summary: '列出已安装的 Skill', options: ['scope-agent', 'only-project', 'user'], run: ({ values }) => list(selection(values)) },
|
|
120
|
+
{
|
|
121
|
+
name: 'update',
|
|
122
|
+
summary: '按原来源重新安装由 funcoding 安装的 Skill',
|
|
123
|
+
params: [['<name>', '目录名或 Skill id;不写时更新全部']],
|
|
124
|
+
options: ['check', 'scope-agent', 'only-project', 'user'],
|
|
125
|
+
run: ({ args, values: v }) => update(args[0], selection(v), !!v.check),
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'remove',
|
|
129
|
+
summary: '删除由 funcoding 安装的 Skill(先备份)',
|
|
130
|
+
params: [['<name>...', '目录名或 Skill id']],
|
|
131
|
+
options: ['force', 'scope-agent', 'only-project', 'user'],
|
|
132
|
+
run: ({ args, values: v }) => remove(need(args, '要删除的 Skill', `${BIN} skills remove pdf`), selection(v), !!v.force),
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'mcp',
|
|
138
|
+
summary: '查找 MCP Server,并写入各智能体的 MCP 配置',
|
|
139
|
+
commands: [
|
|
140
|
+
{ name: 'find', summary: '按关键词查找 MCP Server', params: [['<query>', '关键词']], options: ['limit'], run: ({ args, values }) => find('mcp', args.join(' '), limitOf(values)) },
|
|
141
|
+
{
|
|
142
|
+
name: 'info',
|
|
143
|
+
summary: '查看启动方式,以及需要提供的环境变量或请求头',
|
|
144
|
+
params: [['<id>', 'owner/repo 或 owner/repo/sub,或网站上的详情页地址']],
|
|
145
|
+
run: ({ args }) => mcpInfo(need(args, ' MCP Server', `${BIN} mcp info github/github-mcp-server`)[0]),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: 'add',
|
|
149
|
+
summary: '把 MCP Server 写入智能体的配置',
|
|
150
|
+
params: [['<id>', 'owner/repo 或 owner/repo/sub,或网站上的详情页地址']],
|
|
151
|
+
options: ['agent', 'project', 'force', 'env', 'header', 'via', 'name'],
|
|
152
|
+
examples: [`${BIN} mcp add vexa-ai/vexa --header=Authorization="Bearer <key>" --agent=cursor`],
|
|
153
|
+
run: ({ args, values: v }) => {
|
|
154
|
+
const via = str(v.via);
|
|
155
|
+
if (via && !['remote', 'npm', 'pypi'].includes(via))
|
|
156
|
+
fail('usage', `--via 只能是 remote、npm 或 pypi:${via}`);
|
|
157
|
+
return mcpAdd(need(args, ' MCP Server', `${BIN} mcp add github/github-mcp-server`)[0], {
|
|
158
|
+
targets: targets(v),
|
|
159
|
+
env: pairs(v.env, '--env'),
|
|
160
|
+
headers: pairs(v.header, '--header'),
|
|
161
|
+
via: via,
|
|
162
|
+
name: str(v.name),
|
|
163
|
+
force: !!v.force,
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{ name: 'list', summary: '列出已配置的 MCP Server', options: ['scope-agent', 'only-project', 'user'], run: ({ values }) => mcpList(selection(values)) },
|
|
168
|
+
{
|
|
169
|
+
name: 'remove',
|
|
170
|
+
summary: '删除 MCP Server 配置(先备份原文件)',
|
|
171
|
+
params: [['<name>...', '配置名,见 mcp list']],
|
|
172
|
+
options: ['scope-agent', 'only-project', 'user'],
|
|
173
|
+
run: ({ args, values }) => mcpRemove(need(args, '要删除的配置名', `${BIN} mcp remove vexa`), selection(values)),
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: 'favorites',
|
|
179
|
+
summary: '网站上收藏的 Skill 和 MCP Server(需要登录)',
|
|
180
|
+
commands: [
|
|
181
|
+
{ name: 'list', summary: '列出收藏', run: () => favorites() },
|
|
182
|
+
{ name: 'sync', summary: '安装全部收藏,已安装的跳过', options: ['agent', 'project', 'dry-run'], run: ({ values: v }) => sync(targets(v), !!v['dry-run']) },
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'auth',
|
|
187
|
+
summary: '登录 funcoding.ai',
|
|
188
|
+
commands: [
|
|
189
|
+
{ name: 'login', summary: '在浏览器中授权登录(设备码)', options: ['no-browser'], run: ({ values }) => login({ browser: !values['no-browser'] }) },
|
|
190
|
+
{ name: 'logout', summary: '退出登录,并使登录凭证失效', run: () => logout() },
|
|
191
|
+
{ name: 'status', summary: '查看当前登录的账号', run: () => status() },
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'news',
|
|
196
|
+
summary: 'AI 热点',
|
|
197
|
+
commands: [
|
|
198
|
+
{ name: 'list', summary: '当前 AI 热榜:多家来源报道的事件,按热度排序', options: ['limit'], run: ({ values }) => newsList(limitOf(values)) },
|
|
199
|
+
{ name: 'daily', summary: '某一天的 AI 日报', params: [['<date>', 'YYYY-MM-DD;不写时为最新一期']], run: ({ args }) => newsDaily(args[0]) },
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'docs',
|
|
204
|
+
summary: 'AI 编程智能体的中文文档',
|
|
205
|
+
commands: [
|
|
206
|
+
{ name: 'search', summary: '搜索文档', params: [['<query>', '关键词']], options: ['doc-agent', 'limit'], run: ({ args, values }) => docsSearch(args.join(' '), str(values.agent)?.toLowerCase(), limitOf(values)) },
|
|
207
|
+
{
|
|
208
|
+
name: 'fetch',
|
|
209
|
+
summary: '读取文档全文(Markdown),开头附官方来源和核实日期',
|
|
210
|
+
params: [['<path>', '<智能体>/<路径>,比如 claude-code/build/hooks;也可以是网站上的文档地址']],
|
|
211
|
+
run: ({ args }) => docsFetch(need(args, '文档路径', `${BIN} docs fetch claude-code/build/hooks`)[0]),
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
];
|
|
216
|
+
// ---------------- 帮助 ----------------
|
|
217
|
+
const pad = (rows, indent = ' ') => {
|
|
218
|
+
const w = Math.max(...rows.map(([a]) => a.length)) + 2;
|
|
219
|
+
return rows.map(([a, b]) => `${indent}${a.padEnd(w)}${b}`).join('\n');
|
|
220
|
+
};
|
|
221
|
+
function usageLine(path) {
|
|
222
|
+
const cmd = path.at(-1);
|
|
223
|
+
const names = path.map((c) => c.name).join(' ');
|
|
224
|
+
if (!cmd)
|
|
225
|
+
return `${BIN} [-hV] [--json] <command>`;
|
|
226
|
+
if (cmd.commands)
|
|
227
|
+
return `${BIN} ${names} <command> [options]`;
|
|
228
|
+
const opts = (cmd.options ?? []).map((k) => `[${OPTIONS[k].flag.split(', ').pop()}]`).join(' ');
|
|
229
|
+
const params = (cmd.params ?? []).map(([p]) => (cmd.name === 'help' || cmd.name === 'info' || p === '<name>' || p === '<date>' ? `[${p}]` : p)).join(' ');
|
|
230
|
+
return [BIN, names, opts, params].filter(Boolean).join(' ');
|
|
231
|
+
}
|
|
232
|
+
function helpText(path) {
|
|
233
|
+
const cmd = path.at(-1);
|
|
234
|
+
const out = [`${bold('Usage:')} ${usageLine(path)}`];
|
|
235
|
+
if (!cmd) {
|
|
236
|
+
out.push(`${dim(`funcoding v${VERSION}`)} — 从 ${SITE} 查找和安装 Agent Skills、配置 MCP Server、查看 AI 热点和中文文档`);
|
|
237
|
+
out.push(dim('可以简写为 fun,比如 fun skills find pdf;给智能体用时加 --json'));
|
|
238
|
+
}
|
|
239
|
+
else
|
|
240
|
+
out.push(cmd.summary);
|
|
241
|
+
const options = cmd?.options ?? [];
|
|
242
|
+
if (options.length)
|
|
243
|
+
out.push('', bold('Options:'), pad(options.map((k) => [OPTIONS[k].flag, OPTIONS[k].desc])));
|
|
244
|
+
out.push('', bold(cmd ? 'Global options:' : 'Options:'), pad(GLOBAL.map((k) => [OPTIONS[k].flag, OPTIONS[k].desc])));
|
|
245
|
+
if (cmd?.params)
|
|
246
|
+
out.push('', bold('Positional parameters:'), pad(cmd.params));
|
|
247
|
+
const subs = cmd ? cmd.commands : COMMANDS;
|
|
248
|
+
if (subs)
|
|
249
|
+
out.push('', bold('Commands:'), pad(subs.map((c) => [c.name, c.summary])));
|
|
250
|
+
if (cmd?.examples)
|
|
251
|
+
out.push('', bold('Examples:'), cmd.examples.map((e) => ` ${e}`).join('\n'));
|
|
252
|
+
if (!cmd) {
|
|
253
|
+
out.push('', bold('Exit codes:'), ' 0 成功 1 一般错误 2 参数错误或缺少必填项 3 未找到 4 目标已存在 5 未登录或登录已过期');
|
|
254
|
+
out.push('', `运行 ${BIN} <command> --help 查看每条命令的用法。`);
|
|
255
|
+
}
|
|
256
|
+
return out.join('\n');
|
|
257
|
+
}
|
|
258
|
+
/** 从位置参数里找出命令路径,剩下的是这条命令的参数 */
|
|
259
|
+
function resolve(words) {
|
|
260
|
+
const path = [];
|
|
261
|
+
let list = COMMANDS;
|
|
262
|
+
let i = 0;
|
|
263
|
+
while (list && i < words.length) {
|
|
264
|
+
const c = list.find((x) => x.name === words[i]);
|
|
265
|
+
if (!c) {
|
|
266
|
+
const where = path.map((p) => p.name).join(' ');
|
|
267
|
+
fail('usage', `不认识的命令:${[where, words[i]].filter(Boolean).join(' ')}`, `运行 ${BIN} ${where ? `${where} ` : ''}--help 查看可用命令`);
|
|
268
|
+
}
|
|
269
|
+
path.push(c);
|
|
270
|
+
list = c.commands;
|
|
271
|
+
i++;
|
|
272
|
+
}
|
|
273
|
+
return { path, rest: words.slice(i) };
|
|
274
|
+
}
|
|
275
|
+
function parserConfig(keys) {
|
|
276
|
+
const out = {};
|
|
277
|
+
for (const k of keys) {
|
|
278
|
+
const { type, ...o } = OPTIONS[k];
|
|
279
|
+
// scope-agent / doc-agent 等是同一个 --agent 的不同说明
|
|
280
|
+
const name = k === 'scope-agent' || k === 'doc-agent' ? 'agent' : k === 'only-project' ? 'project' : k;
|
|
281
|
+
out[name] = { type, ...(o.short ? { short: o.short } : {}), ...(o.multiple ? { multiple: true } : {}) };
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
// ---------------- 入口 ----------------
|
|
286
|
+
async function main() {
|
|
287
|
+
const argv = process.argv.slice(2);
|
|
288
|
+
// 参数解析出错时也要按 JSON 输出
|
|
289
|
+
setJsonMode(argv.includes('--json'));
|
|
290
|
+
// 先宽松解析一遍,找出命令路径
|
|
291
|
+
const loose = parseArgs({ args: argv, allowPositionals: true, strict: false, options: parserConfig(Object.keys(OPTIONS)) });
|
|
292
|
+
const { path, rest } = resolve(loose.positionals);
|
|
293
|
+
const cmd = path.at(-1);
|
|
294
|
+
if (cmd?.name === 'help' && path.length === 1)
|
|
295
|
+
return info(helpText(resolve(rest).path));
|
|
296
|
+
// 再按这条命令允许的选项严格解析
|
|
297
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: parserConfig([...GLOBAL, ...(cmd?.options ?? [])]) });
|
|
298
|
+
const values = parsed.values;
|
|
299
|
+
const positionals = parsed.positionals;
|
|
300
|
+
if (values.version)
|
|
301
|
+
return info(VERSION);
|
|
302
|
+
if (values.help || !cmd?.run)
|
|
303
|
+
return info(helpText(path));
|
|
304
|
+
await cmd.run({ args: positionals.slice(path.length), values });
|
|
305
|
+
}
|
|
306
|
+
main().catch((err) => {
|
|
307
|
+
if (err instanceof CliError)
|
|
308
|
+
return reportError(err);
|
|
309
|
+
// parseArgs 的参数错误,比如这条命令不支持的选项
|
|
310
|
+
if (err instanceof TypeError && err.code?.startsWith('ERR_PARSE_ARGS')) {
|
|
311
|
+
const unknown = /Unknown option '([^']+)'/.exec(err.message)?.[1];
|
|
312
|
+
const missing = /Option '([^']+)' argument missing/.exec(err.message)?.[1];
|
|
313
|
+
const message = unknown ? `这条命令不支持选项 ${unknown}` : missing ? `选项 ${missing.split(', ').pop()?.replace(' <value>', '')} 缺少取值` : err.message;
|
|
314
|
+
return reportError(new CliError('usage', message, `运行 ${BIN} <command> --help 查看这条命令支持的选项`));
|
|
315
|
+
}
|
|
316
|
+
reportError(new CliError('error', err instanceof Error ? err.message : String(err)));
|
|
317
|
+
});
|