funcoding-cli 0.2.1 → 0.4.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.
@@ -0,0 +1,238 @@
1
+ /**
2
+ * 命令表和选项表:cli.ts 用它解析参数、生成每条命令的 --help,gen-skill.ts 用它生成 funcoding-cli Skill 的 references/。
3
+ * 命令格式参照 android CLI 和 sentry-cli:按「名词 动词」分组(cli setup、skills add、mcp list…),顶层只有 info 和 help
4
+ */
5
+ import { AGENT_IDS, resolveAgents } from './agents.js';
6
+ import { login, logout, status } from './auth.js';
7
+ import { docsFetch, docsSearch, find, newsDaily, newsList } from './content.js';
8
+ import { envInfo } from './info.js';
9
+ import { install, list, remove, skillInfo, update } from './install.js';
10
+ import { listAdd, listInfo } from './lists.js';
11
+ import { mcpAdd, mcpInfo, mcpList, mcpRemove } from './mcp.js';
12
+ import { setup, upgrade } from './setup.js';
13
+ import { favorites, sync } from './sync.js';
14
+ import { fail, setQuiet } from './ui.js';
15
+ /** 帮助和提示里统一写全名;fun 是同一个命令的简写 */
16
+ export const BIN = 'funcoding';
17
+ export const OPTIONS = {
18
+ json: { type: 'boolean', flag: '--json', desc: '只向 stdout 输出一个 JSON 对象(给智能体用)' },
19
+ help: { type: 'boolean', short: 'h', flag: '-h, --help', desc: '显示这条命令的帮助' },
20
+ version: { type: 'boolean', short: 'V', flag: '-V, --version', desc: '显示版本' },
21
+ agent: { type: 'string', short: 'a', flag: '-a, --agent=<name>', desc: `目标智能体:${AGENT_IDS.join('、')},多个用逗号分隔,all 表示本机检测到的全部(默认:当前所在的智能体,否则 claude-code)` },
22
+ 'scope-agent': { type: 'string', short: 'a', flag: '-a, --agent=<name>', desc: `只看这些智能体:${AGENT_IDS.join('、')},多个用逗号分隔(默认全部)` },
23
+ 'doc-agent': { type: 'string', short: 'a', flag: '-a, --agent=<slug>', desc: '只搜这个智能体的文档,比如 claude-code、codex、gemini-cli' },
24
+ project: { type: 'boolean', short: 'p', flag: '-p, --project', desc: '安装到当前项目(可随仓库提交),不加时安装到用户目录' },
25
+ 'only-project': { type: 'boolean', short: 'p', flag: '-p, --project', desc: '只看当前项目' },
26
+ user: { type: 'boolean', short: 'u', flag: '-u, --user', desc: '只看用户目录(默认用户目录和当前项目都看)' },
27
+ force: { type: 'boolean', short: 'f', flag: '-f, --force', desc: '目标已存在时覆盖(原内容先备份到配置目录)' },
28
+ env: { type: 'string', short: 'e', multiple: true, flag: '-e, --env=<KEY=VALUE>', desc: 'stdio Server 的环境变量,可重复' },
29
+ header: { type: 'string', multiple: true, flag: '--header=<KEY=VALUE>', desc: '远程 Server 的请求头,可重复' },
30
+ via: { type: 'string', flag: '--via=<kind>', desc: '指定启动方式:remote、npm 或 pypi' },
31
+ name: { type: 'string', flag: '--name=<name>', desc: '自定义配置名(默认取自 Server 名称)' },
32
+ 'dry-run': { type: 'boolean', flag: '--dry-run', desc: '只列出将要执行的操作' },
33
+ check: { type: 'boolean', flag: '--check', desc: '只检查远端有没有更新,不安装' },
34
+ limit: { type: 'string', flag: '--limit=<n>', desc: '返回条数(1–30,默认 10)' },
35
+ 'no-browser': { type: 'boolean', flag: '--no-browser', desc: '不自动打开浏览器' },
36
+ 'no-global': { type: 'boolean', flag: '--no-global', desc: '不全局安装' },
37
+ 'no-agent-skills': { type: 'boolean', flag: '--no-agent-skills', desc: '不为智能体安装 funcoding-cli Skill' },
38
+ quiet: { type: 'boolean', short: 'q', flag: '-q, --quiet', desc: '不输出给人看的文字(脚本里用)' },
39
+ };
40
+ export const GLOBAL = ['json', 'help', 'version'];
41
+ const str = (v) => (typeof v === 'string' ? v : undefined);
42
+ const limitOf = (v) => Math.min(30, Math.max(1, Number(str(v.limit)) || 10));
43
+ const need = (args, what, example) => {
44
+ if (!args[0])
45
+ fail('usage', `缺少${what}`, `例如 ${example}`);
46
+ return args;
47
+ };
48
+ /** 写入类命令的目标:智能体 × 一个范围 */
49
+ function targets(v) {
50
+ const agents = resolveAgents(str(v.agent));
51
+ if (!agents)
52
+ return fail('usage', `不支持的智能体:${str(v.agent)}`, `可选:${AGENT_IDS.join('、')}、all`);
53
+ const scope = v.project ? 'project' : 'user';
54
+ return agents.map((agent) => ({ agent, scope }));
55
+ }
56
+ /** 读取和维护类命令的范围:默认全部智能体、两个范围 */
57
+ function selection(v) {
58
+ const agents = str(v.agent) ? resolveAgents(str(v.agent)) : AGENT_IDS;
59
+ if (!agents)
60
+ return fail('usage', `不支持的智能体:${str(v.agent)}`, `可选:${AGENT_IDS.join('、')}、all`);
61
+ const scopes = v.project && !v.user ? ['project'] : v.user && !v.project ? ['user'] : ['user', 'project'];
62
+ return { agents, scopes };
63
+ }
64
+ /** KEY=VALUE 列表转成对象 */
65
+ function pairs(list, flag) {
66
+ const out = {};
67
+ for (const item of Array.isArray(list) ? list : []) {
68
+ const i = item.indexOf('=');
69
+ if (i <= 0)
70
+ fail('usage', `${flag} 格式应为 KEY=VALUE:${item}`);
71
+ out[item.slice(0, i)] = item.slice(i + 1);
72
+ }
73
+ return out;
74
+ }
75
+ export const COMMANDS = [
76
+ {
77
+ name: 'info',
78
+ summary: '环境信息:版本、站点、登录状态、本机的智能体和 Skill 目录、配置目录',
79
+ params: [['<field>', '只输出这一项,比如 version、user、agents;不写时输出全部']],
80
+ run: ({ args }) => envInfo(args[0]),
81
+ },
82
+ {
83
+ name: 'help',
84
+ summary: '显示帮助',
85
+ params: [['<command>', '要查看帮助的命令,比如 skills add']],
86
+ },
87
+ {
88
+ name: 'cli',
89
+ summary: '安装、升级本工具',
90
+ commands: [
91
+ {
92
+ name: 'setup',
93
+ summary: '全局安装本工具,并为本机的智能体安装 funcoding-cli Skill',
94
+ options: ['no-global', 'no-agent-skills', 'force', 'quiet'],
95
+ examples: ['npx --yes funcoding-cli@latest cli setup', `${BIN} cli setup --no-agent-skills`],
96
+ run: ({ values: v }) => {
97
+ setQuiet(!!v.quiet);
98
+ return setup({ global: !v['no-global'], agentSkills: !v['no-agent-skills'], force: !!v.force });
99
+ },
100
+ },
101
+ { name: 'upgrade', summary: '升级到最新版本,并刷新已安装的 funcoding-cli Skill', run: () => upgrade() },
102
+ ],
103
+ },
104
+ {
105
+ name: 'skills',
106
+ summary: '查找、安装和管理 Agent Skills',
107
+ commands: [
108
+ { name: 'find', summary: '按关键词查找 Skill', params: [['<query>', '关键词']], options: ['limit'], run: ({ args, values }) => find('skill', args.join(' '), limitOf(values)) },
109
+ {
110
+ name: 'info',
111
+ summary: '查看 Skill 的描述、文件清单和 SKILL.md 全文(安装前检查用)',
112
+ params: [['<id>', 'owner/repo/skill,或网站上的详情页地址']],
113
+ run: ({ args }) => skillInfo(need(args, ' Skill', `${BIN} skills info anthropics/skills/pdf`)[0]),
114
+ },
115
+ {
116
+ name: 'add',
117
+ summary: '安装一个或多个 Skill',
118
+ params: [['<id>...', 'owner/repo/skill,或网站上的详情页地址']],
119
+ options: ['agent', 'project', 'force'],
120
+ examples: [`${BIN} skills add anthropics/skills/pdf`, `${BIN} skills add anthropics/skills/pdf --agent=codex,cursor --project`],
121
+ run: ({ args, values: v }) => install(need(args, '要安装的 Skill', `${BIN} skills add anthropics/skills/pdf`), targets(v), !!v.force),
122
+ },
123
+ { name: 'list', summary: '列出已安装的 Skill', options: ['scope-agent', 'only-project', 'user'], run: ({ values }) => list(selection(values)) },
124
+ {
125
+ name: 'update',
126
+ summary: '按原来源重新安装由 funcoding 安装的 Skill',
127
+ params: [['<name>', '目录名或 Skill id;不写时更新全部']],
128
+ options: ['check', 'scope-agent', 'only-project', 'user'],
129
+ run: ({ args, values: v }) => update(args[0], selection(v), !!v.check),
130
+ },
131
+ {
132
+ name: 'remove',
133
+ summary: '删除由 funcoding 安装的 Skill(先备份)',
134
+ params: [['<name>...', '目录名或 Skill id']],
135
+ options: ['force', 'scope-agent', 'only-project', 'user'],
136
+ run: ({ args, values: v }) => remove(need(args, '要删除的 Skill', `${BIN} skills remove pdf`), selection(v), !!v.force),
137
+ },
138
+ ],
139
+ },
140
+ {
141
+ name: 'mcp',
142
+ summary: '查找 MCP Server,并写入各智能体的 MCP 配置',
143
+ commands: [
144
+ { name: 'find', summary: '按关键词查找 MCP Server', params: [['<query>', '关键词']], options: ['limit'], run: ({ args, values }) => find('mcp', args.join(' '), limitOf(values)) },
145
+ {
146
+ name: 'info',
147
+ summary: '查看启动方式,以及需要提供的环境变量或请求头',
148
+ params: [['<id>', 'owner/repo 或 owner/repo/sub,或网站上的详情页地址']],
149
+ run: ({ args }) => mcpInfo(need(args, ' MCP Server', `${BIN} mcp info github/github-mcp-server`)[0]),
150
+ },
151
+ {
152
+ name: 'add',
153
+ summary: '把 MCP Server 写入智能体的配置',
154
+ params: [['<id>', 'owner/repo 或 owner/repo/sub,或网站上的详情页地址']],
155
+ options: ['agent', 'project', 'force', 'env', 'header', 'via', 'name'],
156
+ examples: [`${BIN} mcp add vexa-ai/vexa --header=Authorization="Bearer <key>" --agent=cursor`],
157
+ run: ({ args, values: v }) => {
158
+ const via = str(v.via);
159
+ if (via && !['remote', 'npm', 'pypi'].includes(via))
160
+ fail('usage', `--via 可选 remote、npm、pypi:${via}`);
161
+ return mcpAdd(need(args, ' MCP Server', `${BIN} mcp add github/github-mcp-server`)[0], {
162
+ targets: targets(v),
163
+ env: pairs(v.env, '--env'),
164
+ headers: pairs(v.header, '--header'),
165
+ via: via,
166
+ name: str(v.name),
167
+ force: !!v.force,
168
+ });
169
+ },
170
+ },
171
+ { name: 'list', summary: '列出已配置的 MCP Server', options: ['scope-agent', 'only-project', 'user'], run: ({ values }) => mcpList(selection(values)) },
172
+ {
173
+ name: 'remove',
174
+ summary: '删除 MCP Server 配置(先备份原文件)',
175
+ params: [['<name>...', '配置名,见 mcp list']],
176
+ options: ['scope-agent', 'only-project', 'user'],
177
+ run: ({ args, values }) => mcpRemove(need(args, '要删除的配置名', `${BIN} mcp remove vexa`), selection(values)),
178
+ },
179
+ ],
180
+ },
181
+ {
182
+ name: 'favorites',
183
+ summary: '网站上收藏的 Skill 和 MCP Server(需要登录)',
184
+ commands: [
185
+ { name: 'list', summary: '列出收藏', run: () => favorites() },
186
+ { name: 'sync', summary: '安装全部收藏,已安装的跳过', options: ['agent', 'project', 'dry-run'], run: ({ values: v }) => sync(targets(v), !!v['dry-run']) },
187
+ ],
188
+ },
189
+ {
190
+ name: 'lists',
191
+ summary: '别人分享的 Skill 和 MCP 清单(不需要登录)',
192
+ commands: [
193
+ {
194
+ name: 'info',
195
+ summary: '查看清单里的 Skill 和 MCP Server',
196
+ params: [['<link>', '清单链接(https://funcoding.ai/share/<id>/)或 id']],
197
+ run: ({ args }) => listInfo(need(args, '清单链接', `${BIN} lists info https://funcoding.ai/share/k3x9q2abcd/`)[0]),
198
+ },
199
+ {
200
+ name: 'add',
201
+ summary: '安装清单里的全部 Skill 和 MCP Server,已安装的跳过',
202
+ params: [['<link>', '清单链接(https://funcoding.ai/share/<id>/)或 id']],
203
+ options: ['agent', 'project', 'dry-run'],
204
+ run: ({ args, values: v }) => listAdd(need(args, '清单链接', `${BIN} lists add https://funcoding.ai/share/k3x9q2abcd/`)[0], targets(v), !!v['dry-run']),
205
+ },
206
+ ],
207
+ },
208
+ {
209
+ name: 'auth',
210
+ summary: '登录 funcoding.ai',
211
+ commands: [
212
+ { name: 'login', summary: '在浏览器中授权登录(设备码)', options: ['no-browser'], run: ({ values }) => login({ browser: !values['no-browser'] }) },
213
+ { name: 'logout', summary: '退出登录,并使登录凭证失效', run: () => logout() },
214
+ { name: 'status', summary: '查看当前登录的账号', run: () => status() },
215
+ ],
216
+ },
217
+ {
218
+ name: 'news',
219
+ summary: 'AI 热点',
220
+ commands: [
221
+ { name: 'list', summary: '当前 AI 热榜:过去 48 小时被多个来源共同报道的事件,前 10 名', options: ['limit'], run: ({ values }) => newsList(limitOf(values)) },
222
+ { name: 'daily', summary: '某一天的 AI 日报', params: [['<date>', 'YYYY-MM-DD;不写时为最新一期']], run: ({ args }) => newsDaily(args[0]) },
223
+ ],
224
+ },
225
+ {
226
+ name: 'docs',
227
+ summary: 'AI 编程智能体的中文文档',
228
+ commands: [
229
+ { name: 'search', summary: '搜索文档', params: [['<query>', '关键词']], options: ['doc-agent', 'limit'], run: ({ args, values }) => docsSearch(args.join(' '), str(values.agent)?.toLowerCase(), limitOf(values)) },
230
+ {
231
+ name: 'fetch',
232
+ summary: '读取文档全文(Markdown),开头附官方来源和核实日期',
233
+ params: [['<path>', '<智能体>/<路径>,比如 claude-code/build/hooks;也可以是网站上的文档地址']],
234
+ run: ({ args }) => docsFetch(need(args, '文档路径', `${BIN} docs fetch claude-code/build/hooks`)[0]),
235
+ },
236
+ ],
237
+ },
238
+ ];
package/dist/content.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /** skills find / mcp find / news / docs:只读查询网站内容 */
2
2
  import { api, siteUrl } from './api.js';
3
- import { bold, CliError, cyan, dim, done, fail, info, stars, truncate } from './ui.js';
3
+ import { bold, CliError, cyan, dim, done, fail, info, next, stars, truncate } from './ui.js';
4
4
  function printCards(title, cards, hrefOf) {
5
5
  if (cards.length === 0)
6
6
  return;
@@ -17,70 +17,80 @@ function printCards(title, cards, hrefOf) {
17
17
  export async function find(kind, query, limit) {
18
18
  const cmd = kind === 'skill' ? 'skills' : 'mcp';
19
19
  if (!query)
20
- fail('usage', '缺少关键词。', `例如 funcoding ${cmd} find pdf`);
20
+ fail('usage', '缺少关键词', `例如 funcoding ${cmd} find pdf`);
21
21
  const r = await api(`/api/cli/search?q=${encodeURIComponent(query)}&limit=${limit}`);
22
22
  const items = kind === 'skill' ? r.skills : r.mcp;
23
23
  done({ [kind === 'skill' ? 'skills' : 'servers']: items }, () => {
24
24
  if (items.length === 0)
25
- return info(`没有找到「${query}」相关的${kind === 'skill' ? ' Skill' : ' MCP Server'}。`);
25
+ return info(`没有找到和「${query}」相关的${kind === 'skill' ? ' Skill' : ' MCP Server'}`);
26
26
  printCards(kind === 'skill' ? 'Skills' : 'MCP Server', items, (c) => `funcoding ${cmd} add ${c.id}`);
27
27
  });
28
28
  }
29
- function printStory(s, n) {
30
- info(`${n ? `${String(n).padStart(2)}. ` : ' · '}${bold(s.title)}${dim(` ${s.sourceCount} 家报道`)}`);
31
- if (s.summary)
32
- info(` ${truncate(s.summary, (process.stdout.columns || 100) - 6)}`);
33
- info(dim(` ${s.url}`));
34
- }
29
+ const width = () => (process.stdout.columns || 100) - 6;
35
30
  export async function newsDaily(date) {
36
31
  const day = date || 'latest';
37
32
  if (day !== 'latest' && !/^\d{4}-\d{2}-\d{2}$/.test(day))
38
- fail('usage', `日期要写成 YYYY-MM-DD:${day}`);
33
+ fail('usage', `日期格式应为 YYYY-MM-DD:${day}`);
39
34
  const issue = await api(`/api/cli/news?daily=${day}`);
40
35
  done({ daily: issue }, () => {
41
- info(bold(`AI 日报 · 第 ${issue.no} 期 · ${issue.day}`));
36
+ info(bold(`AI 日报 · ${issue.day}`));
42
37
  info(dim(issue.url));
43
- info('');
44
- info(bold('头条'));
45
- printStory(issue.headline);
38
+ if (issue.lead) {
39
+ info('');
40
+ info(bold(issue.lead.title));
41
+ info(` ${truncate(issue.lead.summary, width())}`);
42
+ }
46
43
  for (const sec of issue.sections) {
47
44
  info('');
48
45
  info(bold(sec.label));
49
- for (const s of sec.stories)
50
- printStory(s);
46
+ for (const i of sec.items) {
47
+ info(` · ${bold(i.title)}${dim(` ${i.source}`)}`);
48
+ if (i.summary)
49
+ info(` ${truncate(i.summary, width())}`);
50
+ info(dim(` ${i.url}`));
51
+ }
51
52
  }
52
- if (issue.flash.length > 0) {
53
+ if (issue.flashes.length > 0) {
53
54
  info('');
54
55
  info(bold('快讯'));
55
- for (const s of issue.flash)
56
- info(` · ${s.title} ${dim(s.url)}`);
56
+ for (const i of issue.flashes)
57
+ info(` · ${i.title} ${dim(i.url)}`);
57
58
  }
59
+ info('');
60
+ info(dim('数据来自 AIHOT(aihot.news)'));
58
61
  });
59
62
  }
60
63
  export async function newsList(limit) {
61
64
  const r = await api(`/api/cli/news?limit=${limit}`);
62
65
  done({ updatedAt: r.updatedAt, stories: r.stories }, () => {
63
66
  if (r.stories.length === 0)
64
- return info('暂无热点。');
67
+ return info('暂无热点');
65
68
  info(bold('AI 热榜') + dim(` ${siteUrl('/news/')}`));
66
- r.stories.forEach((s, i) => printStory(s, i + 1));
69
+ for (const s of r.stories) {
70
+ info(`${String(s.rank).padStart(2)}. ${bold(s.title)}${dim(` ${s.sourceCount} 个来源`)}`);
71
+ if (s.latest)
72
+ info(` ${truncate(s.latest, width() - 5)}`);
73
+ info(dim(` ${s.url}`));
74
+ }
75
+ info('');
76
+ info(dim('数据来自 AIHOT(aihot.news)'));
67
77
  });
68
78
  }
69
79
  export async function docsSearch(query, agent, limit) {
70
80
  if (!query)
71
- fail('usage', '缺少搜索词。', '例如 funcoding docs search hooks --agent=claude-code');
81
+ fail('usage', '缺少关键词', '例如 funcoding docs search hooks --agent=claude-code');
72
82
  const params = new URLSearchParams({ q: query, limit: String(limit), ...(agent ? { agent } : {}) });
73
83
  const r = await api(`/api/cli/docs/search?${params}`);
74
84
  done({ docs: r.docs }, () => {
75
85
  if (r.docs.length === 0)
76
- return info(`没有找到「${query}」相关的文档。`);
86
+ return info(`没有找到和「${query}」相关的文档`);
77
87
  for (const d of r.docs) {
78
88
  info(`${cyan(d.id)} ${bold(d.title)} ${dim(d.agent)}`);
79
89
  if (d.description)
80
90
  info(` ${truncate(d.description, (process.stdout.columns || 100) - 4)}`);
81
91
  }
82
92
  info('');
83
- info(dim(`运行 ${cyan('funcoding docs fetch <id>')} 阅读全文。`));
93
+ next(cyan('funcoding docs fetch <id>'));
84
94
  });
85
95
  }
86
96
  export async function docsFetch(id) {
@@ -90,10 +100,10 @@ export async function docsFetch(id) {
90
100
  .replace(/^\/?agents\//, '')
91
101
  .replace(/^\/+|\/+$/g, '');
92
102
  if (!/^[a-z0-9][\w-]*(\/[\w.-]+)*$/i.test(path))
93
- fail('usage', '文档要写成 <智能体>/<路径>,例如 claude-code/hooks。', '运行 funcoding docs search <关键词> 查找');
103
+ fail('usage', `无法识别的文档路径:${path}`, '写成 <智能体>/<路径>,例如 claude-code/build/hooks');
94
104
  const markdown = await api(`/api/cli/docs/${path}`, { headers: { Accept: 'text/markdown' } }).catch((err) => {
95
105
  if (err instanceof CliError && err.code === 'not_found')
96
- fail('not_found', `没有找到文档:${path}`, '运行 funcoding docs search <关键词> 查找');
106
+ fail('not_found', `没有找到文档:${path}`, '运行 funcoding docs search <关键词>');
97
107
  throw err;
98
108
  });
99
109
  done({ id: path, url: siteUrl(`/agents/${path}/`), markdown }, () => info(markdown));
package/dist/info.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { AGENT_IDS, AGENTS, currentAgent, installedAgents, skillsRoot } from './agents.js';
3
3
  import { SITE, VERSION } from './api.js';
4
4
  import { configDir, credentialsPath, loadCredentials } from './credentials.js';
5
- import { onPath } from './init.js';
5
+ import { AGENT_SKILL, agentSkillDirs, onPath } from './setup.js';
6
6
  import { bold, dim, done, fail, info } from './ui.js';
7
7
  export async function envInfo(field) {
8
8
  const creds = await loadCredentials(SITE);
@@ -14,6 +14,8 @@ export async function envInfo(field) {
14
14
  /** 是否已全局安装(终端里能直接用 funcoding / fun) */
15
15
  global: onPath('funcoding'),
16
16
  user: creds?.user ?? null,
17
+ /** 已安装的 funcoding-cli Skill 目录 */
18
+ agentSkill: agentSkillDirs(),
17
19
  currentAgent: currentAgent(),
18
20
  agents: AGENT_IDS.map((id) => ({ id, installed: found.includes(id), skills: { user: skillsRoot(id, 'user'), project: skillsRoot(id, 'project') } })),
19
21
  configDir: configDir(),
@@ -30,11 +32,12 @@ export async function envInfo(field) {
30
32
  row('version', VERSION);
31
33
  row('site', SITE);
32
34
  row('node', process.versions.node);
33
- row('global', data.global ? '已全局安装(funcoding / fun)' : '未全局安装,运行 npx --yes funcoding-cli@latest init');
35
+ row('global', data.global ? '已安装(funcoding、fun)' : '未安装,运行 npx --yes funcoding-cli@latest cli setup');
36
+ row('agentSkill', data.agentSkill.length > 0 ? data.agentSkill.join('、') : `未安装,运行 funcoding cli setup 安装 ${AGENT_SKILL}`);
34
37
  row('user', creds?.user ? `${creds.user.name}${creds.user.githubLogin ? ` (@${creds.user.githubLogin})` : ''}` : '未登录');
35
38
  row('currentAgent', data.currentAgent ? AGENTS[data.currentAgent].label : '无');
36
39
  for (const id of AGENT_IDS)
37
- row(id, `${found.includes(id) ? '已安装' : dim('未检测到')} ${dim(skillsRoot(id, 'user'))}`);
40
+ row(id, `${found.includes(id) ? '已检测到' : dim('未检测到')} ${dim(skillsRoot(id, 'user'))}`);
38
41
  row('configDir', configDir());
39
42
  });
40
43
  }
package/dist/install.js CHANGED
@@ -10,8 +10,8 @@ import { AGENT_IDS, AGENTS, skillsRoot } from './agents.js';
10
10
  import { api, SITE, siteUrl, USER_AGENT } from './api.js';
11
11
  import { configDir } from './credentials.js';
12
12
  import { isSafeFolderName, resolveInside } from './paths.js';
13
- import { builtinContent, installBuiltin } from './init.js';
14
- import { bold, CliError, cyan, dim, done, fail, green, info, stars, warn } from './ui.js';
13
+ import { AGENT_SKILL, agentSkillContent, installAgentSkill } from './setup.js';
14
+ import { bold, CliError, cyan, dim, done, fail, green, info, next, stars, step, success, warn } from './ui.js';
15
15
  /** 和服务端一致的上限 */
16
16
  const MAX_FILES = 200;
17
17
  const MAX_BYTES = 10 * 1024 * 1024;
@@ -34,7 +34,7 @@ async function download(url, maxBytes) {
34
34
  }
35
35
  const buf = new Uint8Array(await res.arrayBuffer());
36
36
  if (buf.byteLength > maxBytes)
37
- throw new CliError('error', `文件太大:${url}`);
37
+ throw new CliError('error', `文件超过 10 MB:${url}`);
38
38
  return buf;
39
39
  }
40
40
  async function mapLimit(items, limit, fn) {
@@ -60,13 +60,13 @@ export async function moveAway(dir, agent, folder) {
60
60
  export async function fetchSkill(spec) {
61
61
  const id = parseSkillSpec(spec);
62
62
  if (!id)
63
- return fail('usage', `Skill 要写成 owner/repo/skill,或者 ${SITE}/skills/ 下的详情页地址:${spec}`);
63
+ return fail('usage', `无法识别的 Skill:${spec}`, `写成 owner/repo/skill,或 ${SITE}/skills/ 下的详情页地址`);
64
64
  try {
65
65
  return await api(`/api/cli/skills/${id}`);
66
66
  }
67
67
  catch (err) {
68
68
  if (err instanceof CliError && err.code === 'not_found')
69
- fail('not_found', `没有找到 Skill:${id}`, '运行 funcoding skills find <关键词> 查找');
69
+ fail('not_found', `没有找到 Skill:${id}`, '运行 funcoding skills find <关键词>');
70
70
  throw err;
71
71
  }
72
72
  }
@@ -82,7 +82,7 @@ export async function skillInfo(spec) {
82
82
  info(dim(`${s.repo.url}${s.path ? `/tree/${s.repo.branch}/${s.path}` : ''}`));
83
83
  info(dim(page));
84
84
  info('');
85
- info(bold(`文件(${s.files.length}${s.truncated ? ',未列全' : ''})`));
85
+ info(bold(`文件(${s.files.length} 个${s.truncated ? ',未列全' : ''})`));
86
86
  for (const f of s.files)
87
87
  info(` ${f.path}${scripts.includes(f.path) ? dim(' 可执行脚本') : ''}`);
88
88
  info('');
@@ -96,12 +96,12 @@ async function downloadTo(skill, dir) {
96
96
  await mapLimit(skill.files, 4, async (f) => {
97
97
  const target = resolveInside(dir, f.path);
98
98
  if (!target)
99
- throw new CliError('error', `拒绝写入可疑路径:${f.path}`);
99
+ throw new CliError('error', `已拒绝超出目标目录的路径:${f.path}`);
100
100
  const url = skill.rawBase + f.path.split('/').map(encodeURIComponent).join('/');
101
101
  const data = await download(url, MAX_BYTES);
102
102
  total += data.byteLength;
103
103
  if (total > MAX_BYTES)
104
- throw new CliError('error', 'Skill 总大小超过 10 MB,请手动安装。', `仓库地址:${skill.repo.url}`);
104
+ throw new CliError('error', 'Skill 超过 10 MB', `从仓库手动安装:${skill.repo.url}`);
105
105
  await mkdir(dirname(target), { recursive: true });
106
106
  await writeFile(target, data);
107
107
  });
@@ -112,13 +112,13 @@ async function downloadTo(skill, dir) {
112
112
  /** 装到多个目标:只下载一次,其余目标复制 */
113
113
  export async function installSkill(skill, targets, force) {
114
114
  if (!isSafeFolderName(skill.folder))
115
- fail('error', `服务端返回的目录名不合法:${skill.folder}`);
115
+ fail('error', `目录名不合法:${skill.folder}`);
116
116
  if (skill.files.length === 0 || !skill.files.some((f) => f.path === 'SKILL.md'))
117
- fail('not_found', `${skill.id} 没有 SKILL.md,可能已被仓库删除。`);
117
+ fail('not_found', `${skill.id} 缺少 SKILL.md,可能已从仓库删除`);
118
118
  if (skill.files.length > MAX_FILES)
119
- fail('error', `文件太多(${skill.files.length} 个),请手动安装。`, `仓库地址:${skill.repo.url}`);
119
+ fail('error', `文件超过 ${MAX_FILES} 个(${skill.files.length} 个)`, `从仓库手动安装:${skill.repo.url}`);
120
120
  if (skill.truncated)
121
- warn(`${skill.id} 的文件太多,只会安装前一部分,缺少的请从仓库补齐。`);
121
+ warn(`文件过多,只安装前 ${skill.files.length} 个,其余从仓库补齐:${skill.repo.url}`);
122
122
  const plan = targets.map((t) => ({ ...t, root: skillsRoot(t.agent, t.scope), dest: join(skillsRoot(t.agent, t.scope), skill.folder) }));
123
123
  if (!force) {
124
124
  const taken = [];
@@ -126,9 +126,9 @@ export async function installSkill(skill, targets, force) {
126
126
  if (await exists(p.dest))
127
127
  taken.push(p.dest);
128
128
  if (taken.length > 0)
129
- fail('exists', `已经存在:${taken.join('、')}`, '确认要覆盖就加 --force(旧版本会备份到配置目录)', { paths: taken });
129
+ fail('exists', `已存在:${taken.join('、')}`, '覆盖请加 --force(会先备份)', { paths: taken });
130
130
  }
131
- info(`${bold(skill.name)} ${dim(`${skill.repo.owner}/${skill.repo.name}${skill.path ? `/${skill.path}` : ''} · ${skill.files.length} 个文件`)}`);
131
+ step(`正在安装 ${skill.id}(${skill.files.length} 个文件)`);
132
132
  const results = [];
133
133
  let source = null;
134
134
  for (const p of plan) {
@@ -151,23 +151,18 @@ export async function installSkill(skill, targets, force) {
151
151
  }
152
152
  return results;
153
153
  }
154
- function printInstalled(r) {
155
- const a = AGENTS[r.agent];
156
- info(green(`✓ 已安装到 ${r.dir}`));
157
- if (r.backup)
158
- info(dim(` 旧版本备份在 ${r.backup}`));
159
- info(` 在 ${a.label} 里输入 ${cyan(a.invoke(r.dir.split(/[\\/]/).pop()))} 调用,或直接描述任务让它自动选用。`);
160
- }
161
154
  export async function install(specs, targets, force) {
162
155
  if (specs.length === 0)
163
- fail('usage', '缺少要安装的 Skill。', '例如 funcoding skills add anthropics/skills/pdf');
156
+ fail('usage', '缺少要安装的 Skill', '例如 funcoding skills add anthropics/skills/pdf');
164
157
  const installed = [];
165
158
  for (const spec of specs)
166
159
  installed.push(...(await installSkill(await fetchSkill(spec), targets, force)));
167
160
  done({ installed }, () => {
168
161
  for (const r of installed)
169
- printInstalled(r);
170
- info(dim('Skill 会按 SKILL.md 的指示行事,可能运行其中的脚本,使用前请先阅读 SKILL.md。'));
162
+ success(`已安装 ${r.dir.split(/[\\/]/).pop()}(${AGENTS[r.agent].label})`, [r.dir, r.backup && `备份:${r.backup}`]);
163
+ warn('Skill 可能运行其中的脚本,使用前先阅读 SKILL.md');
164
+ const invokes = [...new Set(installed.map((r) => `${AGENTS[r.agent].label} 输入 ${cyan(AGENTS[r.agent].invoke(r.dir.split(/[\\/]/).pop()))}`))];
165
+ next(`新开会话,${invokes.join(',')}`);
171
166
  });
172
167
  }
173
168
  const frontmatter = (text, key) => new RegExp(`^${key}:\\s*["']?(.+?)["']?\\s*$`, 'm').exec(/^---\r?\n([\s\S]*?)\r?\n---/.exec(text)?.[1] ?? '')?.[1];
@@ -199,14 +194,16 @@ export async function scanAll(sel) {
199
194
  export async function list(sel) {
200
195
  const items = await scanAll(sel);
201
196
  done({ skills: items }, () => {
202
- if (items.length === 0)
203
- return info(`还没有安装 Skill。可以运行 ${cyan('funcoding skills find <关键词>')} 查找。`);
197
+ if (items.length === 0) {
198
+ info('暂无已安装的 Skill');
199
+ return next(cyan('funcoding skills find <关键词>'));
200
+ }
204
201
  for (const agent of AGENT_IDS) {
205
202
  for (const scope of sel.scopes) {
206
203
  const group = items.filter((s) => s.agent === agent && s.scope === scope);
207
204
  if (group.length === 0)
208
205
  continue;
209
- info(`${bold(AGENTS[agent].label)} ${dim(`${scope === 'user' ? '个人' : '项目'} · ${skillsRoot(agent, scope)}`)}`);
206
+ info(`${bold(AGENTS[agent].label)} ${dim(`${scope === 'user' ? '用户目录' : '当前项目'} ${skillsRoot(agent, scope)}`)}`);
210
207
  for (const s of group)
211
208
  info(` ${s.folder}${s.source ? dim(` ← ${s.source.id}`) : ''}`);
212
209
  info('');
@@ -215,12 +212,13 @@ export async function list(sel) {
215
212
  });
216
213
  }
217
214
  // ---------------- update ----------------
218
- const matches = (s, name) => !name || s.folder === name || s.source?.id === name.toLowerCase();
215
+ // 旧版本的 funcoding Skill 也按 funcoding-cli 匹配,刷新时换成新版
216
+ const matches = (s, name) => !name || s.folder === name || s.source?.id === name.toLowerCase() || (!!s.source?.builtin && name === AGENT_SKILL);
219
217
  /** 按 .funcoding.json 记录的来源重新安装;check 时只比较远端 SKILL.md 有没有变化 */
220
218
  export async function update(name, sel, check) {
221
219
  const items = (await scanAll(sel)).filter((s) => s.source && matches(s, name));
222
220
  if (items.length === 0)
223
- fail('not_found', name ? `没有找到由 funcoding 安装的 ${name}` : '没有由 funcoding 安装的 Skill', '运行 funcoding skills list 查看已安装的 Skill');
221
+ fail('not_found', name ? `没有找到由 funcoding 安装的 ${name}` : '暂无由 funcoding 安装的 Skill', '运行 funcoding skills list');
224
222
  const results = [];
225
223
  const cache = new Map();
226
224
  for (const s of items) {
@@ -228,10 +226,10 @@ export async function update(name, sel, check) {
228
226
  if (s.source.builtin) {
229
227
  const local = await readFile(join(s.dir, 'SKILL.md'), 'utf8').catch(() => null);
230
228
  if (check)
231
- results.push({ id, agent: s.agent, scope: s.scope, dir: s.dir, status: local === (await builtinContent()) ? 'up_to_date' : 'outdated' });
229
+ results.push({ id: AGENT_SKILL, agent: s.agent, scope: s.scope, dir: s.dir, status: local === (await agentSkillContent()) ? 'up_to_date' : 'outdated' });
232
230
  else {
233
- await installBuiltin([{ agent: s.agent, scope: s.scope }], true);
234
- results.push({ id, agent: s.agent, scope: s.scope, dir: s.dir, status: 'updated' });
231
+ const [r] = await installAgentSkill([{ agent: s.agent, scope: s.scope }], true);
232
+ results.push({ id: AGENT_SKILL, agent: s.agent, scope: s.scope, dir: r.dir, status: 'updated' });
235
233
  }
236
234
  continue;
237
235
  }
@@ -250,29 +248,29 @@ export async function update(name, sel, check) {
250
248
  done({ skills: results }, () => {
251
249
  const label = { updated: green('已更新'), up_to_date: dim('已是最新'), outdated: cyan('有更新'), unknown: dim('无法比较') };
252
250
  for (const r of results)
253
- info(`${label[r.status]} ${r.id} ${dim(r.dir)}`);
251
+ info(`${label[r.status]} ${r.id} ${dim(r.dir)}`);
254
252
  if (check && results.some((r) => r.status === 'outdated'))
255
- info(dim(`运行 ${cyan('funcoding skills update')} 更新。`));
253
+ next(cyan('funcoding skills update'));
256
254
  });
257
255
  }
258
256
  // ---------------- remove ----------------
259
257
  export async function remove(names, sel, force) {
260
258
  if (names.length === 0)
261
- fail('usage', '缺少要删除的 Skill。', '运行 funcoding skills list 查看已安装的 Skill');
259
+ fail('usage', '缺少要删除的 Skill', '运行 funcoding skills list');
262
260
  const all = await scanAll(sel);
263
261
  const removed = [];
264
262
  for (const name of names) {
265
263
  const found = all.filter((s) => s.folder === name || s.source?.id === name.toLowerCase());
266
264
  if (found.length === 0)
267
- fail('not_found', `没有找到已安装的 ${name}`, '运行 funcoding skills list 查看已安装的 Skill');
265
+ fail('not_found', `没有找到已安装的 ${name}`, '运行 funcoding skills list');
268
266
  const manual = found.filter((s) => !s.source);
269
267
  if (manual.length > 0 && !force)
270
- fail('usage', `${manual.map((s) => s.dir).join('、')} 不是由 funcoding 安装的。`, '确认要删除就加 --force(会先备份到配置目录)', { paths: manual.map((s) => s.dir) });
268
+ fail('usage', `不是由 funcoding 安装的:${manual.map((s) => s.dir).join('、')}`, '删除请加 --force(会先备份)', { paths: manual.map((s) => s.dir) });
271
269
  for (const s of found)
272
270
  removed.push({ id: s.source?.id ?? null, agent: s.agent, scope: s.scope, dir: s.dir, backup: await moveAway(s.dir, s.agent, s.folder) });
273
271
  }
274
272
  done({ removed }, () => {
275
273
  for (const r of removed)
276
- info(`${green('✓ 已删除')} ${r.dir} ${dim(`(备份在 ${r.backup})`)}`);
274
+ success(`已删除 ${r.dir.split(/[\\/]/).pop()}(${AGENTS[r.agent].label})`, [r.dir, `备份:${r.backup}`]);
277
275
  });
278
276
  }