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/README.md +41 -13
- package/dist/auth.js +16 -18
- package/dist/cli.js +8 -214
- package/dist/commands.js +238 -0
- package/dist/content.js +36 -26
- package/dist/info.js +6 -3
- package/dist/install.js +36 -38
- package/dist/lists.js +42 -0
- package/dist/mcp.js +25 -28
- package/dist/setup.js +110 -48
- package/dist/skill-docs.js +36 -0
- package/dist/sync.js +30 -17
- package/dist/ui.js +43 -2
- package/package.json +3 -2
- package/skill/funcoding-cli/SKILL.md +67 -0
- package/skill/funcoding-cli/references/auth.md +21 -0
- package/skill/funcoding-cli/references/cli.md +27 -0
- package/skill/funcoding-cli/references/docs.md +26 -0
- package/skill/funcoding-cli/references/favorites.md +19 -0
- package/skill/funcoding-cli/references/lists.md +27 -0
- package/skill/funcoding-cli/references/mcp.md +73 -0
- package/skill/funcoding-cli/references/news.md +21 -0
- package/skill/funcoding-cli/references/skills.md +86 -0
- package/dist/init.js +0 -90
- package/skill/funcoding/SKILL.md +0 -96
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,
|
|
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',
|
|
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(
|
|
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(
|
|
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',
|
|
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}
|
|
150
|
+
warn(`${s.name} 属于 MCP 开发工具(SDK、框架等),可能不能直接运行`);
|
|
151
151
|
if (s.launches.length === 0)
|
|
152
|
-
fail('not_found', `${s.name}
|
|
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',
|
|
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})` : ''}
|
|
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}
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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' ? '
|
|
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', '
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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 {
|
|
12
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
if (r.status === 0)
|
|
42
|
+
step(`正在全局安装 funcoding-cli ${VERSION}`);
|
|
43
|
+
if (npmInstall(pkg))
|
|
33
44
|
return { status: 'installed' };
|
|
34
|
-
return { status: 'failed', hint:
|
|
45
|
+
return { status: 'failed', hint: `运行 npm install -g ${pkg}(权限不足见 ${EACCES_DOC})` };
|
|
35
46
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
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',
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
await mkdir(
|
|
55
|
-
|
|
56
|
-
await
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
await
|
|
61
|
-
|
|
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
|
|
94
|
+
return results;
|
|
64
95
|
}
|
|
65
|
-
export async function setup(
|
|
66
|
-
const cli = global ? installGlobal() : { status: 'skipped' };
|
|
67
|
-
const
|
|
68
|
-
done({
|
|
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
|
-
|
|
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(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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) =>
|
|
36
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
},
|