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.
package/dist/lists.js ADDED
@@ -0,0 +1,42 @@
1
+ import { api } from './api.js';
2
+ import { installCards, printCards, printItems } from './sync.js';
3
+ import { bold, cyan, dim, done, fail, info, next } from './ui.js';
4
+ const SHARE_ID = /^[23456789abcdefghijkmnpqrstuvwxyz]{10}$/;
5
+ /** 从分享链接或裸 id 里取出清单 id,不合法返回 undefined */
6
+ export function parseShareId(input) {
7
+ const s = input.trim();
8
+ const m = /\/share\/([^/?#]+)/.exec(s);
9
+ const id = (m ? m[1] : s).toLowerCase();
10
+ return SHARE_ID.test(id) ? id : undefined;
11
+ }
12
+ async function fetchList(input) {
13
+ const id = parseShareId(input);
14
+ if (!id)
15
+ fail('usage', `无法识别的清单链接:${input}`, '链接形如 https://funcoding.ai/share/k3x9q2abcd/');
16
+ return api(`/api/cli/lists/${id}`);
17
+ }
18
+ export async function listInfo(input) {
19
+ const list = await fetchList(input);
20
+ done({ ...list }, () => {
21
+ info(`${bold(list.title)} ${dim(`· ${list.owner.name} 分享`)}`);
22
+ if (list.description)
23
+ info(list.description);
24
+ info(dim(list.url));
25
+ info('');
26
+ if (list.skills.length + list.mcp.length === 0)
27
+ return info('清单为空');
28
+ printCards('Skill', list.skills, (c) => `funcoding skills add ${c.id}`);
29
+ printCards('MCP Server', list.mcp, (c) => `funcoding mcp add ${c.id}`);
30
+ next(cyan(`funcoding lists add ${list.shareId}`));
31
+ });
32
+ }
33
+ export async function listAdd(input, targets, dryRun) {
34
+ const list = await fetchList(input);
35
+ const items = await installCards(list, targets, dryRun);
36
+ done({ dryRun, list: { shareId: list.shareId, title: list.title, url: list.url }, items }, () => {
37
+ info(`${bold(list.title)} ${dim(`· ${list.owner.name} 分享`)}`);
38
+ if (items.length === 0)
39
+ return info('清单为空');
40
+ printItems(items);
41
+ });
42
+ }
package/dist/mcp.js CHANGED
@@ -11,7 +11,7 @@ import { AGENT_IDS, AGENTS, codexHome } from './agents.js';
11
11
  import { api, siteUrl } from './api.js';
12
12
  import { jsonServerNames, setJsonServer, setTomlServer, tomlServerNames } from './config-files.js';
13
13
  import { configDir } from './credentials.js';
14
- import { bold, CliError, dim, done, fail, green, info, stars, warn } from './ui.js';
14
+ import { bold, CliError, cyan, dim, done, fail, info, next, stars, success, warn } from './ui.js';
15
15
  /** 接受 owner/repo[/sub],或者网站上的详情页地址 */
16
16
  export function parseMcpSpec(input) {
17
17
  const s = input.trim();
@@ -22,13 +22,13 @@ export function parseMcpSpec(input) {
22
22
  export async function fetchMcp(spec) {
23
23
  const id = parseMcpSpec(spec);
24
24
  if (!id)
25
- return fail('usage', `MCP Server 要写成 owner/repo 或 owner/repo/sub,或者网站上的详情页地址:${spec}`);
25
+ return fail('usage', `无法识别的 MCP Server:${spec}`, '写成 owner/repo、owner/repo/sub,或网站上的详情页地址');
26
26
  try {
27
27
  return await api(`/api/cli/mcp/${id}`);
28
28
  }
29
29
  catch (err) {
30
30
  if (err instanceof CliError && err.code === 'not_found')
31
- fail('not_found', `没有找到 MCP Server:${id}`, '运行 funcoding mcp find <关键词> 查找');
31
+ fail('not_found', `没有找到 MCP Server:${id}`, '运行 funcoding mcp find <关键词>');
32
32
  throw err;
33
33
  }
34
34
  }
@@ -47,7 +47,7 @@ export async function mcpInfo(spec) {
47
47
  info(dim(page));
48
48
  info('');
49
49
  if (s.launches.length === 0)
50
- return info(`没有登记标准安装包,配置方法见 README:${s.repo.url}`);
50
+ return info(`没有标准安装包,按 README 手动配置:${s.repo.url}`);
51
51
  info(bold(`启动方式(配置名 ${s.configName})`));
52
52
  for (const l of s.launches) {
53
53
  info(` ${describeLaunch(l)}`);
@@ -73,7 +73,7 @@ async function configuredNames(loc) {
73
73
  return loc.format === 'toml' ? tomlServerNames(text) : jsonServerNames(text);
74
74
  }
75
75
  catch {
76
- warn(`无法解析 ${file},已跳过`);
76
+ warn(`无法解析,已跳过:${file}`);
77
77
  return [];
78
78
  }
79
79
  }
@@ -96,7 +96,7 @@ async function writeConfig(loc, name, config) {
96
96
  next = setJsonServer(text, name, config ? jsonEntry(loc.agent, config) : null);
97
97
  }
98
98
  catch (err) {
99
- return fail('error', `无法解析 ${loc.file}:${err.message}`, '请先修正这个文件');
99
+ return fail('error', `无法解析:${loc.file}(${err.message})`, '修正这个文件后重试');
100
100
  }
101
101
  }
102
102
  const backup = await backupFile(loc.file, loc.agent);
@@ -147,25 +147,25 @@ export function toConfig(l, opts) {
147
147
  /** 配置一个 Server 到多个目标;失败抛 CliError */
148
148
  export async function addServer(s, opts) {
149
149
  if (s.devtool)
150
- warn(`${s.name} 被归为 MCP 开发工具(SDK、框架等),可能不是可以直接运行的 Server。`);
150
+ warn(`${s.name} 属于 MCP 开发工具(SDK、框架等),可能不能直接运行`);
151
151
  if (s.launches.length === 0)
152
- fail('not_found', `${s.name} 没有登记标准安装包,无法自动配置。`, `按 README 手动配置:${s.repo.url}`, { readme: s.repo.url });
152
+ fail('not_found', `${s.name} 没有标准安装包`, `按 README 手动配置:${s.repo.url}`, { readme: s.repo.url });
153
153
  const name = opts.name ?? s.configName;
154
154
  if (!/^[A-Za-z0-9_-]{1,64}$/.test(name))
155
- fail('usage', `配置名只能包含字母、数字、下划线和连字符:${name}`);
155
+ fail('usage', `配置名只能用字母、数字、下划线和连字符:${name}`);
156
156
  // 先检查所有目标,确定都能写再动手
157
157
  const plan = [];
158
158
  for (const t of opts.targets) {
159
159
  const picked = pickLaunch(s.launches, t.agent, opts);
160
160
  if (!picked)
161
- fail('not_found', `${s.name} 没有 ${AGENTS[t.agent].label} 支持的启动方式${opts.via ? `(--via ${opts.via})` : ''}。`, `可选的启动方式见 funcoding mcp info ${s.id}`);
161
+ fail('not_found', `${s.name} 没有 ${AGENTS[t.agent].label} 支持的启动方式${opts.via ? `(--via ${opts.via})` : ''}`, `运行 funcoding mcp info ${s.id}`);
162
162
  if ('missing' in picked) {
163
163
  const { launch, inputs } = picked.missing;
164
164
  fail('usage', `${s.name} 需要提供:${inputs.join('、')}`, `向用户索取后运行 funcoding mcp add ${s.id} ${inputs.map((i) => `${flagOf(launch)} ${i}=<值>`).join(' ')}`, { missing: inputs, flag: flagOf(launch) });
165
165
  }
166
166
  const loc = locationOf(t.agent, t.scope);
167
167
  if (!opts.force && (await configuredNames(loc)).includes(name))
168
- fail('exists', `${AGENTS[t.agent].label} 里已经配置了 ${name}。`, '确认要覆盖就加 --force');
168
+ fail('exists', `${AGENTS[t.agent].label} 已配置 ${name}`, '覆盖请加 --force');
169
169
  plan.push({ loc, config: toConfig(picked.launch, opts), launch: picked.launch });
170
170
  }
171
171
  const results = [];
@@ -183,7 +183,7 @@ export async function addServer(s, opts) {
183
183
  if (r)
184
184
  fail('error', `claude mcp add-json 失败:${r.output}`);
185
185
  if (!loc.file)
186
- fail('error', '没有找到 claude 命令,无法写入 Claude Code 的用户范围配置。', `安装 Claude Code 后运行:claude mcp add-json ${name} ${shellQuote(json)} --scope user`);
186
+ fail('error', '没有找到 claude 命令,无法写入 Claude Code 的用户配置', `安装 Claude Code 后运行:claude mcp add-json ${name} ${shellQuote(json)} --scope user`);
187
187
  }
188
188
  const backup = await writeConfig(loc, name, config);
189
189
  results.push({ ...base, file: loc.file, ...(backup ? { backup } : {}) });
@@ -193,16 +193,11 @@ export async function addServer(s, opts) {
193
193
  export async function mcpAdd(spec, opts) {
194
194
  const results = await addServer(await fetchMcp(spec), opts);
195
195
  done({ added: results }, () => {
196
- for (const r of results) {
197
- info(green(`✓ 已在 ${AGENTS[r.agent].label}(${r.scope === 'user' ? '个人' : '项目'})配置 ${r.name}`) + dim(` · ${r.launch}`));
198
- if (r.file)
199
- info(dim(` ${r.file}`));
200
- if (r.backup)
201
- info(dim(` 原文件备份在 ${r.backup}`));
202
- }
196
+ for (const r of results)
197
+ success(`已配置 ${r.name}(${AGENTS[r.agent].label},${r.scope === 'user' ? '用户目录' : '当前项目'})`, [r.launch, r.file, r.backup && `备份:${r.backup}`]);
203
198
  if (results.some((r) => r.agent === 'claude-code' && r.scope === 'project'))
204
- info(dim('项目范围的 Server 需要在 Claude Code 里确认后才会启用。'));
205
- info(dim('重启智能体或新开会话后生效。'));
199
+ warn('当前项目的 Server 需要在 Claude Code 里确认后才会启用');
200
+ next('重启智能体或新开会话');
206
201
  });
207
202
  }
208
203
  // ---------------- list / remove ----------------
@@ -215,14 +210,16 @@ export async function mcpList(sel) {
215
210
  servers.push({ agent, scope, name, file: loc.file ?? join(homedir(), '.claude.json') });
216
211
  }
217
212
  done({ servers }, () => {
218
- if (servers.length === 0)
219
- return info('还没有配置 MCP Server。');
213
+ if (servers.length === 0) {
214
+ info('暂无已配置的 MCP Server');
215
+ return next(cyan('funcoding mcp find <关键词>'));
216
+ }
220
217
  for (const agent of AGENT_IDS)
221
218
  for (const scope of sel.scopes) {
222
219
  const group = servers.filter((s) => s.agent === agent && s.scope === scope);
223
220
  if (group.length === 0)
224
221
  continue;
225
- info(`${bold(AGENTS[agent].label)} ${dim(`${scope === 'user' ? '个人' : '项目'} · ${group[0].file}`)}`);
222
+ info(`${bold(AGENTS[agent].label)} ${dim(`${scope === 'user' ? '用户目录' : '当前项目'} ${group[0].file}`)}`);
226
223
  for (const s of group)
227
224
  info(` ${s.name}`);
228
225
  info('');
@@ -231,7 +228,7 @@ export async function mcpList(sel) {
231
228
  }
232
229
  export async function mcpRemove(names, sel) {
233
230
  if (names.length === 0)
234
- fail('usage', '缺少要删除的 MCP Server 配置名。', '运行 funcoding mcp list 查看');
231
+ fail('usage', '缺少要删除的配置名', '运行 funcoding mcp list');
235
232
  const removed = [];
236
233
  for (const name of names) {
237
234
  let found = false;
@@ -250,16 +247,16 @@ export async function mcpRemove(names, sel) {
250
247
  if (r)
251
248
  fail('error', `claude mcp remove 失败:${r.output}`);
252
249
  if (!loc.file)
253
- fail('error', '没有找到 claude 命令,无法修改 Claude Code 的用户范围配置。', `安装 Claude Code 后运行:claude mcp remove ${name} --scope user`);
250
+ fail('error', '没有找到 claude 命令,无法修改 Claude Code 的用户配置', `安装 Claude Code 后运行:claude mcp remove ${name} --scope user`);
254
251
  }
255
252
  const backup = await writeConfig(loc, name, null);
256
253
  removed.push({ agent, scope, name, file: loc.file, ...(backup ? { backup } : {}) });
257
254
  }
258
255
  if (!found)
259
- fail('not_found', `没有找到配置名为 ${name} 的 MCP Server`, '运行 funcoding mcp list 查看');
256
+ fail('not_found', `没有找到配置名为 ${name} 的 MCP Server`, '运行 funcoding mcp list');
260
257
  }
261
258
  done({ removed }, () => {
262
259
  for (const r of removed)
263
- info(`${green('✓ 已删除')} ${AGENTS[r.agent].label} ${r.name}${r.file ? dim(` · ${r.file}`) : ''}`);
260
+ success(`已删除 ${r.name}(${AGENTS[r.agent].label})`, [r.file]);
264
261
  });
265
262
  }
package/dist/setup.js CHANGED
@@ -1,19 +1,27 @@
1
1
  /**
2
- * setup:
3
- * 1. 没有全局安装时用 npm 全局安装本 CLI,之后终端里可以直接用 funcoding 或简写 fun(--no-global 跳过)
4
- * 2. 把随 CLI 发布的 funcoding Skill(skill/funcoding/SKILL.md)装到智能体,让它知道什么时候、怎么用这个 CLI。
5
- * 这个 Skill 不在网站目录里,来源记录标成 builtin,update 时从当前版本的 CLI 重新复制
2
+ * cli setup / cli upgrade,做法参照 sentry-cli(cli.sentry.dev/agentic-usage):
3
+ * - setup:没有全局安装时用 npm 全局安装本 CLI(--no-global 跳过),再把随 CLI 发布的 funcoding-cli Skill
4
+ * (skill/funcoding-cli/:SKILL.md + references/)装到本机检测到的各智能体的用户目录(--no-agent-skills 跳过),
5
+ * 让智能体知道什么时候、怎么用这个 CLI。Skill 打包在 CLI 里,不联网
6
+ * - upgrade:全局安装最新版,再用新版刷新已安装的 funcoding-cli Skill
7
+ * 旧版本装的 funcoding Skill(init 时代)在 setup 和刷新时一并删掉
6
8
  */
7
9
  import { spawnSync } from 'node:child_process';
8
10
  import { randomBytes } from 'node:crypto';
9
11
  import { existsSync } from 'node:fs';
10
- import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
11
- import { delimiter, dirname, join } from 'node:path';
12
- import { AGENTS, skillsRoot } from './agents.js';
12
+ import { cp, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
13
+ import { homedir } from 'node:os';
14
+ import { delimiter, join } from 'node:path';
15
+ import { AGENT_IDS, AGENTS, DEFAULT_AGENT, installedAgents, skillsRoot } from './agents.js';
13
16
  import { USER_AGENT, VERSION } from './api.js';
14
17
  import { META_FILE, moveAway } from './install.js';
15
- import { cyan, dim, done, fail, green, info, warn } from './ui.js';
18
+ import { done, fail, next, step, success, warn } from './ui.js';
16
19
  const isWindows = process.platform === 'win32';
20
+ /** 随 CLI 发布的 Skill 的目录名,也是它的 name */
21
+ export const AGENT_SKILL = 'funcoding-cli';
22
+ /** 0.3 及以前 init 装的同类 Skill,setup 和刷新时删掉 */
23
+ const LEGACY_SKILL = 'funcoding';
24
+ const SOURCE_DIR = new URL('../skill/funcoding-cli/', import.meta.url);
17
25
  /** PATH 上能不能找到这个命令;npx 运行时会把临时目录加进 PATH,那些目录不算 */
18
26
  export function onPath(command, env = process.env) {
19
27
  const exts = isWindows ? ['.cmd', '.exe', ''] : [''];
@@ -22,57 +30,111 @@ export function onPath(command, env = process.env) {
22
30
  .filter((dir) => dir && !/[\\/]_npx[\\/]/.test(dir))
23
31
  .some((dir) => exts.some((ext) => existsSync(join(dir, command + ext))));
24
32
  }
25
- /** 全局安装当前版本;npm 的输出写到 stderr,不影响 --json 的 stdout */
33
+ const EACCES_DOC = 'https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally';
34
+ function npmInstall(spec) {
35
+ // npm 的输出写到 stderr,不影响 --json 的 stdout
36
+ return spawnSync('npm', ['install', '--global', spec], { stdio: ['ignore', process.stderr, process.stderr], shell: isWindows }).status === 0;
37
+ }
26
38
  function installGlobal() {
27
39
  if (onPath('funcoding'))
28
40
  return { status: 'present' };
29
41
  const pkg = `funcoding-cli@${VERSION}`;
30
- info(dim(`正在全局安装 ${pkg},之后可以直接使用 funcoding 或 fun 命令…`));
31
- const r = spawnSync('npm', ['install', '--global', pkg], { stdio: ['ignore', process.stderr, process.stderr], shell: isWindows });
32
- if (r.status === 0)
42
+ step(`正在全局安装 funcoding-cli ${VERSION}`);
43
+ if (npmInstall(pkg))
33
44
  return { status: 'installed' };
34
- return { status: 'failed', hint: `手动运行 npm install -g ${pkg}(权限不足时参考 https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally)` };
45
+ return { status: 'failed', hint: `运行 npm install -g ${pkg}(权限不足见 ${EACCES_DOC})` };
35
46
  }
36
- export const BUILTIN_ID = 'funcoding';
37
- const SOURCE = new URL('../skill/funcoding/SKILL.md', import.meta.url);
38
- export const builtinContent = () => readFile(SOURCE, 'utf8');
39
- export async function installBuiltin(targets, force) {
40
- const content = await builtinContent();
41
- const dirs = targets.map((t) => ({ ...t, dir: join(skillsRoot(t.agent, t.scope), BUILTIN_ID) }));
47
+ /**
48
+ * 要装 Skill 的智能体:本机检测到的全部;~/.agents 存在也算(Codex 和其他读 ~/.agents/skills 的智能体用它);
49
+ * 一个都没检测到时装给默认的 Claude Code
50
+ */
51
+ export function skillAgents(home = homedir()) {
52
+ const found = new Set(installedAgents({ home }));
53
+ if (existsSync(join(home, '.agents')))
54
+ found.add('codex');
55
+ return found.size > 0 ? AGENT_IDS.filter((a) => found.has(a)) : [DEFAULT_AGENT];
56
+ }
57
+ /** 随 CLI 发布的 SKILL.md(skills update --check 用来比较) */
58
+ export const agentSkillContent = () => readFile(new URL('SKILL.md', SOURCE_DIR), 'utf8');
59
+ const exists = (p) => stat(p).then(() => true, () => false);
60
+ const isOurs = async (dir) => {
61
+ const meta = await readFile(join(dir, META_FILE), 'utf8').catch(() => null);
62
+ return !!meta && !!JSON.parse(meta).builtin;
63
+ };
64
+ /** 把 funcoding-cli Skill 装到这些智能体;同名但不是本 CLI 装的目录要 --force,并且先备份 */
65
+ export async function installAgentSkill(targets, force) {
66
+ const plan = targets.map((t) => ({ ...t, root: skillsRoot(t.agent, t.scope), dir: join(skillsRoot(t.agent, t.scope), AGENT_SKILL) }));
42
67
  const foreign = [];
43
- for (const d of dirs) {
44
- const meta = await readFile(join(d.dir, META_FILE), 'utf8').catch(() => null);
45
- const present = meta !== null || (await stat(d.dir).then(() => true, () => false));
46
- // 之前 setup 装的直接换成新版;同名的其他 Skill 要 --force,并且先备份
47
- if (present && !(meta && JSON.parse(meta).builtin))
48
- foreign.push(d);
49
- }
68
+ for (const p of plan)
69
+ if ((await exists(p.dir)) && !(await isOurs(p.dir)))
70
+ foreign.push(p.dir);
50
71
  if (foreign.length > 0 && !force)
51
- fail('exists', `${foreign.map((d) => d.dir).join('、')} 已存在,且不是 funcoding setup 安装的。`, '确认要覆盖就加 --force(会先备份到配置目录)');
52
- for (const d of dirs) {
53
- const tmp = join(dirname(d.dir), `.funcoding-tmp-${randomBytes(4).toString('hex')}`);
54
- await mkdir(tmp, { recursive: true });
55
- await writeFile(join(tmp, 'SKILL.md'), content);
56
- await writeFile(join(tmp, META_FILE), `${JSON.stringify({ id: BUILTIN_ID, name: BUILTIN_ID, builtin: true, installedAt: new Date().toISOString(), cli: USER_AGENT }, null, 2)}\n`);
57
- if (foreign.includes(d))
58
- await moveAway(d.dir, d.agent, BUILTIN_ID);
59
- else
60
- await rm(d.dir, { recursive: true, force: true });
61
- await rename(tmp, d.dir);
72
+ fail('exists', `已存在同名 Skill:${foreign.join('、')}`, '覆盖请加 --force(会先备份)', { paths: foreign });
73
+ const results = [];
74
+ for (const p of plan) {
75
+ await mkdir(p.root, { recursive: true });
76
+ const tmp = join(p.root, `.funcoding-tmp-${randomBytes(4).toString('hex')}`);
77
+ await cp(SOURCE_DIR, tmp, { recursive: true });
78
+ await writeFile(join(tmp, META_FILE), `${JSON.stringify({ id: AGENT_SKILL, name: AGENT_SKILL, builtin: true, installedAt: new Date().toISOString(), cli: USER_AGENT }, null, 2)}\n`);
79
+ let backup;
80
+ if (await exists(p.dir)) {
81
+ if (await isOurs(p.dir))
82
+ await rm(p.dir, { recursive: true, force: true });
83
+ else
84
+ backup = await moveAway(p.dir, p.agent, AGENT_SKILL);
85
+ }
86
+ await rename(tmp, p.dir);
87
+ // 旧版 funcoding Skill:只删本 CLI 装的
88
+ const legacy = join(p.root, LEGACY_SKILL);
89
+ const removedLegacy = (await isOurs(legacy)) ? legacy : undefined;
90
+ if (removedLegacy)
91
+ await rm(removedLegacy, { recursive: true, force: true });
92
+ results.push({ agent: p.agent, scope: p.scope, dir: p.dir, ...(backup ? { backup } : {}), ...(removedLegacy ? { removedLegacy } : {}) });
62
93
  }
63
- return dirs;
94
+ return results;
64
95
  }
65
- export async function setup(targets, force, global) {
66
- const cli = global ? installGlobal() : { status: 'skipped' };
67
- const dirs = await installBuiltin(targets, force);
68
- done({ cli, installed: dirs.map((d) => ({ id: BUILTIN_ID, agent: d.agent, scope: d.scope, dir: d.dir })) }, () => {
96
+ export async function setup(opts) {
97
+ const cli = opts.global ? installGlobal() : { status: 'skipped' };
98
+ const skills = opts.agentSkills ? await installAgentSkill(skillAgents().map((agent) => ({ agent, scope: 'user' })), opts.force) : [];
99
+ done({ version: VERSION, cli, skills }, () => {
69
100
  if (cli.status === 'installed')
70
- info(green(`✓ 已全局安装 funcoding-cli,在终端里输入 ${cyan('funcoding')} 或 ${cyan('fun')} 即可使用`));
101
+ success(`已全局安装 funcoding-cli ${VERSION}`, ['命令:funcoding(简写 fun)']);
102
+ if (cli.status === 'present')
103
+ success(`已全局安装 funcoding-cli`, ['命令:funcoding(简写 fun)']);
71
104
  if (cli.status === 'failed')
72
- warn(`全局安装失败,${cli.hint}`);
73
- for (const d of dirs)
74
- info(green(`✓ 已把 funcoding Skill 装到 ${d.dir}`));
75
- info(`之后可以直接让 ${dirs.map((d) => AGENTS[d.agent].label).join('、')} 帮你查找和安装 Skill、配置 MCP,例如「找一个处理 PDF 的 Skill 装上」。`);
76
- info(dim(`也可以输入 ${[...new Set(dirs.map((d) => cyan(AGENTS[d.agent].invoke(BUILTIN_ID))))].join(' / ')} 主动调用。新开会话后生效。`));
105
+ warn(`全局安装失败。${cli.hint}`);
106
+ if (skills.length > 0) {
107
+ success(`已为 ${skills.map((s) => AGENTS[s.agent].label).join('、')} 安装智能体 Skill:${AGENT_SKILL}`, [
108
+ ...skills.map((s) => s.dir),
109
+ ...skills.flatMap((s) => (s.backup ? [`备份:${s.backup}`] : [])),
110
+ ]);
111
+ next('新开智能体会话,直接描述需求');
112
+ }
113
+ });
114
+ }
115
+ export function upgrade() {
116
+ step('正在全局安装 funcoding-cli@latest');
117
+ if (!npmInstall('funcoding-cli@latest'))
118
+ fail('error', '升级失败', `运行 npm install -g funcoding-cli@latest(权限不足见 ${EACCES_DOC})`);
119
+ const version = spawnSync('funcoding', ['--version'], { encoding: 'utf8', shell: isWindows }).stdout?.trim() || null;
120
+ // 用新版本刷新已安装的智能体 Skill(包括旧版的 funcoding);一个都没装过时会报未找到,忽略
121
+ const refreshed = spawnSync('funcoding', ['skills', 'update', AGENT_SKILL, '--json'], { encoding: 'utf8', shell: isWindows });
122
+ const skills = (() => {
123
+ try {
124
+ return JSON.parse(refreshed.stdout).skills ?? [];
125
+ }
126
+ catch {
127
+ return [];
128
+ }
129
+ })();
130
+ done({ from: VERSION, to: version, skills }, () => {
131
+ if (version && version !== VERSION)
132
+ success(`已升级到 funcoding-cli ${version}`, [`原版本 ${VERSION}`]);
133
+ else
134
+ success(`已是最新版本 ${version ?? VERSION}`);
135
+ if (skills.length > 0)
136
+ success('已刷新智能体 Skill', skills.map((s) => s.dir));
77
137
  });
78
138
  }
139
+ /** 用于 info:本机已装的 funcoding-cli Skill 目录 */
140
+ export const agentSkillDirs = (home = homedir()) => AGENT_IDS.map((a) => join(home, ...AGENTS[a].skills, AGENT_SKILL)).filter((d) => existsSync(d));
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 由命令表生成 funcoding-cli Skill 的 references/<分组>.md(每组命令一份,智能体按需读取),
3
+ * 并把 SKILL.md frontmatter 里的 version 同步成 package.json 的版本。
4
+ * 写文件在 scripts/gen-skill.ts(pnpm skill),skill-docs.test.ts 检查仓库里的文件是不是最新的
5
+ */
6
+ import { BIN, COMMANDS, GLOBAL, OPTIONS } from './commands.js';
7
+ function renderCommand(group, cmd) {
8
+ const params = (cmd.params ?? []).map(([p]) => p).join(' ');
9
+ const out = [`## ${[BIN, group.name, cmd.name, params].filter(Boolean).join(' ')}`, '', cmd.summary];
10
+ if (cmd.params?.length)
11
+ out.push('', '参数:', '', ...cmd.params.map(([p, d]) => `- \`${p}\`:${d}`));
12
+ if (cmd.options?.length)
13
+ out.push('', '选项:', '', ...cmd.options.map((k) => `- \`${OPTIONS[k].flag}\`:${OPTIONS[k].desc}`));
14
+ if (cmd.examples?.length)
15
+ out.push('', '示例:', '', '```bash', ...cmd.examples, '```');
16
+ return out.join('\n');
17
+ }
18
+ /** 文件名 → 内容 */
19
+ export function renderReferences() {
20
+ const out = {};
21
+ const global = GLOBAL.map((k) => `\`${OPTIONS[k].flag}\``).join('、');
22
+ for (const group of COMMANDS.filter((c) => c.commands)) {
23
+ const body = [
24
+ `# ${BIN} ${group.name}`,
25
+ '',
26
+ group.summary,
27
+ '',
28
+ `所有命令都支持 ${global}。本文件由 scripts/gen-skill.ts 根据命令表生成。`,
29
+ ...group.commands.flatMap((cmd) => ['', renderCommand(group, cmd)]),
30
+ ];
31
+ out[`${group.name}.md`] = `${body.join('\n')}\n`;
32
+ }
33
+ return out;
34
+ }
35
+ /** 把 SKILL.md frontmatter 里的 version 换成 version */
36
+ export const withVersion = (skillMd, version) => skillMd.replace(/^version: .*$/m, `version: ${version}`);
package/dist/sync.js CHANGED
@@ -2,8 +2,8 @@ import { siteUrl } from './api.js';
2
2
  import { fetchMe, requireToken } from './auth.js';
3
3
  import { fetchSkill, installSkill, scanAll } from './install.js';
4
4
  import { addServer, fetchMcp } from './mcp.js';
5
- import { bold, CliError, cyan, dim, done, green, info, stars, truncate, yellow } from './ui.js';
6
- function printCards(title, cards, hrefOf) {
5
+ import { bold, CliError, cyan, dim, done, green, info, next, stars, truncate, yellow } from './ui.js';
6
+ export function printCards(title, cards, hrefOf) {
7
7
  if (cards.length === 0)
8
8
  return;
9
9
  info(bold(title));
@@ -19,20 +19,33 @@ export async function favorites() {
19
19
  const me = await fetchMe(await requireToken());
20
20
  const { skills, mcp } = me.favorites;
21
21
  done({ skills, mcp }, () => {
22
- if (skills.length + mcp.length === 0)
23
- return info(`还没有收藏。可以在 ${siteUrl('/skills/')} 收藏 Skill 和 MCP Server。`);
22
+ if (skills.length + mcp.length === 0) {
23
+ info('暂无收藏');
24
+ return next(`在 ${siteUrl('/skills/')} 收藏 Skill 或 MCP Server`);
25
+ }
24
26
  printCards('收藏的 Skill', skills, (c) => `funcoding skills add ${c.id}`);
25
27
  printCards('收藏的 MCP Server', mcp, (c) => `funcoding mcp add ${c.id}`);
26
- info(dim(`运行 ${cyan('funcoding favorites sync')} 一次安装全部收藏。`));
28
+ next(cyan('funcoding favorites sync'));
27
29
  });
28
30
  }
29
31
  export async function sync(targets, dryRun) {
30
32
  const me = await fetchMe(await requireToken());
33
+ const items = await installCards(me.favorites, targets, dryRun);
34
+ done({ dryRun, items }, () => {
35
+ if (items.length === 0) {
36
+ info('暂无收藏');
37
+ return next(`在 ${siteUrl('/skills/')} 收藏 Skill 或 MCP Server`);
38
+ }
39
+ printItems(items);
40
+ });
41
+ }
42
+ /** 把一组 Skill 和 MCP Server 装到 targets,已装的跳过;单个条目失败只记为跳过 */
43
+ export async function installCards(cards, targets, dryRun) {
31
44
  const items = [];
32
45
  const agents = [...new Set(targets.map((t) => t.agent))];
33
46
  const scopes = [...new Set(targets.map((t) => t.scope))];
34
47
  const installed = await scanAll({ agents, scopes });
35
- for (const card of me.favorites.skills) {
48
+ for (const card of cards.skills) {
36
49
  const missing = targets.filter((t) => !installed.some((s) => s.agent === t.agent && s.scope === t.scope && s.source?.id === card.id));
37
50
  if (missing.length === 0) {
38
51
  items.push({ kind: 'skill', id: card.id, status: 'present', agents });
@@ -53,7 +66,7 @@ export async function sync(targets, dryRun) {
53
66
  items.push({ kind: 'skill', id: card.id, status: 'skipped', agents: missing.map((t) => t.agent), reason: err.message, hint: err.hint });
54
67
  }
55
68
  }
56
- for (const card of me.favorites.mcp) {
69
+ for (const card of cards.mcp) {
57
70
  try {
58
71
  const server = await fetchMcp(card.id);
59
72
  if (dryRun) {
@@ -73,14 +86,14 @@ export async function sync(targets, dryRun) {
73
86
  items.push({ kind: 'mcp', id: card.id, status: 'skipped', agents, reason: err.message, hint: err.hint ?? (err.code === 'usage' ? undefined : `funcoding mcp info ${card.id}`) });
74
87
  }
75
88
  }
76
- done({ dryRun, items }, () => {
77
- if (items.length === 0)
78
- return info(`还没有收藏。可以在 ${siteUrl('/skills/')} 收藏 Skill 和 MCP Server。`);
79
- const label = { installed: green('新装'), present: dim('已装'), skipped: yellow('跳过'), planned: cyan('将安装') };
80
- for (const i of items) {
81
- info(`${label[i.status]} ${i.kind === 'skill' ? 'Skill' : 'MCP'} ${i.id}${i.reason ? dim(`:${i.reason}`) : ''}`);
82
- if (i.hint)
83
- info(dim(` ${i.hint}`));
84
- }
85
- });
89
+ return items;
90
+ }
91
+ /** 按「已安装 / 已存在 / 跳过 / 待安装」逐行打印 installCards 的结果 */
92
+ export function printItems(items) {
93
+ const label = { installed: green('已安装'), present: dim('已存在'), skipped: yellow('跳过 '), planned: cyan('待安装') };
94
+ for (const i of items) {
95
+ info(`${label[i.status]} ${i.kind === 'skill' ? 'Skill' : 'MCP'} ${i.id}${i.reason ? dim(`:${i.reason}`) : ''}`);
96
+ if (i.hint)
97
+ info(dim(` ${i.hint}`));
98
+ }
86
99
  }
package/dist/ui.js CHANGED
@@ -4,7 +4,14 @@
4
4
  * 过程提示一律写到 stderr;不加时是给人看的文字
5
5
  * - 退出码固定(见 EXIT),智能体可以据此决定下一步
6
6
  * - 颜色只在交互终端里用,NO_COLOR 或 --json 时关闭
7
+ * 给人看的文字统一用这几种行(写法规则见 README「输出」):
8
+ * 正在… step() 进度,灰色
9
+ * ✓ 已<动词><对象> success() 结果;下面缩进两格、灰色的是细节(路径、版本、备份位置)
10
+ * ! … warn() 提醒
11
+ * ✗ … reportError() 错误,下一行是怎么办
12
+ * 下一步:… next() 只在确实需要时出现
7
13
  */
14
+ import { homedir } from 'node:os';
8
15
  export const EXIT = { ok: 0, error: 1, usage: 2, not_found: 3, exists: 4, auth: 5 };
9
16
  export class CliError extends Error {
10
17
  code;
@@ -22,6 +29,11 @@ let json = false;
22
29
  export const setJsonMode = (v) => {
23
30
  json = v;
24
31
  };
32
+ /** --quiet:不输出给人看的文字(--json 的结果照常输出) */
33
+ let quiet = false;
34
+ export const setQuiet = (v) => {
35
+ quiet = v;
36
+ };
25
37
  export const isJsonMode = () => json;
26
38
  const color = () => !json && !!process.stdout.isTTY && !process.env.NO_COLOR;
27
39
  const wrap = (code) => (s) => (color() ? `\x1b[${code}m${s}\x1b[0m` : s);
@@ -32,8 +44,37 @@ export const yellow = wrap('33');
32
44
  export const red = wrap('31');
33
45
  export const cyan = wrap('36');
34
46
  /** 给人看的输出:--json 时改写到 stderr,不污染 stdout 的 JSON */
35
- export const info = (msg) => (json ? console.error(msg) : console.log(msg));
36
- export const warn = (msg) => console.error(yellow(`! ${msg}`));
47
+ export const info = (msg) => {
48
+ if (quiet)
49
+ return;
50
+ if (json)
51
+ console.error(msg);
52
+ else
53
+ console.log(msg);
54
+ };
55
+ export const warn = (msg) => {
56
+ if (!quiet)
57
+ console.error(yellow(`! ${msg}`));
58
+ };
59
+ /** 进度:正在做什么 */
60
+ export const step = (msg) => info(dim(`${msg}…`));
61
+ /** 给人看的文字里,用户主目录写成 ~(细节行里可能是「备份:<路径>」) */
62
+ export const tildify = (text) => {
63
+ const home = homedir();
64
+ return text.replace(new RegExp(`${home.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?=[\\/]|$)`, 'g'), '~');
65
+ };
66
+ /** 结果:✓ 一行,下面是缩进的细节(路径里的用户主目录写成 ~) */
67
+ export const success = (msg, details = []) => {
68
+ info(green(`✓ ${msg}`));
69
+ for (const d of details)
70
+ if (d)
71
+ info(dim(` ${tildify(d)}`));
72
+ };
73
+ /** 下一步:一条一行 */
74
+ export const next = (...lines) => {
75
+ for (const l of lines)
76
+ info(`${dim('下一步:')}${l}`);
77
+ };
37
78
  export function fail(code, message, hint, details) {
38
79
  throw new CliError(code, message, hint, details);
39
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "funcoding-cli",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "funcoding.ai 的命令行:给 AI 编程智能体用,查找和安装 Agent Skills、配置 MCP Server、同步收藏、查看 AI 热点和中文文档",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,8 @@
13
13
  "skill"
14
14
  ],
15
15
  "scripts": {
16
- "build": "tsc -p tsconfig.json",
16
+ "skill": "node --experimental-strip-types --no-warnings scripts/gen-skill.ts",
17
+ "build": "pnpm skill && rm -rf dist && tsc -p tsconfig.json",
17
18
  "test": "node --experimental-strip-types --no-warnings --test 'src/**/*.test.ts'",
18
19
  "prepublishOnly": "pnpm build && pnpm test"
19
20
  },