funcoding-cli 0.2.0 → 0.3.0
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 +42 -25
- package/dist/api.js +1 -1
- package/dist/auth.js +6 -6
- package/dist/cli.js +308 -191
- package/dist/content.js +55 -43
- package/dist/info.js +40 -0
- package/dist/init.js +90 -0
- package/dist/install.js +8 -8
- package/dist/lists.js +42 -0
- package/dist/mcp.js +1 -1
- package/dist/sync.js +24 -15
- package/package.json +3 -2
- package/skill/funcoding/SKILL.md +72 -29
- package/dist/setup.js +0 -49
package/dist/cli.js
CHANGED
|
@@ -1,75 +1,72 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* funcoding-cli:给 AI 编程智能体用的 funcoding.ai 命令行(人也能用),命令是 funcoding,简写 fun。
|
|
4
|
+
* 命令格式参照 android CLI:顶层是 init / update / info / help,其余按「名词 动词」分组(skills add、mcp list…),
|
|
5
|
+
* 每条命令都有自己的 --help。命令表 COMMANDS 同时用来解析参数和生成帮助
|
|
6
|
+
*/
|
|
3
7
|
import { parseArgs } from 'node:util';
|
|
4
|
-
import { AGENT_IDS,
|
|
8
|
+
import { AGENT_IDS, resolveAgents } from './agents.js';
|
|
5
9
|
import { SITE, VERSION } from './api.js';
|
|
6
|
-
import { login, logout,
|
|
7
|
-
import {
|
|
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';
|
|
8
14
|
import { install, list, remove, skillInfo, update } from './install.js';
|
|
9
15
|
import { mcpAdd, mcpInfo, mcpList, mcpRemove } from './mcp.js';
|
|
10
|
-
import {
|
|
16
|
+
import { listAdd, listInfo } from './lists.js';
|
|
11
17
|
import { favorites, sync } from './sync.js';
|
|
12
18
|
import { bold, CliError, dim, fail, info, reportError, setJsonMode } from './ui.js';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
${bold('退出码')}
|
|
61
|
-
0 成功 1 一般错误 2 参数错误或缺少必填项 3 未找到 4 目标已存在 5 未登录或登录已过期
|
|
62
|
-
|
|
63
|
-
${bold('例子')}
|
|
64
|
-
npx funcoding-cli search pdf --json
|
|
65
|
-
npx funcoding-cli info anthropics/skills/pdf --json
|
|
66
|
-
npx funcoding-cli install anthropics/skills/pdf --agent codex --project
|
|
67
|
-
npx funcoding-cli mcp add upstash/context7 --agent all
|
|
68
|
-
`;
|
|
19
|
+
/** 帮助和提示里统一写全名;fun 是同一个命令的简写 */
|
|
20
|
+
const BIN = 'funcoding';
|
|
21
|
+
const OPTIONS = {
|
|
22
|
+
json: { type: 'boolean', flag: '--json', desc: '只向 stdout 输出一个 JSON 对象(给智能体用)' },
|
|
23
|
+
help: { type: 'boolean', short: 'h', flag: '-h, --help', desc: '显示这条命令的帮助' },
|
|
24
|
+
version: { type: 'boolean', short: 'V', flag: '-V, --version', desc: '显示版本' },
|
|
25
|
+
agent: { type: 'string', short: 'a', flag: '-a, --agent=<name>', desc: `目标智能体:${AGENT_IDS.join('、')},多个用逗号分隔,all 表示本机检测到的全部(默认:当前所在的智能体,否则 claude-code)` },
|
|
26
|
+
'scope-agent': { type: 'string', short: 'a', flag: '-a, --agent=<name>', desc: `只看这些智能体:${AGENT_IDS.join('、')},多个用逗号分隔(默认全部)` },
|
|
27
|
+
'doc-agent': { type: 'string', short: 'a', flag: '-a, --agent=<slug>', desc: '只搜这个智能体的文档,比如 claude-code、codex、gemini-cli' },
|
|
28
|
+
project: { type: 'boolean', short: 'p', flag: '-p, --project', desc: '装到当前项目(可随仓库提交),不加时装到用户目录' },
|
|
29
|
+
'only-project': { type: 'boolean', short: 'p', flag: '-p, --project', desc: '只看当前项目' },
|
|
30
|
+
user: { type: 'boolean', short: 'u', flag: '-u, --user', desc: '只看用户目录(默认用户目录和当前项目都看)' },
|
|
31
|
+
force: { type: 'boolean', short: 'f', flag: '-f, --force', desc: '目标已存在时覆盖(原内容先备份到配置目录)' },
|
|
32
|
+
env: { type: 'string', short: 'e', multiple: true, flag: '-e, --env=<KEY=VALUE>', desc: 'stdio Server 的环境变量,可重复' },
|
|
33
|
+
header: { type: 'string', multiple: true, flag: '--header=<KEY=VALUE>', desc: '远程 Server 的请求头,可重复' },
|
|
34
|
+
via: { type: 'string', flag: '--via=<kind>', desc: '指定启动方式:remote、npm 或 pypi' },
|
|
35
|
+
name: { type: 'string', flag: '--name=<name>', desc: '自定义配置名(默认取自 Server 名称)' },
|
|
36
|
+
'dry-run': { type: 'boolean', flag: '--dry-run', desc: '只列出将要执行的操作' },
|
|
37
|
+
check: { type: 'boolean', flag: '--check', desc: '只检查远端有没有更新,不安装' },
|
|
38
|
+
limit: { type: 'string', flag: '--limit=<n>', desc: '返回条数(1–30,默认 10)' },
|
|
39
|
+
'no-browser': { type: 'boolean', flag: '--no-browser', desc: '不自动打开浏览器' },
|
|
40
|
+
'no-global': { type: 'boolean', flag: '--no-global', desc: '不全局安装,只装 funcoding Skill' },
|
|
41
|
+
};
|
|
42
|
+
const GLOBAL = ['json', 'help', 'version'];
|
|
43
|
+
const str = (v) => (typeof v === 'string' ? v : undefined);
|
|
44
|
+
const limitOf = (v) => Math.min(30, Math.max(1, Number(str(v.limit)) || 10));
|
|
45
|
+
const need = (args, what, example) => {
|
|
46
|
+
if (!args[0])
|
|
47
|
+
fail('usage', `缺少${what}。`, `例如 ${example}`);
|
|
48
|
+
return args;
|
|
49
|
+
};
|
|
50
|
+
/** 写入类命令的目标:智能体 × 一个范围 */
|
|
51
|
+
function targets(v) {
|
|
52
|
+
const agents = resolveAgents(str(v.agent));
|
|
53
|
+
if (!agents)
|
|
54
|
+
return fail('usage', `不认识的智能体:${str(v.agent)}`, `可选:${AGENT_IDS.join('、')}、all`);
|
|
55
|
+
const scope = v.project ? 'project' : 'user';
|
|
56
|
+
return agents.map((agent) => ({ agent, scope }));
|
|
57
|
+
}
|
|
58
|
+
/** 读取和维护类命令的范围:默认全部智能体、两个范围 */
|
|
59
|
+
function selection(v) {
|
|
60
|
+
const agents = str(v.agent) ? resolveAgents(str(v.agent)) : AGENT_IDS;
|
|
61
|
+
if (!agents)
|
|
62
|
+
return fail('usage', `不认识的智能体:${str(v.agent)}`, `可选:${AGENT_IDS.join('、')}、all`);
|
|
63
|
+
const scopes = v.project && !v.user ? ['project'] : v.user && !v.project ? ['user'] : ['user', 'project'];
|
|
64
|
+
return { agents, scopes };
|
|
65
|
+
}
|
|
69
66
|
/** KEY=VALUE 列表转成对象 */
|
|
70
67
|
function pairs(list, flag) {
|
|
71
68
|
const out = {};
|
|
72
|
-
for (const item of list
|
|
69
|
+
for (const item of Array.isArray(list) ? list : []) {
|
|
73
70
|
const i = item.indexOf('=');
|
|
74
71
|
if (i <= 0)
|
|
75
72
|
fail('usage', `${flag} 要写成 KEY=VALUE:${item}`);
|
|
@@ -77,144 +74,264 @@ function pairs(list, flag) {
|
|
|
77
74
|
}
|
|
78
75
|
return out;
|
|
79
76
|
}
|
|
77
|
+
const COMMANDS = [
|
|
78
|
+
{
|
|
79
|
+
name: 'init',
|
|
80
|
+
summary: '初始化:全局安装本工具(之后可用 funcoding 或 fun),并给智能体装上 funcoding Skill',
|
|
81
|
+
options: ['agent', 'project', 'force', 'no-global'],
|
|
82
|
+
examples: ['npx --yes funcoding-cli@latest init', `${BIN} init --agent=all`],
|
|
83
|
+
run: ({ values: v }) => init(targets(v), !!v.force, !v['no-global']),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'update',
|
|
87
|
+
summary: '把本工具更新到最新版本,并刷新已安装的 funcoding Skill',
|
|
88
|
+
run: () => selfUpdate(),
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'info',
|
|
92
|
+
summary: '环境信息:版本、站点、登录状态、本机的智能体和 Skill 目录、配置目录',
|
|
93
|
+
params: [['<field>', '只输出这一项,比如 version、user、agents;不写时输出全部']],
|
|
94
|
+
run: ({ args }) => envInfo(args[0]),
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'help',
|
|
98
|
+
summary: '显示帮助',
|
|
99
|
+
params: [['<command>', '要查看帮助的命令,比如 skills add']],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'skills',
|
|
103
|
+
summary: '查找、安装和管理 Agent Skills',
|
|
104
|
+
commands: [
|
|
105
|
+
{ name: 'find', summary: '按关键词查找 Skill', params: [['<query>', '关键词']], options: ['limit'], run: ({ args, values }) => find('skill', args.join(' '), limitOf(values)) },
|
|
106
|
+
{
|
|
107
|
+
name: 'info',
|
|
108
|
+
summary: '查看 Skill 的描述、文件清单和 SKILL.md 全文(安装前检查用)',
|
|
109
|
+
params: [['<id>', 'owner/repo/skill,或网站上的详情页地址']],
|
|
110
|
+
run: ({ args }) => skillInfo(need(args, ' Skill', `${BIN} skills info anthropics/skills/pdf`)[0]),
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'add',
|
|
114
|
+
summary: '安装一个或多个 Skill',
|
|
115
|
+
params: [['<id>...', 'owner/repo/skill,或网站上的详情页地址']],
|
|
116
|
+
options: ['agent', 'project', 'force'],
|
|
117
|
+
examples: [`${BIN} skills add anthropics/skills/pdf`, `${BIN} skills add anthropics/skills/pdf --agent=codex,cursor --project`],
|
|
118
|
+
run: ({ args, values: v }) => install(need(args, '要安装的 Skill', `${BIN} skills add anthropics/skills/pdf`), targets(v), !!v.force),
|
|
119
|
+
},
|
|
120
|
+
{ name: 'list', summary: '列出已安装的 Skill', options: ['scope-agent', 'only-project', 'user'], run: ({ values }) => list(selection(values)) },
|
|
121
|
+
{
|
|
122
|
+
name: 'update',
|
|
123
|
+
summary: '按原来源重新安装由 funcoding 安装的 Skill',
|
|
124
|
+
params: [['<name>', '目录名或 Skill id;不写时更新全部']],
|
|
125
|
+
options: ['check', 'scope-agent', 'only-project', 'user'],
|
|
126
|
+
run: ({ args, values: v }) => update(args[0], selection(v), !!v.check),
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'remove',
|
|
130
|
+
summary: '删除由 funcoding 安装的 Skill(先备份)',
|
|
131
|
+
params: [['<name>...', '目录名或 Skill id']],
|
|
132
|
+
options: ['force', 'scope-agent', 'only-project', 'user'],
|
|
133
|
+
run: ({ args, values: v }) => remove(need(args, '要删除的 Skill', `${BIN} skills remove pdf`), selection(v), !!v.force),
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'mcp',
|
|
139
|
+
summary: '查找 MCP Server,并写入各智能体的 MCP 配置',
|
|
140
|
+
commands: [
|
|
141
|
+
{ name: 'find', summary: '按关键词查找 MCP Server', params: [['<query>', '关键词']], options: ['limit'], run: ({ args, values }) => find('mcp', args.join(' '), limitOf(values)) },
|
|
142
|
+
{
|
|
143
|
+
name: 'info',
|
|
144
|
+
summary: '查看启动方式,以及需要提供的环境变量或请求头',
|
|
145
|
+
params: [['<id>', 'owner/repo 或 owner/repo/sub,或网站上的详情页地址']],
|
|
146
|
+
run: ({ args }) => mcpInfo(need(args, ' MCP Server', `${BIN} mcp info github/github-mcp-server`)[0]),
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'add',
|
|
150
|
+
summary: '把 MCP Server 写入智能体的配置',
|
|
151
|
+
params: [['<id>', 'owner/repo 或 owner/repo/sub,或网站上的详情页地址']],
|
|
152
|
+
options: ['agent', 'project', 'force', 'env', 'header', 'via', 'name'],
|
|
153
|
+
examples: [`${BIN} mcp add vexa-ai/vexa --header=Authorization="Bearer <key>" --agent=cursor`],
|
|
154
|
+
run: ({ args, values: v }) => {
|
|
155
|
+
const via = str(v.via);
|
|
156
|
+
if (via && !['remote', 'npm', 'pypi'].includes(via))
|
|
157
|
+
fail('usage', `--via 只能是 remote、npm 或 pypi:${via}`);
|
|
158
|
+
return mcpAdd(need(args, ' MCP Server', `${BIN} mcp add github/github-mcp-server`)[0], {
|
|
159
|
+
targets: targets(v),
|
|
160
|
+
env: pairs(v.env, '--env'),
|
|
161
|
+
headers: pairs(v.header, '--header'),
|
|
162
|
+
via: via,
|
|
163
|
+
name: str(v.name),
|
|
164
|
+
force: !!v.force,
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{ name: 'list', summary: '列出已配置的 MCP Server', options: ['scope-agent', 'only-project', 'user'], run: ({ values }) => mcpList(selection(values)) },
|
|
169
|
+
{
|
|
170
|
+
name: 'remove',
|
|
171
|
+
summary: '删除 MCP Server 配置(先备份原文件)',
|
|
172
|
+
params: [['<name>...', '配置名,见 mcp list']],
|
|
173
|
+
options: ['scope-agent', 'only-project', 'user'],
|
|
174
|
+
run: ({ args, values }) => mcpRemove(need(args, '要删除的配置名', `${BIN} mcp remove vexa`), selection(values)),
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'favorites',
|
|
180
|
+
summary: '网站上收藏的 Skill 和 MCP Server(需要登录)',
|
|
181
|
+
commands: [
|
|
182
|
+
{ name: 'list', summary: '列出收藏', run: () => favorites() },
|
|
183
|
+
{ name: 'sync', summary: '安装全部收藏,已安装的跳过', options: ['agent', 'project', 'dry-run'], run: ({ values: v }) => sync(targets(v), !!v['dry-run']) },
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'lists',
|
|
188
|
+
summary: '别人分享的 Skill 和 MCP 清单(不需要登录)',
|
|
189
|
+
commands: [
|
|
190
|
+
{
|
|
191
|
+
name: 'info',
|
|
192
|
+
summary: '查看清单里的 Skill 和 MCP Server',
|
|
193
|
+
params: [['<link>', '清单链接(https://funcoding.ai/share/<id>/)或 id']],
|
|
194
|
+
run: ({ args }) => listInfo(need(args, '清单链接', `${BIN} lists info https://funcoding.ai/share/k3x9q2abcd/`)[0]),
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'add',
|
|
198
|
+
summary: '安装清单里的全部 Skill 和 MCP Server,已安装的跳过',
|
|
199
|
+
params: [['<link>', '清单链接(https://funcoding.ai/share/<id>/)或 id']],
|
|
200
|
+
options: ['agent', 'project', 'dry-run'],
|
|
201
|
+
run: ({ args, values: v }) => listAdd(need(args, '清单链接', `${BIN} lists add https://funcoding.ai/share/k3x9q2abcd/`)[0], targets(v), !!v['dry-run']),
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'auth',
|
|
207
|
+
summary: '登录 funcoding.ai',
|
|
208
|
+
commands: [
|
|
209
|
+
{ name: 'login', summary: '在浏览器中授权登录(设备码)', options: ['no-browser'], run: ({ values }) => login({ browser: !values['no-browser'] }) },
|
|
210
|
+
{ name: 'logout', summary: '退出登录,并使登录凭证失效', run: () => logout() },
|
|
211
|
+
{ name: 'status', summary: '查看当前登录的账号', run: () => status() },
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: 'news',
|
|
216
|
+
summary: 'AI 热点',
|
|
217
|
+
commands: [
|
|
218
|
+
{ name: 'list', summary: '当前 AI 热榜:过去 48 小时被多个来源共同报道的事件,前 10 名', options: ['limit'], run: ({ values }) => newsList(limitOf(values)) },
|
|
219
|
+
{ name: 'daily', summary: '某一天的 AI 日报', params: [['<date>', 'YYYY-MM-DD;不写时为最新一期']], run: ({ args }) => newsDaily(args[0]) },
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: 'docs',
|
|
224
|
+
summary: 'AI 编程智能体的中文文档',
|
|
225
|
+
commands: [
|
|
226
|
+
{ name: 'search', summary: '搜索文档', params: [['<query>', '关键词']], options: ['doc-agent', 'limit'], run: ({ args, values }) => docsSearch(args.join(' '), str(values.agent)?.toLowerCase(), limitOf(values)) },
|
|
227
|
+
{
|
|
228
|
+
name: 'fetch',
|
|
229
|
+
summary: '读取文档全文(Markdown),开头附官方来源和核实日期',
|
|
230
|
+
params: [['<path>', '<智能体>/<路径>,比如 claude-code/build/hooks;也可以是网站上的文档地址']],
|
|
231
|
+
run: ({ args }) => docsFetch(need(args, '文档路径', `${BIN} docs fetch claude-code/build/hooks`)[0]),
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
];
|
|
236
|
+
// ---------------- 帮助 ----------------
|
|
237
|
+
const pad = (rows, indent = ' ') => {
|
|
238
|
+
const w = Math.max(...rows.map(([a]) => a.length)) + 2;
|
|
239
|
+
return rows.map(([a, b]) => `${indent}${a.padEnd(w)}${b}`).join('\n');
|
|
240
|
+
};
|
|
241
|
+
function usageLine(path) {
|
|
242
|
+
const cmd = path.at(-1);
|
|
243
|
+
const names = path.map((c) => c.name).join(' ');
|
|
244
|
+
if (!cmd)
|
|
245
|
+
return `${BIN} [-hV] [--json] <command>`;
|
|
246
|
+
if (cmd.commands)
|
|
247
|
+
return `${BIN} ${names} <command> [options]`;
|
|
248
|
+
const opts = (cmd.options ?? []).map((k) => `[${OPTIONS[k].flag.split(', ').pop()}]`).join(' ');
|
|
249
|
+
const params = (cmd.params ?? []).map(([p]) => (cmd.name === 'help' || cmd.name === 'info' || p === '<name>' || p === '<date>' ? `[${p}]` : p)).join(' ');
|
|
250
|
+
return [BIN, names, opts, params].filter(Boolean).join(' ');
|
|
251
|
+
}
|
|
252
|
+
function helpText(path) {
|
|
253
|
+
const cmd = path.at(-1);
|
|
254
|
+
const out = [`${bold('Usage:')} ${usageLine(path)}`];
|
|
255
|
+
if (!cmd) {
|
|
256
|
+
out.push(`${dim(`funcoding v${VERSION}`)} — 从 ${SITE} 查找和安装 Agent Skills、配置 MCP Server、查看 AI 热点和中文文档`);
|
|
257
|
+
out.push(dim('可以简写为 fun,比如 fun skills find pdf;给智能体用时加 --json'));
|
|
258
|
+
}
|
|
259
|
+
else
|
|
260
|
+
out.push(cmd.summary);
|
|
261
|
+
const options = cmd?.options ?? [];
|
|
262
|
+
if (options.length)
|
|
263
|
+
out.push('', bold('Options:'), pad(options.map((k) => [OPTIONS[k].flag, OPTIONS[k].desc])));
|
|
264
|
+
out.push('', bold(cmd ? 'Global options:' : 'Options:'), pad(GLOBAL.map((k) => [OPTIONS[k].flag, OPTIONS[k].desc])));
|
|
265
|
+
if (cmd?.params)
|
|
266
|
+
out.push('', bold('Positional parameters:'), pad(cmd.params));
|
|
267
|
+
const subs = cmd ? cmd.commands : COMMANDS;
|
|
268
|
+
if (subs)
|
|
269
|
+
out.push('', bold('Commands:'), pad(subs.map((c) => [c.name, c.summary])));
|
|
270
|
+
if (cmd?.examples)
|
|
271
|
+
out.push('', bold('Examples:'), cmd.examples.map((e) => ` ${e}`).join('\n'));
|
|
272
|
+
if (!cmd) {
|
|
273
|
+
out.push('', bold('Exit codes:'), ' 0 成功 1 一般错误 2 参数错误或缺少必填项 3 未找到 4 目标已存在 5 未登录或登录已过期');
|
|
274
|
+
out.push('', `运行 ${BIN} <command> --help 查看每条命令的用法。`);
|
|
275
|
+
}
|
|
276
|
+
return out.join('\n');
|
|
277
|
+
}
|
|
278
|
+
/** 从位置参数里找出命令路径,剩下的是这条命令的参数 */
|
|
279
|
+
function resolve(words) {
|
|
280
|
+
const path = [];
|
|
281
|
+
let list = COMMANDS;
|
|
282
|
+
let i = 0;
|
|
283
|
+
while (list && i < words.length) {
|
|
284
|
+
const c = list.find((x) => x.name === words[i]);
|
|
285
|
+
if (!c) {
|
|
286
|
+
const where = path.map((p) => p.name).join(' ');
|
|
287
|
+
fail('usage', `不认识的命令:${[where, words[i]].filter(Boolean).join(' ')}`, `运行 ${BIN} ${where ? `${where} ` : ''}--help 查看可用命令`);
|
|
288
|
+
}
|
|
289
|
+
path.push(c);
|
|
290
|
+
list = c.commands;
|
|
291
|
+
i++;
|
|
292
|
+
}
|
|
293
|
+
return { path, rest: words.slice(i) };
|
|
294
|
+
}
|
|
295
|
+
function parserConfig(keys) {
|
|
296
|
+
const out = {};
|
|
297
|
+
for (const k of keys) {
|
|
298
|
+
const { type, ...o } = OPTIONS[k];
|
|
299
|
+
// scope-agent / doc-agent 等是同一个 --agent 的不同说明
|
|
300
|
+
const name = k === 'scope-agent' || k === 'doc-agent' ? 'agent' : k === 'only-project' ? 'project' : k;
|
|
301
|
+
out[name] = { type, ...(o.short ? { short: o.short } : {}), ...(o.multiple ? { multiple: true } : {}) };
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
// ---------------- 入口 ----------------
|
|
80
306
|
async function main() {
|
|
81
307
|
const argv = process.argv.slice(2);
|
|
82
308
|
// 参数解析出错时也要按 JSON 输出
|
|
83
309
|
setJsonMode(argv.includes('--json'));
|
|
84
|
-
//
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
project: { type: 'boolean', short: 'p', default: false },
|
|
95
|
-
user: { type: 'boolean', short: 'u', default: false },
|
|
96
|
-
force: { type: 'boolean', short: 'f', default: false },
|
|
97
|
-
env: { type: 'string', short: 'e', multiple: true },
|
|
98
|
-
header: { type: 'string', multiple: true },
|
|
99
|
-
via: { type: 'string' },
|
|
100
|
-
name: { type: 'string' },
|
|
101
|
-
'dry-run': { type: 'boolean', default: false },
|
|
102
|
-
check: { type: 'boolean', default: false },
|
|
103
|
-
daily: { type: 'string' },
|
|
104
|
-
limit: { type: 'string' },
|
|
105
|
-
'no-browser': { type: 'boolean', default: false },
|
|
106
|
-
help: { type: 'boolean', short: 'h', default: false },
|
|
107
|
-
version: { type: 'boolean', short: 'v', default: false },
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
setJsonMode(values.json);
|
|
310
|
+
// 先宽松解析一遍,找出命令路径
|
|
311
|
+
const loose = parseArgs({ args: argv, allowPositionals: true, strict: false, options: parserConfig(Object.keys(OPTIONS)) });
|
|
312
|
+
const { path, rest } = resolve(loose.positionals);
|
|
313
|
+
const cmd = path.at(-1);
|
|
314
|
+
if (cmd?.name === 'help' && path.length === 1)
|
|
315
|
+
return info(helpText(resolve(rest).path));
|
|
316
|
+
// 再按这条命令允许的选项严格解析
|
|
317
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: parserConfig([...GLOBAL, ...(cmd?.options ?? [])]) });
|
|
318
|
+
const values = parsed.values;
|
|
319
|
+
const positionals = parsed.positionals;
|
|
111
320
|
if (values.version)
|
|
112
321
|
return info(VERSION);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const limit = Math.min(30, Math.max(1, Number(values.limit) || 10));
|
|
117
|
-
/** 写入类命令的目标:智能体 × 一个范围 */
|
|
118
|
-
const targets = () => {
|
|
119
|
-
const agents = resolveAgents(values.agent);
|
|
120
|
-
if (!agents)
|
|
121
|
-
return fail('usage', `不认识的智能体:${values.agent}`, `可选:${AGENT_IDS.join('、')}、all`);
|
|
122
|
-
const scope = values.project ? 'project' : 'user';
|
|
123
|
-
return agents.map((agent) => ({ agent, scope }));
|
|
124
|
-
};
|
|
125
|
-
/** 读取和维护类命令的范围:默认全部智能体、两个范围 */
|
|
126
|
-
const selection = () => {
|
|
127
|
-
const agents = values.agent ? resolveAgents(values.agent) : AGENT_IDS;
|
|
128
|
-
if (!agents)
|
|
129
|
-
return fail('usage', `不认识的智能体:${values.agent}`, `可选:${AGENT_IDS.join('、')}、all`);
|
|
130
|
-
const scopes = values.project && !values.user ? ['project'] : values.user && !values.project ? ['user'] : ['user', 'project'];
|
|
131
|
-
return { agents, scopes };
|
|
132
|
-
};
|
|
133
|
-
switch (command) {
|
|
134
|
-
case 'search':
|
|
135
|
-
case 's':
|
|
136
|
-
return search(args.join(' '), limit);
|
|
137
|
-
case 'info':
|
|
138
|
-
if (!args[0])
|
|
139
|
-
fail('usage', '缺少 Skill。', '例如 funcoding info anthropics/skills/pdf');
|
|
140
|
-
return skillInfo(args[0]);
|
|
141
|
-
case 'install':
|
|
142
|
-
case 'add':
|
|
143
|
-
case 'i':
|
|
144
|
-
return install(args, targets(), values.force);
|
|
145
|
-
case 'list':
|
|
146
|
-
case 'ls':
|
|
147
|
-
return list(selection());
|
|
148
|
-
case 'update':
|
|
149
|
-
case 'up':
|
|
150
|
-
return update(args[0], selection(), values.check);
|
|
151
|
-
case 'remove':
|
|
152
|
-
case 'uninstall':
|
|
153
|
-
case 'rm':
|
|
154
|
-
return remove(args, selection(), values.force);
|
|
155
|
-
case 'mcp': {
|
|
156
|
-
const [sub, ...rest] = args;
|
|
157
|
-
if (sub === 'info') {
|
|
158
|
-
if (!rest[0])
|
|
159
|
-
fail('usage', '缺少 MCP Server。', '例如 funcoding mcp info upstash/context7');
|
|
160
|
-
return mcpInfo(rest[0]);
|
|
161
|
-
}
|
|
162
|
-
if (sub === 'add' || sub === 'install') {
|
|
163
|
-
if (!rest[0])
|
|
164
|
-
fail('usage', '缺少 MCP Server。', '例如 funcoding mcp add upstash/context7');
|
|
165
|
-
if (values.via && !['remote', 'npm', 'pypi'].includes(values.via))
|
|
166
|
-
fail('usage', `--via 只能是 remote、npm 或 pypi:${values.via}`);
|
|
167
|
-
return mcpAdd(rest[0], {
|
|
168
|
-
targets: targets(),
|
|
169
|
-
env: pairs(values.env, '--env'),
|
|
170
|
-
headers: pairs(values.header, '--header'),
|
|
171
|
-
via: values.via,
|
|
172
|
-
name: values.name,
|
|
173
|
-
force: values.force,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
if (sub === 'list' || sub === 'ls')
|
|
177
|
-
return mcpList(selection());
|
|
178
|
-
if (sub === 'remove' || sub === 'rm')
|
|
179
|
-
return mcpRemove(rest, selection());
|
|
180
|
-
return fail('usage', `不认识的命令:mcp ${sub ?? ''}`.trim(), '可用:mcp info / add / list / remove');
|
|
181
|
-
}
|
|
182
|
-
case 'login':
|
|
183
|
-
return login({ browser: !values['no-browser'] });
|
|
184
|
-
case 'logout':
|
|
185
|
-
return logout();
|
|
186
|
-
case 'whoami':
|
|
187
|
-
return whoami();
|
|
188
|
-
case 'favorites':
|
|
189
|
-
case 'fav':
|
|
190
|
-
return favorites();
|
|
191
|
-
case 'sync':
|
|
192
|
-
return sync(targets(), values['dry-run']);
|
|
193
|
-
case 'news':
|
|
194
|
-
return news({ daily: values.daily, limit });
|
|
195
|
-
case 'docs': {
|
|
196
|
-
const [sub, ...rest] = args;
|
|
197
|
-
// 文档的 --agent 是站内任意智能体的 slug,不限于能装 Skill 的三个
|
|
198
|
-
if (sub === 'search')
|
|
199
|
-
return docsSearch(rest.join(' '), values.agent?.toLowerCase(), limit);
|
|
200
|
-
if (sub === 'read') {
|
|
201
|
-
if (!rest[0])
|
|
202
|
-
fail('usage', '缺少文档。', '例如 funcoding docs read claude-code/hooks');
|
|
203
|
-
return docsRead(rest[0]);
|
|
204
|
-
}
|
|
205
|
-
return fail('usage', `不认识的命令:docs ${sub ?? ''}`.trim(), '可用:docs search / read');
|
|
206
|
-
}
|
|
207
|
-
case 'setup':
|
|
208
|
-
return setup(targets(), values.force);
|
|
209
|
-
default:
|
|
210
|
-
return fail('usage', `不认识的命令:${command}`, '运行 funcoding --help 查看用法');
|
|
211
|
-
}
|
|
322
|
+
if (values.help || !cmd?.run)
|
|
323
|
+
return info(helpText(path));
|
|
324
|
+
await cmd.run({ args: positionals.slice(path.length), values });
|
|
212
325
|
}
|
|
213
326
|
main().catch((err) => {
|
|
214
327
|
if (err instanceof CliError)
|
|
215
328
|
return reportError(err);
|
|
216
|
-
// parseArgs
|
|
217
|
-
if (err instanceof TypeError && err.code?.startsWith('ERR_PARSE_ARGS'))
|
|
218
|
-
|
|
329
|
+
// parseArgs 的参数错误,比如这条命令不支持的选项
|
|
330
|
+
if (err instanceof TypeError && err.code?.startsWith('ERR_PARSE_ARGS')) {
|
|
331
|
+
const unknown = /Unknown option '([^']+)'/.exec(err.message)?.[1];
|
|
332
|
+
const missing = /Option '([^']+)' argument missing/.exec(err.message)?.[1];
|
|
333
|
+
const message = unknown ? `这条命令不支持选项 ${unknown}` : missing ? `选项 ${missing.split(', ').pop()?.replace(' <value>', '')} 缺少取值` : err.message;
|
|
334
|
+
return reportError(new CliError('usage', message, `运行 ${BIN} <command> --help 查看这条命令支持的选项`));
|
|
335
|
+
}
|
|
219
336
|
reportError(new CliError('error', err instanceof Error ? err.message : String(err)));
|
|
220
337
|
});
|