@cli-skill/cli 0.0.1-beta-d3a2d41
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/bin/cli-skill +3 -0
- package/package.json +25 -0
- package/skill/SKILL.md +106 -0
- package/skill/agents/openai.yaml +3 -0
- package/src/app.ts +28 -0
- package/src/bun.ts +51 -0
- package/src/commands/config.ts +53 -0
- package/src/commands/create.ts +25 -0
- package/src/commands/disable.ts +13 -0
- package/src/commands/docs.ts +22 -0
- package/src/commands/enable.ts +13 -0
- package/src/commands/install.ts +120 -0
- package/src/commands/list.ts +76 -0
- package/src/commands/publish.ts +24 -0
- package/src/commands/uninstall.ts +12 -0
- package/src/config.ts +150 -0
- package/src/constants.ts +21 -0
- package/src/index.ts +15 -0
- package/src/project.ts +133 -0
- package/src/registry.ts +219 -0
- package/tsconfig.json +8 -0
package/bin/cli-skill
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cli-skill/cli",
|
|
3
|
+
"version": "0.0.1-beta-d3a2d41",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"cli-skill": "./bin/cli-skill"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./src/index.ts",
|
|
13
|
+
"import": "./src/index.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.json",
|
|
18
|
+
"check": "tsc --noEmit -p tsconfig.json"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@cli-skill/core": "^0.0.1-beta-d3a2d41",
|
|
22
|
+
"cac": "^6.7.14",
|
|
23
|
+
"lodash": "^4.17.21"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cli-skill-creator
|
|
3
|
+
description: 当需要创建、接通、安装或维护 cli skill 时,使用 cli-skill CLI 完成标准流程。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# cli-skill-creator
|
|
7
|
+
|
|
8
|
+
当用户要创建一个新的 cli skill,接通本地开发中的 skill,安装一个已发布的 skill,或者维护 cli-skill 的全局配置时,使用这个 skill。
|
|
9
|
+
|
|
10
|
+
## 何时使用
|
|
11
|
+
|
|
12
|
+
- 用户说“创建一个 cli skill”
|
|
13
|
+
- 用户要把一个本地 skill 接成可直接执行的 CLI
|
|
14
|
+
- 用户要安装或卸载一个已发布的 cli skill
|
|
15
|
+
- 用户要查看或修改 `~/.cli-skill/config.json`
|
|
16
|
+
- 用户要更新某个 skill 的 `SKILL.md` 中的 Tool / Config 文档区块
|
|
17
|
+
|
|
18
|
+
## 默认流程
|
|
19
|
+
|
|
20
|
+
如果用户要新建一个 skill,默认按下面顺序执行:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cli-skill create <skill-name> --cli-name <cli-name>
|
|
24
|
+
bun install
|
|
25
|
+
cli-skill enable <skill-name>
|
|
26
|
+
cli-skill sync-skill --write
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
如果用户没有指定 `cli-name`,默认令它等于 `skill-name`。
|
|
30
|
+
|
|
31
|
+
## 命令对照
|
|
32
|
+
|
|
33
|
+
| 场景 | 命令 |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| 创建 skill | `cli-skill create <skill-name> --cli-name <cli-name> [--template <templateName>]` |
|
|
36
|
+
| 激活本地 skill | `cli-skill enable <skill-name> [--agentPath <path>]` |
|
|
37
|
+
| 取消激活本地 skill | `cli-skill disable <skill-name> [--agentPath <path>]` |
|
|
38
|
+
| 安装已发布 skill | `cli-skill install <skill-name>` |
|
|
39
|
+
| 卸载已发布 skill | `cli-skill uninstall <package-name>` |
|
|
40
|
+
| 发布本地 skill | `cli-skill publish <skill-name> [--dry-run]` |
|
|
41
|
+
| 查看 skill 列表 | `cli-skill list` |
|
|
42
|
+
| 同步 skill 文档 | 在 skill 根目录执行 `cli-skill sync-skill --write` |
|
|
43
|
+
| 读取配置 | `cli-skill config get [keyPath]` |
|
|
44
|
+
| 写入配置 | `cli-skill config set <keyPath> <value>` |
|
|
45
|
+
|
|
46
|
+
## 关键规则
|
|
47
|
+
|
|
48
|
+
- `create` 会通过 `bunx` 调 templates 包创建 skill 项目。
|
|
49
|
+
- 默认模板名是 `basic`,对应 templates 包里的内置基础模板。
|
|
50
|
+
- `create` 只创建项目,不会自动执行 `bun install`,也不会自动注册 CLI。
|
|
51
|
+
- `enable` 只做两件事:
|
|
52
|
+
- 把目标 skill 的 bin 接到 Bun 全局 bin
|
|
53
|
+
- 把目标 skill 的 `./skill` 注册到目标 skill 目录,默认是 `~/.agents/skills/<skill-name>`
|
|
54
|
+
- `enable` / `disable` 都通过 `skill-name` 到 `~/.cli-skill/skills/<skill-name>` 查找本地 skill 项目
|
|
55
|
+
- `install` / `uninstall` 面向已发布或已打包的 skill:
|
|
56
|
+
- `install` 会把 skill 安装到 `~/.cli-skill/installed`
|
|
57
|
+
- 同时把 bin 接到 Bun 全局 bin
|
|
58
|
+
- 同时把 `skill/` 注册到目标 skill 目录,默认是 `~/.agents/skills/<skill-name>`
|
|
59
|
+
- `install <skill-name>` 会先按 skill 名去 npm search API 查找:
|
|
60
|
+
- 搜索条件包含 `cli-skill` 和 `<skill-name>`
|
|
61
|
+
- 只有唯一命中时才会安装
|
|
62
|
+
- `install <skill-name> --packageName <package-name>` 会直接按显式包名安装
|
|
63
|
+
- `i` 是 `install` 的简写
|
|
64
|
+
- `publish <skill-name>` 会从 `~/.cli-skill/skills/<skill-name>` 找到本地 skill 项目,并执行 `bun publish`
|
|
65
|
+
- skill 项目默认创建在:
|
|
66
|
+
- `~/.cli-skill/skills/<skill-name>`
|
|
67
|
+
- 托管安装的 skill 默认放在:
|
|
68
|
+
- `~/.cli-skill/installed`
|
|
69
|
+
- agent 读取 skill 的目录默认是:
|
|
70
|
+
- `~/.agents/skills/<skill-name>`
|
|
71
|
+
- 包名可以带 `cli-skill-` 前缀,但 agent 使用的 skill 名不带这个前缀。
|
|
72
|
+
- 如果用户只是要修改某个已有 skill,不要重新 `create`,直接进入现有 skill 目录工作。
|
|
73
|
+
|
|
74
|
+
## 配置规则
|
|
75
|
+
|
|
76
|
+
- 全局配置文件是:
|
|
77
|
+
- `~/.cli-skill/config.json`
|
|
78
|
+
- 通过下面命令读写:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
cli-skill config get
|
|
82
|
+
cli-skill config get skillConfig.fx
|
|
83
|
+
cli-skill config set skillConfig.fx.baseUrl https://example.com
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- `get` / `set` 支持点路径,如:
|
|
87
|
+
- `skillConfig.fx.baseUrl`
|
|
88
|
+
- `skillConfig.fx.env.TEST_VALUE`
|
|
89
|
+
|
|
90
|
+
## 文档同步规则
|
|
91
|
+
|
|
92
|
+
- skill 的 `SKILL.md` 里,`Tool Reference` 和 `Config Reference` 应由平台命令生成。
|
|
93
|
+
- 当工具或配置发生变化后,优先执行:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
cli-skill sync-skill --write
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- 不要手写维护这两个生成区块,除非用户明确要求。
|
|
100
|
+
|
|
101
|
+
## 不要做的事
|
|
102
|
+
|
|
103
|
+
- 不要把 `create` 当成“已经可执行”
|
|
104
|
+
- 不要让 `enable` 隐式执行 `install`
|
|
105
|
+
- 不要手动改 `~/.agents/skills`,优先通过 `cli-skill` CLI 管理
|
|
106
|
+
- 不要把测试 skill 长期留在工作区;测试时统一使用 `test-skill-1`、`test-skill-2` 这类名字
|
package/src/app.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { cac } from "cac";
|
|
2
|
+
import { registerConfigCommand } from "./commands/config";
|
|
3
|
+
import { registerCreateCommand } from "./commands/create";
|
|
4
|
+
import { registerDisableCommand } from "./commands/disable";
|
|
5
|
+
import { registerSyncSkillCommand } from "./commands/docs";
|
|
6
|
+
import { registerEnableCommand } from "./commands/enable";
|
|
7
|
+
import { registerInstallCommand } from "./commands/install";
|
|
8
|
+
import { registerListCommand } from "./commands/list";
|
|
9
|
+
import { registerPublishCommand } from "./commands/publish";
|
|
10
|
+
import { registerUninstallCommand } from "./commands/uninstall";
|
|
11
|
+
|
|
12
|
+
export function createApp() {
|
|
13
|
+
const cli = cac("cli-skill");
|
|
14
|
+
|
|
15
|
+
registerCreateCommand(cli);
|
|
16
|
+
registerConfigCommand(cli);
|
|
17
|
+
registerSyncSkillCommand(cli);
|
|
18
|
+
registerListCommand(cli);
|
|
19
|
+
registerEnableCommand(cli);
|
|
20
|
+
registerDisableCommand(cli);
|
|
21
|
+
registerInstallCommand(cli);
|
|
22
|
+
registerUninstallCommand(cli);
|
|
23
|
+
registerPublishCommand(cli);
|
|
24
|
+
|
|
25
|
+
cli.help();
|
|
26
|
+
cli.version("0.1.0");
|
|
27
|
+
return cli;
|
|
28
|
+
}
|
package/src/bun.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
export async function runBun(
|
|
9
|
+
args: string[],
|
|
10
|
+
cwd?: string,
|
|
11
|
+
envOverrides?: Record<string, string | undefined>,
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
await execFileAsync("bun", args, {
|
|
14
|
+
cwd,
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
...envOverrides,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function runBunx(args: string[], cwd?: string): Promise<string> {
|
|
23
|
+
const { stdout } = await execFileAsync("bunx", args, {
|
|
24
|
+
cwd,
|
|
25
|
+
env: process.env,
|
|
26
|
+
});
|
|
27
|
+
return stdout.trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function runBunAndCapture(args: string[], cwd?: string): Promise<string> {
|
|
31
|
+
const { stdout } = await execFileAsync("bun", args, {
|
|
32
|
+
cwd,
|
|
33
|
+
env: process.env,
|
|
34
|
+
});
|
|
35
|
+
return stdout.trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function getBunGlobalBinDir(): Promise<string> {
|
|
39
|
+
return runBunAndCapture(["pm", "bin", "-g"]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function installPackageToDirectory(packageSpec: string, installDir: string): Promise<void> {
|
|
43
|
+
await rm(installDir, { recursive: true, force: true });
|
|
44
|
+
await mkdir(installDir, { recursive: true });
|
|
45
|
+
await writeFile(
|
|
46
|
+
path.join(installDir, "package.json"),
|
|
47
|
+
`${JSON.stringify({ name: "cli-skill-managed-install", private: true }, null, 2)}\n`,
|
|
48
|
+
"utf8",
|
|
49
|
+
);
|
|
50
|
+
await runBun(["add", packageSpec], installDir);
|
|
51
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { CAC } from "cac";
|
|
2
|
+
import {
|
|
3
|
+
getBrowserSkillConfigPath,
|
|
4
|
+
getConfigValue,
|
|
5
|
+
loadBrowserSkillCliConfig,
|
|
6
|
+
parseConfigCliValue,
|
|
7
|
+
saveBrowserSkillCliConfig,
|
|
8
|
+
setConfigValue,
|
|
9
|
+
} from "../config";
|
|
10
|
+
|
|
11
|
+
function printConfigValue(value: unknown): void {
|
|
12
|
+
if (typeof value === "string") {
|
|
13
|
+
console.log(value);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (typeof value === "undefined") {
|
|
18
|
+
console.log("undefined");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(JSON.stringify(value, null, 2));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function registerConfigCommand(cli: CAC): void {
|
|
26
|
+
cli
|
|
27
|
+
.command("config [...args]", "Manage cli-skill config")
|
|
28
|
+
.usage("config get [keyPath]\n cli-skill config set <keyPath> <value>")
|
|
29
|
+
.action(async (args: string[] = []) => {
|
|
30
|
+
const [subcommand, keyPath, rawValue] = args;
|
|
31
|
+
|
|
32
|
+
if (subcommand === "get") {
|
|
33
|
+
const currentConfig = await loadBrowserSkillCliConfig();
|
|
34
|
+
const value = getConfigValue(currentConfig, keyPath);
|
|
35
|
+
printConfigValue(value);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (subcommand === "set") {
|
|
40
|
+
if (!keyPath || typeof rawValue === "undefined") {
|
|
41
|
+
throw new Error("Usage: cli-skill config set <keyPath> <value>");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const currentConfig = await loadBrowserSkillCliConfig();
|
|
45
|
+
const nextConfig = setConfigValue(currentConfig, keyPath, parseConfigCliValue(rawValue));
|
|
46
|
+
await saveBrowserSkillCliConfig(nextConfig);
|
|
47
|
+
console.log(getBrowserSkillConfigPath());
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
throw new Error("Usage: cli-skill config get [keyPath] | cli-skill config set <keyPath> <value>");
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CAC } from "cac";
|
|
2
|
+
import { DEFAULT_TEMPLATE_NAME } from "../constants";
|
|
3
|
+
import { createSkillProject } from "../project";
|
|
4
|
+
|
|
5
|
+
function resolveTemplateName(templateOption?: string): string {
|
|
6
|
+
return templateOption ?? DEFAULT_TEMPLATE_NAME;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function registerCreateCommand(cli: CAC): void {
|
|
10
|
+
cli
|
|
11
|
+
.command("create <skillName>", "Create a cli skill project")
|
|
12
|
+
.option("--cli-name <cliName>", "Override the generated CLI name")
|
|
13
|
+
.option(
|
|
14
|
+
"--template <templateName>",
|
|
15
|
+
`Template name. Defaults to ${DEFAULT_TEMPLATE_NAME}`,
|
|
16
|
+
)
|
|
17
|
+
.action(async (skillName: string, options: { cliName?: string; template?: string }) => {
|
|
18
|
+
const targetDir = await createSkillProject(
|
|
19
|
+
skillName,
|
|
20
|
+
options.cliName ?? skillName,
|
|
21
|
+
resolveTemplateName(options.template),
|
|
22
|
+
);
|
|
23
|
+
console.log(targetDir);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CAC } from "cac";
|
|
2
|
+
import { getLocalSkillProjectDir, removeLocalSkill } from "../registry";
|
|
3
|
+
|
|
4
|
+
export function registerDisableCommand(cli: CAC): void {
|
|
5
|
+
cli
|
|
6
|
+
.command("disable <skillName>", "Disable a local skill")
|
|
7
|
+
.option("--agentPath <agentPath>", "Override the target agent skill directory")
|
|
8
|
+
.action(async (skillName: string, options: { agentPath?: string }) => {
|
|
9
|
+
const projectDir = await getLocalSkillProjectDir(skillName);
|
|
10
|
+
const targetPath = await removeLocalSkill(projectDir, { skillRoot: options.agentPath });
|
|
11
|
+
console.log(targetPath);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CAC } from "cac";
|
|
2
|
+
import { renderSkillDocsMarkdown, writeSkillDocsMarkdown } from "@cli-skill/core";
|
|
3
|
+
import { ensureValidSkillProject, loadSkillDefinition } from "../project";
|
|
4
|
+
|
|
5
|
+
export function registerSyncSkillCommand(cli: CAC): void {
|
|
6
|
+
cli
|
|
7
|
+
.command("sync-skill", "Generate Tool / Config sections for the current skill")
|
|
8
|
+
.option("--write", "Write generated sections back into skill/SKILL.md")
|
|
9
|
+
.action(async (options: { write?: boolean }) => {
|
|
10
|
+
const projectDir = process.cwd();
|
|
11
|
+
await ensureValidSkillProject(projectDir);
|
|
12
|
+
const skill = await loadSkillDefinition(projectDir);
|
|
13
|
+
|
|
14
|
+
if (options.write) {
|
|
15
|
+
const updatedPath = await writeSkillDocsMarkdown(skill);
|
|
16
|
+
console.log(updatedPath);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
console.log(renderSkillDocsMarkdown(skill));
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CAC } from "cac";
|
|
2
|
+
import { getLocalSkillProjectDir, setupLocalSkill } from "../registry";
|
|
3
|
+
|
|
4
|
+
export function registerEnableCommand(cli: CAC): void {
|
|
5
|
+
cli
|
|
6
|
+
.command("enable <skillName>", "Enable a local skill for use")
|
|
7
|
+
.option("--agentPath <agentPath>", "Override the target agent skill directory")
|
|
8
|
+
.action(async (skillName: string, options: { agentPath?: string }) => {
|
|
9
|
+
const projectDir = await getLocalSkillProjectDir(skillName);
|
|
10
|
+
const targetPath = await setupLocalSkill(projectDir, { skillRoot: options.agentPath });
|
|
11
|
+
console.log(targetPath);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { CAC } from "cac";
|
|
2
|
+
import https from "node:https";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { installPackageToDirectory } from "../bun";
|
|
5
|
+
import { getInstalledSkillsRoot } from "../constants";
|
|
6
|
+
import { installManagedSkill } from "../registry";
|
|
7
|
+
|
|
8
|
+
interface SearchResultPackage {
|
|
9
|
+
name: string;
|
|
10
|
+
keywords?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SearchResultObject {
|
|
14
|
+
package: SearchResultPackage;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface NpmSearchResponse {
|
|
18
|
+
objects?: SearchResultObject[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function fetchJson<T>(url: string): Promise<T> {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
https
|
|
24
|
+
.get(url, (response) => {
|
|
25
|
+
const statusCode = response.statusCode ?? 500;
|
|
26
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
27
|
+
reject(new Error(`Request failed with status ${statusCode}`));
|
|
28
|
+
response.resume();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let raw = "";
|
|
33
|
+
response.setEncoding("utf8");
|
|
34
|
+
response.on("data", (chunk) => {
|
|
35
|
+
raw += chunk;
|
|
36
|
+
});
|
|
37
|
+
response.on("end", () => {
|
|
38
|
+
try {
|
|
39
|
+
resolve(JSON.parse(raw) as T);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
reject(error);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
})
|
|
45
|
+
.on("error", reject);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function searchSkillPackages(skillName: string): Promise<SearchResultPackage[]> {
|
|
50
|
+
const params = new URLSearchParams({
|
|
51
|
+
text: `keywords:cli-skill ${skillName}`,
|
|
52
|
+
size: "20",
|
|
53
|
+
from: "0",
|
|
54
|
+
quality: "0.65",
|
|
55
|
+
popularity: "0.5",
|
|
56
|
+
maintenance: "0.5",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const response = await fetchJson<NpmSearchResponse>(
|
|
60
|
+
`https://registry.npmjs.org/-/v1/search?${params.toString()}`,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return (response.objects ?? [])
|
|
64
|
+
.map((entry) => entry.package)
|
|
65
|
+
.filter((pkg) => {
|
|
66
|
+
const keywords = new Set(pkg.keywords ?? []);
|
|
67
|
+
return keywords.has("cli-skill") && keywords.has(skillName);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function resolveInstallTarget(
|
|
72
|
+
skillName: string,
|
|
73
|
+
explicitPackageName?: string,
|
|
74
|
+
): Promise<{ packageSpec: string; packageName: string }> {
|
|
75
|
+
if (explicitPackageName) {
|
|
76
|
+
return { packageSpec: explicitPackageName, packageName: explicitPackageName };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const matchedPackages = await searchSkillPackages(skillName);
|
|
80
|
+
if (matchedPackages.length === 1) {
|
|
81
|
+
const [pkg] = matchedPackages;
|
|
82
|
+
return { packageSpec: pkg.name, packageName: pkg.name };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (matchedPackages.length > 1) {
|
|
86
|
+
const names = matchedPackages.map((pkg) => pkg.name).join(", ");
|
|
87
|
+
throw new Error(`Multiple skill packages matched "${skillName}": ${names}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
throw new Error(
|
|
91
|
+
`No cli skill package matched "${skillName}". Publish a package with keywords "cli-skill" and "${skillName}", or install by explicit package name via --packageName.`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function registerInstallCommand(cli: CAC): void {
|
|
96
|
+
cli
|
|
97
|
+
.command("install <skillName>", "Install a cli skill into cli-skill managed storage")
|
|
98
|
+
.alias("i")
|
|
99
|
+
.option("--packageName <packageName>", "Install directly by explicit package name")
|
|
100
|
+
.option("--package-name <packageName>", "Install directly by explicit package name")
|
|
101
|
+
.option("--skill-root <skillRoot>", "Override the target skill registration directory")
|
|
102
|
+
.action(
|
|
103
|
+
async (
|
|
104
|
+
skillName: string,
|
|
105
|
+
options: { packageName?: string; "package-name"?: string; skillRoot?: string },
|
|
106
|
+
) => {
|
|
107
|
+
const explicitPackageName = options.packageName ?? options["package-name"];
|
|
108
|
+
const { packageSpec, packageName } = await resolveInstallTarget(skillName, explicitPackageName);
|
|
109
|
+
|
|
110
|
+
const installedSkillsRoot = await getInstalledSkillsRoot();
|
|
111
|
+
const installDir = path.join(installedSkillsRoot, packageName.replaceAll("/", "__"));
|
|
112
|
+
await installPackageToDirectory(packageSpec, installDir);
|
|
113
|
+
const packageDir = path.join(installDir, "node_modules", packageName);
|
|
114
|
+
const targetPath = await installManagedSkill(packageName, packageSpec, packageDir, {
|
|
115
|
+
skillRoot: options.skillRoot,
|
|
116
|
+
});
|
|
117
|
+
console.log(targetPath);
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { CAC } from "cac";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import { listBrowserSkills } from "../registry";
|
|
4
|
+
|
|
5
|
+
function pad(value: string, width: number): string {
|
|
6
|
+
return value.padEnd(width, " ");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function formatPath(value: string): string {
|
|
10
|
+
const home = os.homedir();
|
|
11
|
+
if (value === home) {
|
|
12
|
+
return "~";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (value.startsWith(`${home}/`)) {
|
|
16
|
+
return `~/${value.slice(home.length + 1)}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function registerListCommand(cli: CAC): void {
|
|
23
|
+
cli.command("list", "List local and installed cli skills").action(async () => {
|
|
24
|
+
const skills = await listBrowserSkills();
|
|
25
|
+
if (skills.length === 0) {
|
|
26
|
+
console.log("无");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const rows = skills.map((skill) => ({
|
|
31
|
+
skillName: skill.skillName,
|
|
32
|
+
source: skill.source === "local" ? "local" : "remote",
|
|
33
|
+
packageName: skill.packageName,
|
|
34
|
+
active: skill.active ? "yes" : "no",
|
|
35
|
+
agentPaths: `[${skill.agentPaths.map((agentPath) => formatPath(agentPath)).join(", ")}]`,
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
const headers = {
|
|
39
|
+
skillName: "Skill",
|
|
40
|
+
source: "Source",
|
|
41
|
+
packageName: "Package",
|
|
42
|
+
active: "Active",
|
|
43
|
+
agentPaths: "AgentPaths",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const widths = {
|
|
47
|
+
skillName: Math.max(headers.skillName.length, ...rows.map((row) => row.skillName.length)),
|
|
48
|
+
source: Math.max(headers.source.length, ...rows.map((row) => row.source.length)),
|
|
49
|
+
packageName: Math.max(headers.packageName.length, ...rows.map((row) => row.packageName.length)),
|
|
50
|
+
active: Math.max(headers.active.length, ...rows.map((row) => row.active.length)),
|
|
51
|
+
agentPaths: Math.max(headers.agentPaths.length, ...rows.map((row) => row.agentPaths.length)),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
console.log(
|
|
55
|
+
[
|
|
56
|
+
pad(headers.skillName, widths.skillName),
|
|
57
|
+
pad(headers.source, widths.source),
|
|
58
|
+
pad(headers.packageName, widths.packageName),
|
|
59
|
+
pad(headers.active, widths.active),
|
|
60
|
+
headers.agentPaths,
|
|
61
|
+
].join(" "),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
for (const row of rows) {
|
|
65
|
+
console.log(
|
|
66
|
+
[
|
|
67
|
+
pad(row.skillName, widths.skillName),
|
|
68
|
+
pad(row.source, widths.source),
|
|
69
|
+
pad(row.packageName, widths.packageName),
|
|
70
|
+
pad(row.active, widths.active),
|
|
71
|
+
row.agentPaths,
|
|
72
|
+
].join(" "),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CAC } from "cac";
|
|
2
|
+
import { runBun } from "../bun";
|
|
3
|
+
import { getLocalSkillProjectDir } from "../registry";
|
|
4
|
+
|
|
5
|
+
export function registerPublishCommand(cli: CAC): void {
|
|
6
|
+
cli
|
|
7
|
+
.command("publish <skillName>", "Publish a local skill package")
|
|
8
|
+
.option("--dry-run", "Run publish in dry-run mode")
|
|
9
|
+
.option("--tag <tag>", "Publish under the given dist-tag")
|
|
10
|
+
.action(async (skillName: string, options: { dryRun?: boolean; tag?: string }) => {
|
|
11
|
+
const projectDir = await getLocalSkillProjectDir(skillName);
|
|
12
|
+
const args = ["publish"];
|
|
13
|
+
|
|
14
|
+
if (options.dryRun) {
|
|
15
|
+
args.push("--dry-run");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (options.tag) {
|
|
19
|
+
args.push("--tag", options.tag);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
await runBun(args, projectDir);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CAC } from "cac";
|
|
2
|
+
import { removeManagedSkill } from "../registry";
|
|
3
|
+
|
|
4
|
+
export function registerUninstallCommand(cli: CAC): void {
|
|
5
|
+
cli
|
|
6
|
+
.command("uninstall <packageName>", "Uninstall a managed cli skill")
|
|
7
|
+
.option("--skill-root <skillRoot>", "Override the target skill registration directory")
|
|
8
|
+
.action(async (packageName: string, options: { skillRoot?: string }) => {
|
|
9
|
+
const targetPath = await removeManagedSkill(packageName, { skillRoot: options.skillRoot });
|
|
10
|
+
console.log(targetPath);
|
|
11
|
+
});
|
|
12
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import get from "lodash/get";
|
|
3
|
+
import set from "lodash/set";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
export interface BrowserSkillCliConfig {
|
|
8
|
+
skillsRoot?: string;
|
|
9
|
+
installedSkillsRoot?: string;
|
|
10
|
+
agentsSkillsRoot?: string;
|
|
11
|
+
skillConfig?: Record<string, Record<string, unknown>>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveUserPath(inputPath: string): string {
|
|
15
|
+
if (inputPath === "~") {
|
|
16
|
+
return os.homedir();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (inputPath.startsWith("~/")) {
|
|
20
|
+
return path.join(os.homedir(), inputPath.slice(2));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return inputPath;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getBrowserSkillHome(): string {
|
|
27
|
+
return path.join(os.homedir(), ".cli-skill");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getBrowserSkillConfigPath(): string {
|
|
31
|
+
return path.join(getBrowserSkillHome(), "config.json");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function loadBrowserSkillCliConfig(): Promise<BrowserSkillCliConfig> {
|
|
35
|
+
try {
|
|
36
|
+
const raw = await readFile(getBrowserSkillConfigPath(), "utf8");
|
|
37
|
+
return (JSON.parse(raw) as BrowserSkillCliConfig) ?? {};
|
|
38
|
+
} catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getDefaultBrowserSkillCliConfig(): Required<
|
|
44
|
+
Pick<BrowserSkillCliConfig, "skillsRoot" | "installedSkillsRoot" | "agentsSkillsRoot" | "skillConfig">
|
|
45
|
+
> {
|
|
46
|
+
return {
|
|
47
|
+
skillsRoot: path.join(getBrowserSkillHome(), "skills"),
|
|
48
|
+
installedSkillsRoot: path.join(getBrowserSkillHome(), "installed"),
|
|
49
|
+
agentsSkillsRoot: path.join(os.homedir(), ".agents", "skills"),
|
|
50
|
+
skillConfig: {},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function getResolvedBrowserSkillCliConfig(): Promise<
|
|
55
|
+
Required<Pick<BrowserSkillCliConfig, "skillsRoot" | "installedSkillsRoot" | "agentsSkillsRoot">> &
|
|
56
|
+
BrowserSkillCliConfig
|
|
57
|
+
> {
|
|
58
|
+
const config = await loadBrowserSkillCliConfig();
|
|
59
|
+
const defaults = getDefaultBrowserSkillCliConfig();
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
...config,
|
|
63
|
+
skillConfig: config.skillConfig ?? defaults.skillConfig,
|
|
64
|
+
skillsRoot: resolveUserPath(config.skillsRoot ?? defaults.skillsRoot),
|
|
65
|
+
installedSkillsRoot: resolveUserPath(config.installedSkillsRoot ?? defaults.installedSkillsRoot),
|
|
66
|
+
agentsSkillsRoot: resolveUserPath(config.agentsSkillsRoot ?? defaults.agentsSkillsRoot),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function ensureBrowserSkillCliConfig(): Promise<string> {
|
|
71
|
+
const configPath = getBrowserSkillConfigPath();
|
|
72
|
+
const defaults = getDefaultBrowserSkillCliConfig();
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const raw = await readFile(configPath, "utf8");
|
|
76
|
+
const parsed = (JSON.parse(raw) as BrowserSkillCliConfig) ?? {};
|
|
77
|
+
const nextConfig: BrowserSkillCliConfig = {
|
|
78
|
+
...parsed,
|
|
79
|
+
skillsRoot: parsed.skillsRoot ?? defaults.skillsRoot,
|
|
80
|
+
installedSkillsRoot: parsed.installedSkillsRoot ?? defaults.installedSkillsRoot,
|
|
81
|
+
agentsSkillsRoot: parsed.agentsSkillsRoot ?? defaults.agentsSkillsRoot,
|
|
82
|
+
skillConfig: parsed.skillConfig ?? defaults.skillConfig,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (JSON.stringify(parsed) !== JSON.stringify(nextConfig)) {
|
|
86
|
+
await saveBrowserSkillCliConfig(nextConfig);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return configPath;
|
|
90
|
+
} catch {
|
|
91
|
+
await saveBrowserSkillCliConfig(defaults);
|
|
92
|
+
return configPath;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function saveBrowserSkillCliConfig(config: BrowserSkillCliConfig): Promise<void> {
|
|
97
|
+
const configPath = getBrowserSkillConfigPath();
|
|
98
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
99
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getConfigValue(config: BrowserSkillCliConfig, keyPath?: string): unknown {
|
|
103
|
+
if (!keyPath) {
|
|
104
|
+
return config;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return get(config, keyPath);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function setConfigValue(
|
|
111
|
+
config: BrowserSkillCliConfig,
|
|
112
|
+
keyPath: string,
|
|
113
|
+
value: unknown,
|
|
114
|
+
): BrowserSkillCliConfig {
|
|
115
|
+
const nextConfig = structuredClone(config);
|
|
116
|
+
set(nextConfig as object, keyPath, value);
|
|
117
|
+
return nextConfig;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function parseConfigCliValue(rawValue: string): unknown {
|
|
121
|
+
if (rawValue === "true") {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (rawValue === "false") {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (rawValue === "null") {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (/^-?\d+(\.\d+)?$/.test(rawValue)) {
|
|
134
|
+
return Number(rawValue);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
(rawValue.startsWith("{") && rawValue.endsWith("}")) ||
|
|
139
|
+
(rawValue.startsWith("[") && rawValue.endsWith("]")) ||
|
|
140
|
+
(rawValue.startsWith("\"") && rawValue.endsWith("\""))
|
|
141
|
+
) {
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(rawValue);
|
|
144
|
+
} catch {
|
|
145
|
+
return rawValue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return rawValue;
|
|
150
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { getResolvedBrowserSkillCliConfig } from "./config";
|
|
3
|
+
|
|
4
|
+
export const LOCAL_CORE_PACKAGE_PATH = path.resolve(import.meta.dirname, "../../core");
|
|
5
|
+
export const LOCAL_TEMPLATE_PACKAGE_PATH = path.resolve(import.meta.dirname, "../../templates");
|
|
6
|
+
export const DEFAULT_TEMPLATE_NAME = "basic";
|
|
7
|
+
|
|
8
|
+
export async function getDefaultSkillsRoot(): Promise<string> {
|
|
9
|
+
const config = await getResolvedBrowserSkillCliConfig();
|
|
10
|
+
return config.skillsRoot;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function getInstalledSkillsRoot(): Promise<string> {
|
|
14
|
+
const config = await getResolvedBrowserSkillCliConfig();
|
|
15
|
+
return config.installedSkillsRoot;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function getAgentsSkillsRoot(): Promise<string> {
|
|
19
|
+
const config = await getResolvedBrowserSkillCliConfig();
|
|
20
|
+
return config.agentsSkillsRoot;
|
|
21
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createApp } from "./app";
|
|
2
|
+
import { ensureBrowserSkillCliConfig } from "./config";
|
|
3
|
+
export { createSkillProject } from "./project";
|
|
4
|
+
|
|
5
|
+
async function main(argv = process.argv.slice(2)): Promise<void> {
|
|
6
|
+
await ensureBrowserSkillCliConfig();
|
|
7
|
+
const cli = createApp();
|
|
8
|
+
cli.parse(["node", "cli-skill", ...argv], { run: false });
|
|
9
|
+
await cli.runMatchedCommand();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
main().catch((error) => {
|
|
13
|
+
console.error(error);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
});
|
package/src/project.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import type { SkillDefinition } from "@cli-skill/core";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_TEMPLATE_NAME,
|
|
7
|
+
LOCAL_CORE_PACKAGE_PATH,
|
|
8
|
+
LOCAL_TEMPLATE_PACKAGE_PATH,
|
|
9
|
+
getDefaultSkillsRoot,
|
|
10
|
+
} from "./constants";
|
|
11
|
+
import { runBunx } from "./bun";
|
|
12
|
+
|
|
13
|
+
export interface SkillPackageJson {
|
|
14
|
+
name?: string;
|
|
15
|
+
bin?: string | Record<string, string>;
|
|
16
|
+
cliSkill?: boolean | Record<string, unknown>;
|
|
17
|
+
version?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function getLocalCorePackageVersion(): Promise<string> {
|
|
21
|
+
const corePackageJsonPath = path.join(LOCAL_CORE_PACKAGE_PATH, "package.json");
|
|
22
|
+
const corePackageJson = JSON.parse(
|
|
23
|
+
await readFile(corePackageJsonPath, "utf8"),
|
|
24
|
+
) as SkillPackageJson;
|
|
25
|
+
|
|
26
|
+
if (typeof corePackageJson.version !== "string" || corePackageJson.version.length === 0) {
|
|
27
|
+
throw new Error(`Missing version in ${corePackageJsonPath}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return corePackageJson.version;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function createSkillProject(
|
|
34
|
+
skillName: string,
|
|
35
|
+
cliName = skillName,
|
|
36
|
+
templateName = DEFAULT_TEMPLATE_NAME,
|
|
37
|
+
targetRoot?: string,
|
|
38
|
+
): Promise<string> {
|
|
39
|
+
const resolvedTargetRoot = targetRoot ?? (await getDefaultSkillsRoot());
|
|
40
|
+
const targetDir = path.join(resolvedTargetRoot, skillName);
|
|
41
|
+
const corePackageVersion = await getLocalCorePackageVersion();
|
|
42
|
+
await runBunx(
|
|
43
|
+
[
|
|
44
|
+
"--bun",
|
|
45
|
+
"--package",
|
|
46
|
+
`file:${LOCAL_TEMPLATE_PACKAGE_PATH}`,
|
|
47
|
+
"cli-skill-create-template",
|
|
48
|
+
"--template",
|
|
49
|
+
templateName,
|
|
50
|
+
"--skill-name",
|
|
51
|
+
skillName,
|
|
52
|
+
"--cli-name",
|
|
53
|
+
cliName,
|
|
54
|
+
"--target-dir",
|
|
55
|
+
targetDir,
|
|
56
|
+
"--core-package-version",
|
|
57
|
+
corePackageVersion,
|
|
58
|
+
],
|
|
59
|
+
process.cwd(),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return targetDir;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function loadSkillPackageJson(projectDir: string): Promise<SkillPackageJson> {
|
|
66
|
+
const packageJsonPath = path.join(projectDir, "package.json");
|
|
67
|
+
return JSON.parse(await readFile(packageJsonPath, "utf8")) as SkillPackageJson;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getSkillNameFromPackageName(packageName: string): string {
|
|
71
|
+
const rawName = packageName.split("/").at(-1) ?? packageName;
|
|
72
|
+
return rawName.startsWith("cli-skill-")
|
|
73
|
+
? rawName.slice("cli-skill-".length)
|
|
74
|
+
: rawName;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function ensureValidSkillProject(
|
|
78
|
+
projectDir: string,
|
|
79
|
+
): Promise<{ packageName: string; skillName: string; bins: Record<string, string> }> {
|
|
80
|
+
const packageJson = await loadSkillPackageJson(projectDir);
|
|
81
|
+
if (!packageJson.cliSkill) {
|
|
82
|
+
throw new Error(`Missing cliSkill field in ${path.join(projectDir, "package.json")}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (typeof packageJson.name !== "string" || packageJson.name.length === 0) {
|
|
86
|
+
throw new Error(`Missing package name in ${path.join(projectDir, "package.json")}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const skillFilePath = path.join(projectDir, "skill", "SKILL.md");
|
|
90
|
+
await readFile(skillFilePath, "utf8");
|
|
91
|
+
|
|
92
|
+
const bins =
|
|
93
|
+
typeof packageJson.bin === "string"
|
|
94
|
+
? { [getSkillNameFromPackageName(packageJson.name)]: packageJson.bin }
|
|
95
|
+
: packageJson.bin && typeof packageJson.bin === "object"
|
|
96
|
+
? packageJson.bin
|
|
97
|
+
: null;
|
|
98
|
+
|
|
99
|
+
if (!bins || Object.keys(bins).length === 0) {
|
|
100
|
+
throw new Error(`Missing bin field in ${path.join(projectDir, "package.json")}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
packageName: packageJson.name,
|
|
105
|
+
skillName: getSkillNameFromPackageName(packageJson.name),
|
|
106
|
+
bins,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function loadSkillDefinition(projectDir: string): Promise<SkillDefinition> {
|
|
111
|
+
await ensureValidSkillProject(projectDir);
|
|
112
|
+
const entryPath = path.join(projectDir, "src", "index.ts");
|
|
113
|
+
const previousCwd = process.cwd();
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
process.chdir(projectDir);
|
|
117
|
+
const imported = await import(pathToFileURL(entryPath).href);
|
|
118
|
+
const skill = (imported.default ?? imported.skill) as SkillDefinition | undefined;
|
|
119
|
+
|
|
120
|
+
if (!skill) {
|
|
121
|
+
throw new Error(`Cannot find exported skill definition in ${entryPath}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return skill.rootDir
|
|
125
|
+
? skill
|
|
126
|
+
: {
|
|
127
|
+
...skill,
|
|
128
|
+
rootDir: projectDir,
|
|
129
|
+
};
|
|
130
|
+
} finally {
|
|
131
|
+
process.chdir(previousCwd);
|
|
132
|
+
}
|
|
133
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getAgentsSkillsRoot, getDefaultSkillsRoot, getInstalledSkillsRoot } from "./constants";
|
|
4
|
+
import { ensureValidSkillProject, getSkillNameFromPackageName, type SkillPackageJson } from "./project";
|
|
5
|
+
import { getBunGlobalBinDir } from "./bun";
|
|
6
|
+
|
|
7
|
+
interface SetupOptions {
|
|
8
|
+
skillRoot?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface RegisteredSkillInfo {
|
|
12
|
+
skillName: string;
|
|
13
|
+
packageName: string;
|
|
14
|
+
sourcePath: string;
|
|
15
|
+
skillPath: string;
|
|
16
|
+
binNames: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface InstalledSkillMetadata {
|
|
20
|
+
packageName: string;
|
|
21
|
+
packageSpec: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function installedPackageDirName(packageName: string): string {
|
|
25
|
+
return packageName.replaceAll("/", "__");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function ensureSymlink(sourcePath: string, targetPath: string, type: "file" | "dir"): Promise<void> {
|
|
29
|
+
await rm(targetPath, { recursive: true, force: true });
|
|
30
|
+
await symlink(sourcePath, targetPath, type);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function loadPackageJson(packageDir: string): Promise<SkillPackageJson> {
|
|
34
|
+
return JSON.parse(await readFile(path.join(packageDir, "package.json"), "utf8")) as SkillPackageJson;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveBinEntries(
|
|
38
|
+
packageName: string,
|
|
39
|
+
bin: SkillPackageJson["bin"],
|
|
40
|
+
): Record<string, string> {
|
|
41
|
+
if (typeof bin === "string") {
|
|
42
|
+
return { [getSkillNameFromPackageName(packageName)]: bin };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (bin && typeof bin === "object") {
|
|
46
|
+
return bin;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function setupBins(sourceDir: string, bins: Record<string, string>): Promise<string[]> {
|
|
53
|
+
const globalBinDir = await getBunGlobalBinDir();
|
|
54
|
+
await mkdir(globalBinDir, { recursive: true });
|
|
55
|
+
|
|
56
|
+
const installedBinNames: string[] = [];
|
|
57
|
+
|
|
58
|
+
for (const [binName, relativeBinPath] of Object.entries(bins)) {
|
|
59
|
+
const sourcePath = path.resolve(sourceDir, relativeBinPath);
|
|
60
|
+
const targetPath = path.join(globalBinDir, binName);
|
|
61
|
+
await ensureSymlink(sourcePath, targetPath, "file");
|
|
62
|
+
installedBinNames.push(binName);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return installedBinNames;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function removeBins(binNames: string[]): Promise<void> {
|
|
69
|
+
const globalBinDir = await getBunGlobalBinDir();
|
|
70
|
+
for (const binName of binNames) {
|
|
71
|
+
await rm(path.join(globalBinDir, binName), { force: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function setupLocalSkill(projectDir: string, options: SetupOptions = {}): Promise<string> {
|
|
76
|
+
const { skillName, bins } = await ensureValidSkillProject(projectDir);
|
|
77
|
+
const agentsSkillsRoot = options.skillRoot ?? (await getAgentsSkillsRoot());
|
|
78
|
+
const targetPath = path.join(agentsSkillsRoot, skillName);
|
|
79
|
+
|
|
80
|
+
await mkdir(agentsSkillsRoot, { recursive: true });
|
|
81
|
+
await setupBins(projectDir, bins);
|
|
82
|
+
await ensureSymlink(path.join(projectDir, "skill"), targetPath, "dir");
|
|
83
|
+
|
|
84
|
+
return targetPath;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function removeLocalSkill(projectDir: string, options: SetupOptions = {}): Promise<string> {
|
|
88
|
+
const { skillName, bins } = await ensureValidSkillProject(projectDir);
|
|
89
|
+
const agentsSkillsRoot = options.skillRoot ?? (await getAgentsSkillsRoot());
|
|
90
|
+
const targetPath = path.join(agentsSkillsRoot, skillName);
|
|
91
|
+
|
|
92
|
+
await removeBins(Object.keys(bins));
|
|
93
|
+
await rm(targetPath, { recursive: true, force: true });
|
|
94
|
+
|
|
95
|
+
return targetPath;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function installManagedSkill(
|
|
99
|
+
packageName: string,
|
|
100
|
+
packageSpec: string,
|
|
101
|
+
packageDir: string,
|
|
102
|
+
options: SetupOptions = {},
|
|
103
|
+
): Promise<string> {
|
|
104
|
+
const packageJson = await loadPackageJson(packageDir);
|
|
105
|
+
if (!packageJson.cliSkill) {
|
|
106
|
+
throw new Error(`Missing cliSkill field in ${path.join(packageDir, "package.json")}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const skillName = getSkillNameFromPackageName(packageName);
|
|
110
|
+
const bins = resolveBinEntries(packageName, packageJson.bin);
|
|
111
|
+
const agentsSkillsRoot = options.skillRoot ?? (await getAgentsSkillsRoot());
|
|
112
|
+
const targetPath = path.join(agentsSkillsRoot, skillName);
|
|
113
|
+
const installedSkillsRoot = await getInstalledSkillsRoot();
|
|
114
|
+
const metadataPath = path.join(installedSkillsRoot, installedPackageDirName(packageName), ".cli-skill.json");
|
|
115
|
+
|
|
116
|
+
await mkdir(path.dirname(metadataPath), { recursive: true });
|
|
117
|
+
await writeFile(metadataPath, `${JSON.stringify({ packageName, packageSpec }, null, 2)}\n`, "utf8");
|
|
118
|
+
await mkdir(agentsSkillsRoot, { recursive: true });
|
|
119
|
+
await setupBins(packageDir, bins);
|
|
120
|
+
await ensureSymlink(path.join(packageDir, "skill"), targetPath, "dir");
|
|
121
|
+
|
|
122
|
+
return targetPath;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function removeManagedSkill(packageName: string, options: SetupOptions = {}): Promise<string> {
|
|
126
|
+
const installedSkillsRoot = await getInstalledSkillsRoot();
|
|
127
|
+
const installDir = path.join(installedSkillsRoot, installedPackageDirName(packageName));
|
|
128
|
+
const packageDir = path.join(installDir, "node_modules", packageName);
|
|
129
|
+
const packageJson = await loadPackageJson(packageDir);
|
|
130
|
+
const bins = resolveBinEntries(packageName, packageJson.bin);
|
|
131
|
+
const skillName = getSkillNameFromPackageName(packageName);
|
|
132
|
+
const agentsSkillsRoot = options.skillRoot ?? (await getAgentsSkillsRoot());
|
|
133
|
+
const targetPath = path.join(agentsSkillsRoot, skillName);
|
|
134
|
+
|
|
135
|
+
await removeBins(Object.keys(bins));
|
|
136
|
+
await rm(targetPath, { recursive: true, force: true });
|
|
137
|
+
await rm(installDir, { recursive: true, force: true });
|
|
138
|
+
|
|
139
|
+
return targetPath;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function getRegisteredAgentSkillNames(skillRoot?: string): Promise<Set<string>> {
|
|
143
|
+
const agentsSkillsRoot = skillRoot ?? (await getAgentsSkillsRoot());
|
|
144
|
+
try {
|
|
145
|
+
const names = await readdir(agentsSkillsRoot);
|
|
146
|
+
return new Set(names);
|
|
147
|
+
} catch {
|
|
148
|
+
return new Set();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function listBrowserSkills(): Promise<
|
|
153
|
+
Array<{
|
|
154
|
+
source: "local" | "remote";
|
|
155
|
+
skillName: string;
|
|
156
|
+
packageName: string;
|
|
157
|
+
projectPath: string;
|
|
158
|
+
active: boolean;
|
|
159
|
+
agentPaths: string[];
|
|
160
|
+
}>
|
|
161
|
+
> {
|
|
162
|
+
const agentsSkillsRoot = await getAgentsSkillsRoot();
|
|
163
|
+
const activeSkills = await getRegisteredAgentSkillNames(agentsSkillsRoot);
|
|
164
|
+
const results: Array<{
|
|
165
|
+
source: "local" | "remote";
|
|
166
|
+
skillName: string;
|
|
167
|
+
packageName: string;
|
|
168
|
+
projectPath: string;
|
|
169
|
+
active: boolean;
|
|
170
|
+
agentPaths: string[];
|
|
171
|
+
}> = [];
|
|
172
|
+
|
|
173
|
+
const skillsRoot = await getDefaultSkillsRoot();
|
|
174
|
+
try {
|
|
175
|
+
for (const entry of await readdir(skillsRoot, { withFileTypes: true })) {
|
|
176
|
+
if (!entry.isDirectory()) continue;
|
|
177
|
+
const projectDir = path.join(skillsRoot, entry.name);
|
|
178
|
+
try {
|
|
179
|
+
const { skillName, packageName } = await ensureValidSkillProject(projectDir);
|
|
180
|
+
results.push({
|
|
181
|
+
source: "local",
|
|
182
|
+
skillName,
|
|
183
|
+
packageName,
|
|
184
|
+
projectPath: projectDir,
|
|
185
|
+
active: activeSkills.has(skillName),
|
|
186
|
+
agentPaths: activeSkills.has(skillName) ? [path.join(agentsSkillsRoot, skillName)] : [],
|
|
187
|
+
});
|
|
188
|
+
} catch {}
|
|
189
|
+
}
|
|
190
|
+
} catch {}
|
|
191
|
+
|
|
192
|
+
const installedSkillsRoot = await getInstalledSkillsRoot();
|
|
193
|
+
try {
|
|
194
|
+
for (const entry of await readdir(installedSkillsRoot, { withFileTypes: true })) {
|
|
195
|
+
if (!entry.isDirectory()) continue;
|
|
196
|
+
const installDir = path.join(installedSkillsRoot, entry.name);
|
|
197
|
+
const metadataPath = path.join(installDir, ".cli-skill.json");
|
|
198
|
+
try {
|
|
199
|
+
const metadata = JSON.parse(await readFile(metadataPath, "utf8")) as InstalledSkillMetadata;
|
|
200
|
+
const skillName = getSkillNameFromPackageName(metadata.packageName);
|
|
201
|
+
results.push({
|
|
202
|
+
source: "remote",
|
|
203
|
+
skillName,
|
|
204
|
+
packageName: metadata.packageName,
|
|
205
|
+
projectPath: path.join(installDir, "node_modules", metadata.packageName),
|
|
206
|
+
active: activeSkills.has(skillName),
|
|
207
|
+
agentPaths: activeSkills.has(skillName) ? [path.join(agentsSkillsRoot, skillName)] : [],
|
|
208
|
+
});
|
|
209
|
+
} catch {}
|
|
210
|
+
}
|
|
211
|
+
} catch {}
|
|
212
|
+
|
|
213
|
+
return results.sort((a, b) => a.source.localeCompare(b.source) || a.skillName.localeCompare(b.skillName));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function getLocalSkillProjectDir(skillName: string): Promise<string> {
|
|
217
|
+
const skillsRoot = await getDefaultSkillsRoot();
|
|
218
|
+
return path.join(skillsRoot, skillName);
|
|
219
|
+
}
|