@crabcloud/skills 0.1.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 +46 -0
- package/dist/crab-skills.js +207 -0
- package/dist/skills/crabcloud/SKILL.md +62 -0
- package/package.json +33 -0
- package/src/bin.ts +30 -0
- package/src/install.ts +197 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @crabcloud/skills
|
|
2
|
+
|
|
3
|
+
Crab Cloud 的 agent skills(SKILL.md)与独立安装器——让 Claude Code / Codex /
|
|
4
|
+
ZCode 等 coding agent 学会通过 `crab` CLI 操作用户的个人云。内容与
|
|
5
|
+
`@crabcloud/cli` 同源(monorepo `packages/skills` 单一事实源),独立版本发布。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @crabcloud/skills install # 自动探测本机 agent(claude/zcode/agents/codex)
|
|
11
|
+
npx @crabcloud/skills install --agent claude
|
|
12
|
+
npx @crabcloud/skills list
|
|
13
|
+
npx @crabcloud/skills uninstall
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
也可经开放生态目录安装([skills.sh](https://www.skills.sh) 收录):
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx skills add niklauslu/crabcloud-skills --skill crabcloud
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
要完整命令面(设备授权登录、whoami、tokens 管理等)请装 CLI:
|
|
23
|
+
`npx @crabcloud/cli init`(登录 + skills 安装一步到位)。
|
|
24
|
+
|
|
25
|
+
## skills 一览
|
|
26
|
+
|
|
27
|
+
| skill | 覆盖域 | 状态 |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `crabcloud` | 平台 / 账号(身份、订阅积分、Agent 令牌管理、设备授权) | 已发布 |
|
|
30
|
+
| `crabcloud-mail` 等 | 邮箱 / 云文件 / 协作项目 | 随各应用上线 |
|
|
31
|
+
|
|
32
|
+
## 安装布局
|
|
33
|
+
|
|
34
|
+
- skill 本体统一落 `~/.config/crabcloud/skills/` 稳定路径(npx 缓存目录易失,
|
|
35
|
+
长期引用必须指向稳定路径);
|
|
36
|
+
- claude / zcode / 通用 agents 目录放副本(auto 模式按目录存在性探测);
|
|
37
|
+
- codex 无原生 skills 机制,写 `~/.codex/AGENTS.md` 标记段(幂等更新、可整体卸载)。
|
|
38
|
+
|
|
39
|
+
## 安全模型
|
|
40
|
+
|
|
41
|
+
凭证只存在用户本机(`~/.config/crabcloud/credentials.json`,0600),服务端只存
|
|
42
|
+
令牌哈希、动作全程审计;scope 按需最小化授权(`crab login --scopes mail.read`)。
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/install.ts
|
|
4
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
var AGENT_TARGETS = ["claude", "zcode", "agents", "codex"];
|
|
9
|
+
var HOME = homedir();
|
|
10
|
+
var STABLE_SKILLS_DIR = join(HOME, ".config", "crabcloud", "skills");
|
|
11
|
+
var CODEX_DIR = join(HOME, ".codex");
|
|
12
|
+
var CODEX_AGENTS_PATH = join(CODEX_DIR, "AGENTS.md");
|
|
13
|
+
var CODEX_MARK_START = "<!-- @crabcloud/cli:skills -->";
|
|
14
|
+
var CODEX_MARK_END = "<!-- /@crabcloud/cli:skills -->";
|
|
15
|
+
var COPY_TARGETS = [
|
|
16
|
+
{ agent: "claude", skillsDir: join(HOME, ".claude", "skills"), detectDir: join(HOME, ".claude") },
|
|
17
|
+
{ agent: "zcode", skillsDir: join(HOME, ".zcode", "skills"), detectDir: join(HOME, ".zcode") },
|
|
18
|
+
{ agent: "agents", skillsDir: join(HOME, ".agents", "skills"), detectDir: join(HOME, ".agents") }
|
|
19
|
+
];
|
|
20
|
+
function agentSelection(rest) {
|
|
21
|
+
const index = rest.indexOf("--agent");
|
|
22
|
+
if (index < 0)
|
|
23
|
+
return { kind: "auto" };
|
|
24
|
+
const names = (rest[index + 1] ?? "all").split(",").map((s) => s.trim()).filter(Boolean);
|
|
25
|
+
const invalid = names.filter((n) => n !== "all" && !AGENT_TARGETS.includes(n));
|
|
26
|
+
if (invalid.length > 0)
|
|
27
|
+
return { kind: "invalid", invalid };
|
|
28
|
+
if (names.includes("all"))
|
|
29
|
+
return { kind: "explicit", agents: [...AGENT_TARGETS] };
|
|
30
|
+
return { kind: "explicit", agents: names };
|
|
31
|
+
}
|
|
32
|
+
function copyTargets(selection) {
|
|
33
|
+
if (selection.kind === "explicit")
|
|
34
|
+
return COPY_TARGETS.filter((t) => selection.agents.includes(t.agent));
|
|
35
|
+
return COPY_TARGETS.filter((t) => existsSync(t.detectDir));
|
|
36
|
+
}
|
|
37
|
+
function codexSelected(selection) {
|
|
38
|
+
if (selection.kind === "explicit")
|
|
39
|
+
return selection.agents.includes("codex");
|
|
40
|
+
return existsSync(CODEX_DIR);
|
|
41
|
+
}
|
|
42
|
+
function bundledSkills() {
|
|
43
|
+
try {
|
|
44
|
+
return readdirSync(fileURLToPath(new URL("./skills/", import.meta.url))).filter((name) => existsSync(join(fileURLToPath(new URL(`./skills/${name}`, import.meta.url)), "SKILL.md")));
|
|
45
|
+
} catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function stableNames() {
|
|
50
|
+
try {
|
|
51
|
+
return readdirSync(STABLE_SKILLS_DIR).filter((name) => existsSync(join(STABLE_SKILLS_DIR, name, "SKILL.md")));
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function copySkillDir(name, baseDir) {
|
|
57
|
+
mkdirSync(baseDir, { recursive: true });
|
|
58
|
+
cpSync(join(fileURLToPath(new URL("./skills/", import.meta.url)), name), join(baseDir, name), { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
function installSkills(rest) {
|
|
61
|
+
const selection = agentSelection(rest);
|
|
62
|
+
if (selection.kind === "invalid") {
|
|
63
|
+
console.error(`未知 agent:${selection.invalid.join(", ")}
|
|
64
|
+
可选:${AGENT_TARGETS.join(", ")} 或 all`);
|
|
65
|
+
return 2;
|
|
66
|
+
}
|
|
67
|
+
const names = bundledSkills();
|
|
68
|
+
if (names.length === 0) {
|
|
69
|
+
console.error("包内未找到 skills(安装可能不完整,请重装 @crabcloud/cli)。");
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
for (const name of names) {
|
|
73
|
+
copySkillDir(name, STABLE_SKILLS_DIR);
|
|
74
|
+
console.log(`✓ ${name} → ${join(STABLE_SKILLS_DIR, name)}`);
|
|
75
|
+
}
|
|
76
|
+
for (const target of copyTargets(selection)) {
|
|
77
|
+
for (const name of names) {
|
|
78
|
+
copySkillDir(name, target.skillsDir);
|
|
79
|
+
console.log(`✓ ${name} → ${join(target.skillsDir, name)}(${target.agent})`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (codexSelected(selection)) {
|
|
83
|
+
writeCodexSection(names);
|
|
84
|
+
console.log(`✓ skills 段 → ${CODEX_AGENTS_PATH}(codex)`);
|
|
85
|
+
}
|
|
86
|
+
if (selection.kind === "auto" && copyTargets(selection).length === 0 && !codexSelected(selection)) {
|
|
87
|
+
console.log(`
|
|
88
|
+
未检测到本机 agent 目录(${COPY_TARGETS.map((t) => t.detectDir).join("、")}、${CODEX_DIR})。`);
|
|
89
|
+
console.log("skills 已就位稳定路径;显式安装可运行:crab skills install --agent claude");
|
|
90
|
+
}
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
function listSkills() {
|
|
94
|
+
const known = [...new Set([...bundledSkills(), ...stableNames()])];
|
|
95
|
+
console.log(`Crab Cloud skills 安装状态:
|
|
96
|
+
`);
|
|
97
|
+
console.log(` stable ${STABLE_SKILLS_DIR}`);
|
|
98
|
+
for (const name of known) {
|
|
99
|
+
console.log(` ${existsSync(join(STABLE_SKILLS_DIR, name, "SKILL.md")) ? "✓" : "·"} ${name}`);
|
|
100
|
+
}
|
|
101
|
+
for (const target of COPY_TARGETS) {
|
|
102
|
+
const detected = existsSync(target.detectDir);
|
|
103
|
+
console.log(` ${target.agent} ${target.skillsDir}${detected ? "" : `(未检测到 ${target.detectDir})`}`);
|
|
104
|
+
for (const name of known) {
|
|
105
|
+
console.log(` ${existsSync(join(target.skillsDir, name, "SKILL.md")) ? "✓" : "·"} ${name}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
console.log(` codex ${CODEX_AGENTS_PATH}`);
|
|
109
|
+
console.log(` ${codexSectionPresent() ? "✓" : "·"} AGENTS.md 标记段`);
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
function uninstallSkills(rest) {
|
|
113
|
+
const selection = agentSelection(rest);
|
|
114
|
+
if (selection.kind === "invalid") {
|
|
115
|
+
console.error(`未知 agent:${selection.invalid.join(", ")}
|
|
116
|
+
可选:${AGENT_TARGETS.join(", ")} 或 all`);
|
|
117
|
+
return 2;
|
|
118
|
+
}
|
|
119
|
+
const names = new Set([...bundledSkills(), ...stableNames()]);
|
|
120
|
+
for (const name of names) {
|
|
121
|
+
rmSync(join(STABLE_SKILLS_DIR, name), { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
for (const target of copyTargets(selection)) {
|
|
124
|
+
for (const name of names) {
|
|
125
|
+
rmSync(join(target.skillsDir, name), { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (codexSelected(selection)) {
|
|
129
|
+
removeCodexSection();
|
|
130
|
+
}
|
|
131
|
+
console.log("✓ 已卸载 Crab Cloud skills。");
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
function codexSectionPresent() {
|
|
135
|
+
try {
|
|
136
|
+
return readFileSync(CODEX_AGENTS_PATH, "utf8").includes(CODEX_MARK_START);
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function codexSection(names) {
|
|
142
|
+
const list = names.map((name) => `- ${name}:${join(STABLE_SKILLS_DIR, name, "SKILL.md")}(意图 → crab 命令映射、--json 输出与退出码见该文件)`).join(`
|
|
143
|
+
`);
|
|
144
|
+
return [
|
|
145
|
+
CODEX_MARK_START,
|
|
146
|
+
"### Crab Cloud(crab CLI)",
|
|
147
|
+
"",
|
|
148
|
+
"已安装的 Crab Cloud skills:",
|
|
149
|
+
list,
|
|
150
|
+
"",
|
|
151
|
+
"CLI 未就绪时先运行 `npx @crabcloud/cli init`(登录 + 安装)。令牌经浏览器设备授权获得,不要向用户索要 token。",
|
|
152
|
+
CODEX_MARK_END
|
|
153
|
+
].join(`
|
|
154
|
+
`);
|
|
155
|
+
}
|
|
156
|
+
function writeCodexSection(names) {
|
|
157
|
+
const base = existsSync(CODEX_AGENTS_PATH) ? readFileSync(CODEX_AGENTS_PATH, "utf8") : "";
|
|
158
|
+
const stripped = stripCodexSection(base);
|
|
159
|
+
const separator = stripped.trim().length > 0 ? `
|
|
160
|
+
|
|
161
|
+
` : "";
|
|
162
|
+
mkdirSync(CODEX_DIR, { recursive: true });
|
|
163
|
+
writeFileSync(CODEX_AGENTS_PATH, `${stripped.replace(/\n+$/, "")}${separator}${codexSection(names)}
|
|
164
|
+
`);
|
|
165
|
+
}
|
|
166
|
+
function removeCodexSection() {
|
|
167
|
+
if (!existsSync(CODEX_AGENTS_PATH))
|
|
168
|
+
return;
|
|
169
|
+
writeFileSync(CODEX_AGENTS_PATH, stripCodexSection(readFileSync(CODEX_AGENTS_PATH, "utf8")));
|
|
170
|
+
}
|
|
171
|
+
function stripCodexSection(content) {
|
|
172
|
+
const startIndex = content.indexOf(CODEX_MARK_START);
|
|
173
|
+
if (startIndex < 0)
|
|
174
|
+
return content;
|
|
175
|
+
const endIndex = content.indexOf(CODEX_MARK_END);
|
|
176
|
+
if (endIndex < 0)
|
|
177
|
+
return content;
|
|
178
|
+
return (content.slice(0, startIndex) + content.slice(endIndex + CODEX_MARK_END.length)).replace(/^\n+/, "");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/bin.ts
|
|
182
|
+
var USAGE = `用法:crab-skills <命令> [选项]
|
|
183
|
+
|
|
184
|
+
命令:
|
|
185
|
+
install [--agent claude,zcode,agents,codex|all] 安装 skills 到本机 agent 目录
|
|
186
|
+
list 查看安装状态
|
|
187
|
+
uninstall [--agent ...] 卸载(目标逻辑与 install 对称)
|
|
188
|
+
|
|
189
|
+
缺省 --agent 时自动探测本机已装 agent。完整命令面(登录、whoami、tokens 等)
|
|
190
|
+
请装 @crabcloud/cli:npx @crabcloud/cli init`;
|
|
191
|
+
var [command, ...rest] = process.argv.slice(2);
|
|
192
|
+
switch (command) {
|
|
193
|
+
case "install":
|
|
194
|
+
process.exit(installSkills(rest));
|
|
195
|
+
case "list":
|
|
196
|
+
process.exit(listSkills());
|
|
197
|
+
case "uninstall":
|
|
198
|
+
process.exit(uninstallSkills(rest));
|
|
199
|
+
case undefined:
|
|
200
|
+
console.log(USAGE);
|
|
201
|
+
process.exit(0);
|
|
202
|
+
default:
|
|
203
|
+
console.error(`未知命令:${command}
|
|
204
|
+
|
|
205
|
+
${USAGE}`);
|
|
206
|
+
process.exit(2);
|
|
207
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: crabcloud
|
|
3
|
+
description: Manage the user's Crab Cloud personal cloud account and agent tokens through the `crab` CLI — check identity/subscription/credits, list and revoke authorized agent tokens, and re-authorize with specific scopes. Use this skill whenever the user asks about their Crab Cloud account ("我的账号信息", "who am I on crabcloud"), wants to see or clean up authorized agents/tokens ("看看我授权了哪些 agent", "revoke that old token", "撤销那个旧令牌"), or asks how to connect their coding agent to Crab Cloud — even if they never say "crab" or "Crab Cloud" explicitly. Mail, files and projects capabilities ship with their own skills (crabcloud-mail etc.) in later phases.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Crab Cloud(平台 / 账号域)
|
|
7
|
+
|
|
8
|
+
Crab Cloud 是用户的个人云底座:账号、订阅与积分是平台层,邮箱 / 云文件 / 协作项目
|
|
9
|
+
是独立应用。`crab` CLI 是 Agent 的稳定接口:凭证在用户本机
|
|
10
|
+
(`~/.config/crabcloud/credentials.json`,0600),服务端只存令牌哈希、全程审计。
|
|
11
|
+
本 skill 目前覆盖**平台与账号域**;邮箱(`crabcloud-mail`)、云文件、协作项目
|
|
12
|
+
能力随各阶段上线——用户问到这些时如实说明尚未开放,**不要猜命令**。
|
|
13
|
+
|
|
14
|
+
## 第一步:确认 CLI 可用
|
|
15
|
+
|
|
16
|
+
- 运行 `crab --version` 确认已安装。命令不存在时让用户运行
|
|
17
|
+
`npx @crabcloud/cli init`(登录 + 安装本 skill 一步完成);已装过但命令缺失
|
|
18
|
+
时运行 `npx @crabcloud/cli@latest init` 升级。
|
|
19
|
+
- `crab whoami` 报「未登录」或退出码 3 → 引导用户 `crab login`。登录走浏览器
|
|
20
|
+
设备授权(用户逐条确认 scope)——**你只发起,不代批**。
|
|
21
|
+
- 永远不要让用户把 token 粘贴进对话;凭证文件与你无关,也不要读取它。
|
|
22
|
+
|
|
23
|
+
## 意图 → 命令映射
|
|
24
|
+
|
|
25
|
+
| 用户意图 | 命令 |
|
|
26
|
+
|---|---|
|
|
27
|
+
| 看账号身份 / 订阅 / 积分 | `crab whoami` |
|
|
28
|
+
| 程序化读取身份 | `crab whoami --json` |
|
|
29
|
+
| 列出已授权的 Agent 令牌 | `crab tokens list`(脚本消费加 `--json`) |
|
|
30
|
+
| 撤销某个令牌(高危,先确认) | `crab tokens revoke <id>` |
|
|
31
|
+
| 重新授权 / 调整 scope | `crab login --scopes mail.read`(浏览器确认,你只发起) |
|
|
32
|
+
| 登出并吊销当前令牌 | `crab logout` |
|
|
33
|
+
|
|
34
|
+
拿不准令牌 id 时先 `crab tokens list` 核对;`revoke` 只接受列表里的 id。
|
|
35
|
+
|
|
36
|
+
## 机器可读输出
|
|
37
|
+
|
|
38
|
+
程序化消费时**始终加 `--json`**(`whoami --json` / `tokens list --json`):
|
|
39
|
+
|
|
40
|
+
- `whoami --json` → `{ actor, scopes, account: { username, displayName, … }, personalSpace, subscription, credits }`;
|
|
41
|
+
- `tokens list --json` → `[{ id, name, scopes, createdAt, lastUsedAt, expiresAt, revoked }]`:
|
|
42
|
+
后续 `revoke` 用 `id`,向用户转述时用 `name` + `scopes`。
|
|
43
|
+
|
|
44
|
+
## 退出码与处置
|
|
45
|
+
|
|
46
|
+
| 码 | 含义 | 你该做什么 |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| 0 | 成功 | 继续 |
|
|
49
|
+
| 2 | 用法错误 | 检查命令参数,不要原样重试 |
|
|
50
|
+
| 3 | 认证失效 | 引导用户运行 `crab login` 重新授权,不要自行重试 |
|
|
51
|
+
| 4 | scope 不足 | 如实转述缺哪个 scope,引导按需最小化重新授权 |
|
|
52
|
+
| 5 | 资源不存在 | 先 `crab tokens list` 核对 id |
|
|
53
|
+
| 7 | 配额/积分不足 | 告知用户,不要自动重试 |
|
|
54
|
+
|
|
55
|
+
## 纪律
|
|
56
|
+
|
|
57
|
+
- **撤销令牌是高危操作**:先向用户复述目标令牌的名称与 scope,得到确认后再
|
|
58
|
+
执行 `crab tokens revoke`;撤销立即生效,不可恢复(重新授权即可再建)。
|
|
59
|
+
- **scope 最小化**:建议按需授权(只读场景 `crab login --scopes account.read`),
|
|
60
|
+
不要怂恿用户一次性给全量 scope。
|
|
61
|
+
- 如实转述能力边界:邮箱 / 云文件 / 协作项目尚未上线,相关请求引导用户关注
|
|
62
|
+
后续版本,不要编造命令或输出。
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crabcloud/skills",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Crab Cloud agent skills (SKILL.md) with a standalone installer — teach coding agents to operate the user's personal cloud via the crab CLI.",
|
|
6
|
+
"bin": {
|
|
7
|
+
"crab-skills": "dist/crab-skills.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/install.ts"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"src",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "bun build ./src/bin.ts --target=node --outfile dist/crab-skills.js && rm -rf dist/skills && cp -R skills dist/skills && chmod +x dist/crab-skills.js",
|
|
26
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
27
|
+
"prepublishOnly": "bun run typecheck && bun run build"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/bun": "latest",
|
|
31
|
+
"typescript": "^5.9.3"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @crabcloud/skills 独立安装器:没有 crab CLI 时的 skills 安装通道(npm 渠道)。
|
|
3
|
+
// 与 `crab skills` 共享同一实现与目标路径;bin 层只管命令分发,退出码 0/2。
|
|
4
|
+
import { installSkills, listSkills, uninstallSkills } from "./install";
|
|
5
|
+
|
|
6
|
+
const USAGE = `用法:crab-skills <命令> [选项]
|
|
7
|
+
|
|
8
|
+
命令:
|
|
9
|
+
install [--agent claude,zcode,agents,codex|all] 安装 skills 到本机 agent 目录
|
|
10
|
+
list 查看安装状态
|
|
11
|
+
uninstall [--agent ...] 卸载(目标逻辑与 install 对称)
|
|
12
|
+
|
|
13
|
+
缺省 --agent 时自动探测本机已装 agent。完整命令面(登录、whoami、tokens 等)
|
|
14
|
+
请装 @crabcloud/cli:npx @crabcloud/cli init`;
|
|
15
|
+
|
|
16
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
17
|
+
switch (command) {
|
|
18
|
+
case "install":
|
|
19
|
+
process.exit(installSkills(rest));
|
|
20
|
+
case "list":
|
|
21
|
+
process.exit(listSkills());
|
|
22
|
+
case "uninstall":
|
|
23
|
+
process.exit(uninstallSkills(rest));
|
|
24
|
+
case undefined:
|
|
25
|
+
console.log(USAGE);
|
|
26
|
+
process.exit(0);
|
|
27
|
+
default:
|
|
28
|
+
console.error(`未知命令:${command}\n\n${USAGE}`);
|
|
29
|
+
process.exit(2);
|
|
30
|
+
}
|
package/src/install.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// skills 管理(issue #13):把随包分发的 skills/ 复制到各 coding agent 的发现目录。
|
|
2
|
+
// 本体统一落 ~/.config/crabcloud/skills/ ——npx 缓存目录易失,codex AGENTS.md 等
|
|
3
|
+
// 长期引用必须指向稳定路径;claude / zcode / 通用 agents 目录放副本,codex 无
|
|
4
|
+
// 原生 skills 机制、写 ~/.codex/AGENTS.md 标记段(幂等,可整体卸载)。
|
|
5
|
+
// 本模块是 @crabcloud/cli 与 @crabcloud/skills(独立安装器)的共享实现:
|
|
6
|
+
// 内容经 `./skills/`(与编译产物同目录,构建期拷入 dist/skills)解析,
|
|
7
|
+
// 两个包的产物布局一致。
|
|
8
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
export const AGENT_TARGETS = ["claude", "zcode", "agents", "codex"] as const;
|
|
14
|
+
export type AgentTarget = (typeof AGENT_TARGETS)[number];
|
|
15
|
+
|
|
16
|
+
const HOME = homedir();
|
|
17
|
+
const STABLE_SKILLS_DIR = join(HOME, ".config", "crabcloud", "skills");
|
|
18
|
+
const CODEX_DIR = join(HOME, ".codex");
|
|
19
|
+
const CODEX_AGENTS_PATH = join(CODEX_DIR, "AGENTS.md");
|
|
20
|
+
const CODEX_MARK_START = "<!-- @crabcloud/cli:skills -->";
|
|
21
|
+
const CODEX_MARK_END = "<!-- /@crabcloud/cli:skills -->";
|
|
22
|
+
|
|
23
|
+
// 复制型目标。agents 是通用规范目录:auto 模式仅在目录已存在时写入(不替用户发明目录)。
|
|
24
|
+
const COPY_TARGETS: { agent: AgentTarget; skillsDir: string; detectDir: string }[] = [
|
|
25
|
+
{ agent: "claude", skillsDir: join(HOME, ".claude", "skills"), detectDir: join(HOME, ".claude") },
|
|
26
|
+
{ agent: "zcode", skillsDir: join(HOME, ".zcode", "skills"), detectDir: join(HOME, ".zcode") },
|
|
27
|
+
{ agent: "agents", skillsDir: join(HOME, ".agents", "skills"), detectDir: join(HOME, ".agents") },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
type AgentSelection =
|
|
31
|
+
| { kind: "auto" }
|
|
32
|
+
| { kind: "explicit"; agents: AgentTarget[] }
|
|
33
|
+
| { kind: "invalid"; invalid: string[] };
|
|
34
|
+
|
|
35
|
+
function agentSelection(rest: string[]): AgentSelection {
|
|
36
|
+
const index = rest.indexOf("--agent");
|
|
37
|
+
if (index < 0) return { kind: "auto" };
|
|
38
|
+
const names = (rest[index + 1] ?? "all").split(",").map((s) => s.trim()).filter(Boolean);
|
|
39
|
+
const invalid = names.filter((n) => n !== "all" && !(AGENT_TARGETS as readonly string[]).includes(n));
|
|
40
|
+
if (invalid.length > 0) return { kind: "invalid", invalid };
|
|
41
|
+
if (names.includes("all")) return { kind: "explicit", agents: [...AGENT_TARGETS] };
|
|
42
|
+
return { kind: "explicit", agents: names as AgentTarget[] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function copyTargets(selection: AgentSelection) {
|
|
46
|
+
if (selection.kind === "explicit") return COPY_TARGETS.filter((t) => selection.agents.includes(t.agent));
|
|
47
|
+
return COPY_TARGETS.filter((t) => existsSync(t.detectDir));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function codexSelected(selection: AgentSelection): boolean {
|
|
51
|
+
if (selection.kind === "explicit") return selection.agents.includes("codex");
|
|
52
|
+
return existsSync(CODEX_DIR);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 产物内 dist/skills/:<skillName>/SKILL.md(后续 crabcloud-mail 等落新目录即可被发现)
|
|
56
|
+
function bundledSkills(): string[] {
|
|
57
|
+
try {
|
|
58
|
+
return readdirSync(fileURLToPath(new URL("./skills/", import.meta.url))).filter((name) =>
|
|
59
|
+
existsSync(join(fileURLToPath(new URL(`./skills/${name}`, import.meta.url)), "SKILL.md")),
|
|
60
|
+
);
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function stableNames(): string[] {
|
|
67
|
+
try {
|
|
68
|
+
return readdirSync(STABLE_SKILLS_DIR).filter((name) => existsSync(join(STABLE_SKILLS_DIR, name, "SKILL.md")));
|
|
69
|
+
} catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function copySkillDir(name: string, baseDir: string): void {
|
|
75
|
+
mkdirSync(baseDir, { recursive: true });
|
|
76
|
+
cpSync(join(fileURLToPath(new URL("./skills/", import.meta.url)), name), join(baseDir, name), { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function installSkills(rest: string[]): number {
|
|
80
|
+
const selection = agentSelection(rest);
|
|
81
|
+
if (selection.kind === "invalid") {
|
|
82
|
+
console.error(`未知 agent:${selection.invalid.join(", ")}\n可选:${AGENT_TARGETS.join(", ")} 或 all`);
|
|
83
|
+
return 2;
|
|
84
|
+
}
|
|
85
|
+
const names = bundledSkills();
|
|
86
|
+
if (names.length === 0) {
|
|
87
|
+
console.error("包内未找到 skills(安装可能不完整,请重装 @crabcloud/cli)。");
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const name of names) {
|
|
92
|
+
copySkillDir(name, STABLE_SKILLS_DIR);
|
|
93
|
+
console.log(`✓ ${name} → ${join(STABLE_SKILLS_DIR, name)}`);
|
|
94
|
+
}
|
|
95
|
+
for (const target of copyTargets(selection)) {
|
|
96
|
+
for (const name of names) {
|
|
97
|
+
copySkillDir(name, target.skillsDir);
|
|
98
|
+
console.log(`✓ ${name} → ${join(target.skillsDir, name)}(${target.agent})`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (codexSelected(selection)) {
|
|
102
|
+
writeCodexSection(names);
|
|
103
|
+
console.log(`✓ skills 段 → ${CODEX_AGENTS_PATH}(codex)`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (selection.kind === "auto" && copyTargets(selection).length === 0 && !codexSelected(selection)) {
|
|
107
|
+
console.log(`\n未检测到本机 agent 目录(${COPY_TARGETS.map((t) => t.detectDir).join("、")}、${CODEX_DIR})。`);
|
|
108
|
+
console.log("skills 已就位稳定路径;显式安装可运行:crab skills install --agent claude");
|
|
109
|
+
}
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function listSkills(): number {
|
|
114
|
+
const known = [...new Set([...bundledSkills(), ...stableNames()])];
|
|
115
|
+
console.log("Crab Cloud skills 安装状态:\n");
|
|
116
|
+
console.log(` stable ${STABLE_SKILLS_DIR}`);
|
|
117
|
+
for (const name of known) {
|
|
118
|
+
console.log(` ${existsSync(join(STABLE_SKILLS_DIR, name, "SKILL.md")) ? "✓" : "·"} ${name}`);
|
|
119
|
+
}
|
|
120
|
+
for (const target of COPY_TARGETS) {
|
|
121
|
+
const detected = existsSync(target.detectDir);
|
|
122
|
+
console.log(` ${target.agent} ${target.skillsDir}${detected ? "" : `(未检测到 ${target.detectDir})`}`);
|
|
123
|
+
for (const name of known) {
|
|
124
|
+
console.log(` ${existsSync(join(target.skillsDir, name, "SKILL.md")) ? "✓" : "·"} ${name}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
console.log(` codex ${CODEX_AGENTS_PATH}`);
|
|
128
|
+
console.log(` ${codexSectionPresent() ? "✓" : "·"} AGENTS.md 标记段`);
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function uninstallSkills(rest: string[]): number {
|
|
133
|
+
const selection = agentSelection(rest);
|
|
134
|
+
if (selection.kind === "invalid") {
|
|
135
|
+
console.error(`未知 agent:${selection.invalid.join(", ")}\n可选:${AGENT_TARGETS.join(", ")} 或 all`);
|
|
136
|
+
return 2;
|
|
137
|
+
}
|
|
138
|
+
const names = new Set([...bundledSkills(), ...stableNames()]);
|
|
139
|
+
for (const name of names) {
|
|
140
|
+
rmSync(join(STABLE_SKILLS_DIR, name), { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
for (const target of copyTargets(selection)) {
|
|
143
|
+
for (const name of names) {
|
|
144
|
+
rmSync(join(target.skillsDir, name), { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (codexSelected(selection)) {
|
|
148
|
+
removeCodexSection();
|
|
149
|
+
}
|
|
150
|
+
console.log("✓ 已卸载 Crab Cloud skills。");
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function codexSectionPresent(): boolean {
|
|
155
|
+
try {
|
|
156
|
+
return readFileSync(CODEX_AGENTS_PATH, "utf8").includes(CODEX_MARK_START);
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function codexSection(names: string[]): string {
|
|
163
|
+
const list = names
|
|
164
|
+
.map((name) => `- ${name}:${join(STABLE_SKILLS_DIR, name, "SKILL.md")}(意图 → crab 命令映射、--json 输出与退出码见该文件)`)
|
|
165
|
+
.join("\n");
|
|
166
|
+
return [
|
|
167
|
+
CODEX_MARK_START,
|
|
168
|
+
"### Crab Cloud(crab CLI)",
|
|
169
|
+
"",
|
|
170
|
+
"已安装的 Crab Cloud skills:",
|
|
171
|
+
list,
|
|
172
|
+
"",
|
|
173
|
+
"CLI 未就绪时先运行 `npx @crabcloud/cli init`(登录 + 安装)。令牌经浏览器设备授权获得,不要向用户索要 token。",
|
|
174
|
+
CODEX_MARK_END,
|
|
175
|
+
].join("\n");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function writeCodexSection(names: string[]): void {
|
|
179
|
+
const base = existsSync(CODEX_AGENTS_PATH) ? readFileSync(CODEX_AGENTS_PATH, "utf8") : "";
|
|
180
|
+
const stripped = stripCodexSection(base);
|
|
181
|
+
const separator = stripped.trim().length > 0 ? "\n\n" : "";
|
|
182
|
+
mkdirSync(CODEX_DIR, { recursive: true });
|
|
183
|
+
writeFileSync(CODEX_AGENTS_PATH, `${stripped.replace(/\n+$/, "")}${separator}${codexSection(names)}\n`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function removeCodexSection(): void {
|
|
187
|
+
if (!existsSync(CODEX_AGENTS_PATH)) return;
|
|
188
|
+
writeFileSync(CODEX_AGENTS_PATH, stripCodexSection(readFileSync(CODEX_AGENTS_PATH, "utf8")));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function stripCodexSection(content: string): string {
|
|
192
|
+
const startIndex = content.indexOf(CODEX_MARK_START);
|
|
193
|
+
if (startIndex < 0) return content;
|
|
194
|
+
const endIndex = content.indexOf(CODEX_MARK_END);
|
|
195
|
+
if (endIndex < 0) return content;
|
|
196
|
+
return (content.slice(0, startIndex) + content.slice(endIndex + CODEX_MARK_END.length)).replace(/^\n+/, "");
|
|
197
|
+
}
|