@liuovo/skills 0.0.0-stage → 0.1.1

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 CHANGED
@@ -1,3 +1,173 @@
1
- # Temporary Holding Version
1
+ # @liuovo/skills
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 自己的 Skills,加上一份精选的外部引用清单。用一个向导选择本次要安装的内容,真正的安装交给固定版本的 [`skills`](https://github.com/vercel-labs/skills)。
4
+
5
+ **当前状态:本地可开发、构建和打包,尚未发布 npm。** `@liuovo/skills` 是拟用包名,未核实 npm 所有权和可用性。
6
+
7
+ ## 本地使用
8
+
9
+ 需要 Node.js >= 22.20.0、pnpm,以及安装远程来源时可用的 Git / GitHub 网络。
10
+
11
+ ```bash
12
+ pnpm install
13
+
14
+ # 无参数:搜索多选 Skills → 项目/全局 → Agent → 确认
15
+ pnpm dev
16
+
17
+ # 查看全部引用,不下载上游内容
18
+ pnpm dev list
19
+
20
+ # 只生成安装计划,不下载、不写入
21
+ pnpm dev --skill handoff grilling --project --agent codex --dry-run
22
+
23
+ # 参数只跳过对应问题:这里仍会询问 Skills 和 Agent
24
+ pnpm dev --global
25
+
26
+ # 明确的非交互安装
27
+ pnpm dev --skill handoff --global --agent codex -y
28
+ ```
29
+
30
+ 项目级安装以**启动命令时的工作目录**为准。在本仓库运行 `pnpm dev --project` 就是安装到本仓库。构建后,可以在另一个项目中调用 CLI 文件的绝对路径:
31
+
32
+ ```bash
33
+ pnpm build
34
+ cd /path/to/target-project
35
+ node /path/to/this-repository/dist/cli.js
36
+ ```
37
+
38
+ 发布后,用户入口才是:
39
+
40
+ ```bash
41
+ pnpx @liuovo/skills
42
+ ```
43
+
44
+ `-y` 只跳过交互与确认,不代表全选,也不允许覆盖。无终端、CI、`--json` 或 `-y` 模式必须明确 Skills、范围和 Agent。安装还需要 `-y` 确认;预览不需要。
45
+
46
+ ### 中文分类选择
47
+
48
+ 仅 Skill 的一句话描述和分类名称使用中文;向导问题、键盘操作提示、帮助、确认、安装结果和错误信息使用英文。Skill 名称、命令参数和 JSON 状态值保留原名,便于复制命令及脚本调用。
49
+
50
+ | 分类 | 内容 |
51
+ | -------------- | -------------------------------------- |
52
+ | 方案推敲 | 通过追问和质询完善方案、检验决策 |
53
+ | 协作与表达 | 对话交接、Agent 文档、减少阅读干扰 |
54
+ | 代码设计与重构 | 模块设计、代码改进、重复代码治理 |
55
+ | 前端设计 | 界面视觉与交互设计 |
56
+ | Vue 开发 | Vue、路由、Pinia、测试、调试与组合函数 |
57
+
58
+ 选择界面显示分类标题和每项的一句话中文用途。直接输入名称、中文用途、分类或仓库名即可搜索;多个关键词以空格分隔。`↑/↓` 移动,`Tab` 勾选或取消,`Enter` 确认,`Esc` 退出。切换搜索词会保留之前勾选的项目,可以跨分类选择。
59
+
60
+ ## 安装规则
61
+
62
+ - 不传 `--skill` 时搜索多选;`--all` 才选择本清单全部条目,不会安装上游仓库全部 Skills。
63
+ - `--project` 和 `--global` 互斥;缺省时询问,不猜测全局范围。
64
+ - 第一版只适配并验证 `codex`、`claude-code`、`opencode`。其他 Agent 不会直接透传给底层。
65
+ - 默认跳过所有已有同名路径,包括手写目录、文件和失效符号链接。锁文件中的来源只用于展示,不被当作“可安全覆盖”的证明。
66
+ - `--overwrite` 明确允许替换已有目标目录及其中的用户修改;可先与 `--dry-run` 组合查看。已有目录的内容不会自动备份。
67
+ - 父目录含符号链接、锁文件损坏或版本不兼容时拒绝安装。即使传 `--overwrite` 也不会绕过这些检查。
68
+ - 已有旧位置的同名 Codex / OpenCode Skill 时,要求手动核对;本工具不会帮你迁移、删除它们。
69
+ - 未勾选不等于卸载;移除清单引用也不会清理机器上的已有安装。
70
+ - 安装按来源串行执行,一项失败会明确报告;已成功的安装不会因其他来源失败被删除。
71
+ - Ctrl+C 停止当前安装并不再启动后续来源。安装不是事务,超时或中断后可能留下部分文件,需要查看结果后重试。
72
+
73
+ 安装前检查会在确认后、每个来源启动前再次执行,但不能与其他独立进程的写入形成原子事务。**不要同时运行多个安装/更新命令操作同一个目标目录。**
74
+
75
+ ### Agent 目录
76
+
77
+ 固定的 `skills@1.7.2` 使用以下布局:
78
+
79
+ | Agent | 项目级 | 全局 |
80
+ | ----------- | -------------------------------------- | ---------------------------------------- |
81
+ | Codex | `.agents/skills/<name>` | `~/.agents/skills/<name>` |
82
+ | OpenCode | 与 Codex 共用 | 与 Codex 共用 |
83
+ | Claude Code | `.claude/skills/<name>` 链接到共享目录 | `~/.claude/skills/<name>` 链接到共享目录 |
84
+
85
+ 全局 Claude Code 遵循 `CLAUDE_CONFIG_DIR`。`CODEX_HOME`、`XDG_CONFIG_HOME` 用于检查旧位置冲突;它们不会改变此版本安装器的共享目录。
86
+
87
+ 选择 Agent **不代表对其他 Agent 隔离**;其他会扫描共享目录的 Agent 也可能发现这些 Skills。第一版固定使用安装器默认的规范目录加符号链接模式,不提供持久依赖 `pnpx` 临时目录的本地链接。
88
+
89
+ 项目锁文件由底层维护为 `skills-lock.json`;全局锁默认是 `~/.agents/.skill-lock.json`,设置 `XDG_STATE_HOME` 时位于其 `skills/` 子目录。封装不维护第二套锁文件,不自动恢复或重写损坏的旧锁。
90
+
91
+ ## 维护清单
92
+
93
+ 外部引用的唯一列表是 `package.json` 的 `skills` 字段,格式借鉴 [`antfu/skills-pack`](https://github.com/antfu/skills-pack):
94
+
95
+ ```json
96
+ {
97
+ "skills": [
98
+ {
99
+ "source": "mattpocock/skills",
100
+ "skills": ["handoff", "grilling"]
101
+ }
102
+ ]
103
+ }
104
+ ```
105
+
106
+ `skillsPack.metadata["owner/repo@skill-name"]` 保存一句话中文 `description`、分类 `category` 和上游目录 `path`。`skillsPack.categories` 控制分类顺序;新增分类时也要加入此数组。没有分类的条目归入“其他”,自有内容归入“自有 Skills”。**路径必须指向上游,中文说明只是本地展示元数据,不改写上游 Skill 正文。**
107
+
108
+ 当前有 15 个引用,来自 Matt Pocock、shadcn、kucherenko、ayghri、vuejs-ai 和 Anthropic。用 `pnpm dev list` 查看完整目录,或 `pnpm dev list --json` 获取名称、来源、说明和链接。
109
+
110
+ 第一版的来源支持 GitHub `owner/repo`,按上游默认分支安装;尚不支持清单级固定 ref、嵌套技能包或预设组合。外部内容和许可归各上游,本项目的引用不重新许可这些内容。
111
+
112
+ ### 添加自己的 Skill
113
+
114
+ 将自己的完整 Skill 目录放进 `skills/`,例如 `skills/my-workflow/SKILL.md`。CLI 递归发现其中带合法 `name`、`description` 的 YAML frontmatter;脚本和参考文件可以放在旁边。
115
+
116
+ 本仓库目前没有自有 Skill 正文,也没有示例 Skill 混入安装列表。
117
+
118
+ 自有项的安装来源由 `skillsPack.ownSource` 指定,当前是 `liuc-c/skills`。**CLI 读取本地/npm 包中的自有目录来展示,但实际安装仍从这个 GitHub 仓库获取;未推送的自有内容不会被安装。** 先推送自有内容、再发布目录包,确保目录与上游一致。开发中的未推送 Skill 可用底层 `skills add` 的本地路径模式单独测试。
119
+
120
+ 同名不同来源可以在目录中共存,但选择时必须用 `owner/repo@name` 消歧;同一次安装不能选择两个同名项,因为底层目标目录按 Skill 名称命名。
121
+
122
+ `pnpx skills add liuc-c/skills` 只发现仓库中的实际自有 Skills,不是本包混合引用目录的入口。
123
+
124
+ ## 更新、验证与发布
125
+
126
+ 包版本、引用清单版本和上游内容版本是不同的东西:
127
+
128
+ - 更新 `@liuovo/skills` 获得新的目录和 CLI;不会自动更新机器上的 Skills。
129
+ - 重装明确选中的内容,需要显式 `--overwrite`;获取的是那次执行时的上游内容,不保证和上次相同。
130
+ - 现有底层锁文件仍可由 `skills` 的原生命令使用,但其更新行为不经过本封装的冲突检查。
131
+ - 没有定时镜像、自动同步删除、自动安装钩子或后台任务。
132
+
133
+ ```bash
134
+ pnpm check # 类型、单元测试、真实安装器隔离测试、格式
135
+ pnpm build
136
+ pnpm pack # 构建并生成本地 tarball,不发布
137
+ ```
138
+
139
+ 测试使用临时项目和隔离 HOME / XDG / Agent 配置,真实调用固定版本安装器安装本地 fixture,覆盖项目级与全局落盘、复制参考文件、符号链接、锁文件、保留用户修改和显式覆盖,不需要 GitHub 网络,也不修改开发者的真实 Skills。
140
+
141
+ 升级 `skills` 依赖时,同时更新 `src/install.ts` 的版本约束,并重新核对目录布局、JSON 返回结构及锁文件版本,再运行隔离测试。不要无验证升级到浮动版本。
142
+
143
+ 发布前还需要确认 npm 包名所有权、选择本项目许可证,并准备远程仓库。目前 `license: UNLICENSED`,表示本项目尚未授予开源许可,不影响引用指向上游各自的许可。
144
+
145
+ ### 使用 bumpp 发布到 npm
146
+
147
+ 发布开发环境需要满足 `bumpp` 的 Node.js 要求:`^22.18.0 || ^24.11.0 || >=26.0.0`,并同时满足本项目的 `>=22.20.0`。例如可使用 Node.js 22.20+(22.x)或 24.11+(24.x)。
148
+
149
+ 首次发布前:
150
+
151
+ 1. 确认拥有 npm 的 `@liuovo` scope 和 `@liuovo/skills` 发布权限,使用 `npm login --registry=https://registry.npmjs.org/` 登录;发布时按 npm 提示完成认证。
152
+ 2. 配置 Git remote,并先将 `main` 推送、建立 upstream(例如确认远程地址后执行 `git push -u origin main`)。
153
+ 3. 确认包名、仓库地址和许可证,提交所有待发布改动,保持工作区干净。
154
+
155
+ ```bash
156
+ # 检查、构建并预览 npm 包内容;不改版本、不提交、不推送、不发布
157
+ pnpm release:check
158
+
159
+ # 交互选择版本并确认,然后提交、打标签、推送、发布 npm
160
+ pnpm release
161
+ ```
162
+
163
+ `pnpm release` 使用固定版本的 bumpp,读取 `bump.config.ts`:
164
+
165
+ - 拒绝未提交的改动;变更版本前检查分支 upstream,并执行 `pnpm check`。
166
+ - 只更新 `package.json` 的版本,生成 `chore: release v<版本号>` 提交和 `v<版本号>` 标签。
167
+ - 先推送当前分支,再推送标签;沿用 bumpp 的 `git push --tags` 行为,会推送所有尚未同步的本地标签,发布前请核对。
168
+ - 只有 bumpp 成功结束才执行 `pnpm publish`;取消选择、检查失败或推送失败都不会进入 npm 发布。
169
+ - npm 发布前再次检查,由 `prepack` 构建 `dist`;目标固定为 npm 官方 registry,scoped 包公开发布。
170
+
171
+ 直接运行交互式 `pnpm release` 即可,**不要在后面追加 `patch` / `minor` 等参数**,这条串联脚本不会把参数传给 bumpp。版本选择在向导里完成。当前版本为 `0.1.0`,正常选择 patch 会发布 `0.1.1`;如果首次希望发布现有的 `0.1.0`,完成上述准备后单独执行 `pnpm publish`,不会自动生成 Git 标签。
172
+
173
+ Git 推送与 npm 发布不是一个事务。如果推送成功、npm 发布失败,提交和标签会保留:先确认该版本是否已经出现在 npm,再排查认证/权限等问题;未发布成功时可以直接重试 `pnpm publish`,不要为了重试再次增加版本号。若 bumpp 在提交、打标签或推送阶段中断,也应先检查本地版本、提交、标签和远程状态,再决定如何继续。
package/dist/args.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { type Agent, type Scope } from "./types.js";
2
+ export interface Options {
3
+ command: "install" | "list" | "help" | "version";
4
+ names: string[];
5
+ agents: Agent[];
6
+ scope?: Scope;
7
+ all: boolean;
8
+ yes: boolean;
9
+ dryRun: boolean;
10
+ overwrite: boolean;
11
+ json: boolean;
12
+ }
13
+ export declare function parseArgs(args: string[]): Options;
14
+ export declare const help = "Usage: liuovo-skills [install] [options]\n liuovo-skills list [--json]\n\nNo arguments: choose skills, scope, and agents interactively.\n\n -s, --skill <names...> Select names or owner/repo@name identifiers\n --all Select the entire curated catalog (not all upstream skills)\n -g, --global Install for the current user\n --project Install in the current working directory\n -a, --agent <agents...> codex, claude-code, opencode\n -y, --yes Skip prompts; requires skills, scope, and agents\n --overwrite Explicitly allow replacing existing skill paths\n --dry-run Inspect the plan; no downloads or writes\n --json Machine-readable list, plan, or results; no prompts\n -h, --help Show help\n -v, --version Show package version\n\nExisting skills are skipped unless --overwrite is provided.\n--yes does not select all skills or authorize overwrites.\nCodex and OpenCode share .agents/skills in the pinned installer.\n";
package/dist/args.js ADDED
@@ -0,0 +1,99 @@
1
+ import { AGENTS } from "./types.js";
2
+ export function parseArgs(args) {
3
+ const options = {
4
+ command: "install",
5
+ names: [],
6
+ agents: [],
7
+ all: false,
8
+ yes: false,
9
+ dryRun: false,
10
+ overwrite: false,
11
+ json: false,
12
+ };
13
+ let i = 0;
14
+ if (["install", "list"].includes(args[0] ?? "")) {
15
+ options.command = args[0];
16
+ i++;
17
+ }
18
+ for (; i < args.length; i++) {
19
+ const arg = args[i];
20
+ if (arg === "--help" || arg === "-h")
21
+ options.command = "help";
22
+ else if (arg === "--version" || arg === "-v")
23
+ options.command = "version";
24
+ else if (arg === "--global" || arg === "-g" || arg === "--project") {
25
+ const scope = arg === "--project" ? "project" : "global";
26
+ if (options.scope && options.scope !== scope) {
27
+ throw new Error("--project and --global are mutually exclusive.");
28
+ }
29
+ options.scope = scope;
30
+ }
31
+ else if (arg === "--all")
32
+ options.all = true;
33
+ else if (arg === "--yes" || arg === "-y")
34
+ options.yes = true;
35
+ else if (arg === "--dry-run")
36
+ options.dryRun = true;
37
+ else if (arg === "--overwrite")
38
+ options.overwrite = true;
39
+ else if (arg === "--json")
40
+ options.json = true;
41
+ else if (["--skill", "-s", "--agent", "-a"].includes(arg)) {
42
+ const values = [];
43
+ while (args[i + 1] && !args[i + 1].startsWith("-")) {
44
+ values.push(args[++i]);
45
+ }
46
+ if (!values.length)
47
+ throw new Error(`${arg} requires at least one value.`);
48
+ if (arg === "--skill" || arg === "-s")
49
+ options.names.push(...values);
50
+ else {
51
+ for (const value of values) {
52
+ if (!AGENTS.includes(value)) {
53
+ throw new Error(`Unknown agent "${value}". Supported: ${AGENTS.join(", ")}`);
54
+ }
55
+ options.agents.push(value);
56
+ }
57
+ }
58
+ }
59
+ else {
60
+ throw new Error(`Unknown argument: ${arg}. Run --help for usage.`);
61
+ }
62
+ }
63
+ if (options.all && options.names.length) {
64
+ throw new Error("--all and --skill are mutually exclusive.");
65
+ }
66
+ if (options.command === "list" &&
67
+ (options.all ||
68
+ options.names.length ||
69
+ options.agents.length ||
70
+ options.scope ||
71
+ options.yes ||
72
+ options.dryRun ||
73
+ options.overwrite)) {
74
+ throw new Error("list only accepts --json.");
75
+ }
76
+ options.agents = [...new Set(options.agents)];
77
+ return options;
78
+ }
79
+ export const help = `Usage: liuovo-skills [install] [options]
80
+ liuovo-skills list [--json]
81
+
82
+ No arguments: choose skills, scope, and agents interactively.
83
+
84
+ -s, --skill <names...> Select names or owner/repo@name identifiers
85
+ --all Select the entire curated catalog (not all upstream skills)
86
+ -g, --global Install for the current user
87
+ --project Install in the current working directory
88
+ -a, --agent <agents...> codex, claude-code, opencode
89
+ -y, --yes Skip prompts; requires skills, scope, and agents
90
+ --overwrite Explicitly allow replacing existing skill paths
91
+ --dry-run Inspect the plan; no downloads or writes
92
+ --json Machine-readable list, plan, or results; no prompts
93
+ -h, --help Show help
94
+ -v, --version Show package version
95
+
96
+ Existing skills are skipped unless --overwrite is provided.
97
+ --yes does not select all skills or authorize overwrites.
98
+ Codex and OpenCode share .agents/skills in the pinned installer.
99
+ `;
@@ -0,0 +1,6 @@
1
+ import type { Skill } from "./types.js";
2
+ export declare const packageRoot: string;
3
+ export declare function validateName(name: unknown): asserts name is string;
4
+ export declare function loadCatalog(root?: string): Promise<Skill[]>;
5
+ export declare function groupByCategory(catalog: Skill[]): Map<string, Skill[]>;
6
+ export declare function selectSkills(catalog: Skill[], names: string[]): Skill[];
@@ -0,0 +1,162 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import { join, relative, sep } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { parse } from "yaml";
5
+ export const packageRoot = fileURLToPath(new URL("../", import.meta.url));
6
+ function record(value) {
7
+ return value !== null && typeof value === "object" && !Array.isArray(value);
8
+ }
9
+ export function validateName(name) {
10
+ if (typeof name !== "string" ||
11
+ !/^[a-z0-9]+(?:[-_][a-z0-9]+)*$/.test(name) ||
12
+ name.length > 64) {
13
+ throw new Error(`Invalid skill name: ${String(name)}`);
14
+ }
15
+ }
16
+ function validateSource(source) {
17
+ if (typeof source !== "string" ||
18
+ !/^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+$/.test(source)) {
19
+ throw new Error(`Expected a GitHub owner/repo source: ${String(source)}`);
20
+ }
21
+ }
22
+ export async function loadCatalog(root = packageRoot) {
23
+ const pkg = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
24
+ if (!record(pkg) ||
25
+ !Array.isArray(pkg.skills) ||
26
+ !record(pkg.skillsPack) ||
27
+ !record(pkg.skillsPack.metadata)) {
28
+ throw new Error("Invalid package.json skills / skillsPack configuration.");
29
+ }
30
+ const metadata = pkg.skillsPack.metadata;
31
+ const categories = pkg.skillsPack.categories ?? [];
32
+ if (!Array.isArray(categories) ||
33
+ categories.some((category) => typeof category !== "string" || !category.trim()) ||
34
+ new Set(categories).size !== categories.length) {
35
+ throw new Error("skillsPack.categories must be an array of unique, non-empty category names.");
36
+ }
37
+ const categoryOrder = ["自有 Skills", ...categories, "其他"];
38
+ validateSource(pkg.skillsPack.ownSource);
39
+ const ownSource = pkg.skillsPack.ownSource;
40
+ const catalog = [];
41
+ for (const group of pkg.skills) {
42
+ if (!record(group) || !Array.isArray(group.skills)) {
43
+ throw new Error("Each skills entry needs a source and a skills array.");
44
+ }
45
+ validateSource(group.source);
46
+ for (const name of group.skills) {
47
+ validateName(name);
48
+ const id = `${group.source}@${name}`;
49
+ const meta = metadata[id];
50
+ if (!record(meta) ||
51
+ typeof meta.description !== "string" ||
52
+ !meta.description.trim() ||
53
+ typeof meta.path !== "string" ||
54
+ !/^skills\/[a-zA-Z0-9_/-]+$/.test(meta.path) ||
55
+ meta.path.split("/").some((part) => !part)) {
56
+ throw new Error(`Missing or invalid display metadata for ${id}.`);
57
+ }
58
+ const category = meta.category ?? "其他";
59
+ if (typeof category !== "string" || !category.trim()) {
60
+ throw new Error(`Category must be a non-empty string for ${id}.`);
61
+ }
62
+ if (categories.length && !categoryOrder.includes(category)) {
63
+ throw new Error(`Category for ${id} is not declared in skillsPack.categories: ${category}`);
64
+ }
65
+ catalog.push({
66
+ id,
67
+ name,
68
+ source: group.source,
69
+ kind: "external",
70
+ description: meta.description,
71
+ category,
72
+ url: `https://github.com/${group.source}/blob/HEAD/${meta.path}/SKILL.md`,
73
+ });
74
+ }
75
+ }
76
+ async function walk(directory) {
77
+ let entries;
78
+ try {
79
+ entries = await readdir(directory, { withFileTypes: true });
80
+ }
81
+ catch (error) {
82
+ if (error.code === "ENOENT")
83
+ return;
84
+ throw error;
85
+ }
86
+ entries.sort((a, b) => a.name.localeCompare(b.name));
87
+ if (entries.some((entry) => entry.name === "SKILL.md" && entry.isFile())) {
88
+ const text = await readFile(join(directory, "SKILL.md"), "utf8");
89
+ const header = text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
90
+ const data = header === undefined ? undefined : parse(header);
91
+ if (!record(data) ||
92
+ typeof data.description !== "string" ||
93
+ !data.description.trim()) {
94
+ throw new Error(`Invalid skill frontmatter: ${directory}/SKILL.md`);
95
+ }
96
+ validateName(data.name);
97
+ catalog.push({
98
+ id: `${ownSource}@${data.name}`,
99
+ name: data.name,
100
+ description: data.description,
101
+ category: "自有 Skills",
102
+ source: ownSource,
103
+ kind: "own",
104
+ url: `https://github.com/${ownSource}/blob/HEAD/${relative(root, directory).split(sep).join("/")}/SKILL.md`,
105
+ });
106
+ return;
107
+ }
108
+ for (const entry of entries) {
109
+ if (entry.isDirectory() &&
110
+ !entry.name.startsWith(".") &&
111
+ entry.name !== "node_modules") {
112
+ await walk(join(directory, entry.name));
113
+ }
114
+ }
115
+ }
116
+ await walk(join(root, "skills"));
117
+ const ids = new Set();
118
+ for (const skill of catalog) {
119
+ if (ids.has(skill.id))
120
+ throw new Error(`Duplicate catalog entry: ${skill.id}`);
121
+ ids.add(skill.id);
122
+ }
123
+ const categoryRank = (category) => {
124
+ const rank = categoryOrder.indexOf(category);
125
+ return rank === -1 ? categoryOrder.length : rank;
126
+ };
127
+ return catalog.sort((a, b) => categoryRank(a.category) - categoryRank(b.category) ||
128
+ a.category.localeCompare(b.category, "zh-CN") ||
129
+ a.source.localeCompare(b.source) ||
130
+ a.name.localeCompare(b.name));
131
+ }
132
+ export function groupByCategory(catalog) {
133
+ const groups = new Map();
134
+ for (const skill of catalog) {
135
+ const group = groups.get(skill.category) ?? [];
136
+ group.push(skill);
137
+ groups.set(skill.category, group);
138
+ }
139
+ return groups;
140
+ }
141
+ export function selectSkills(catalog, names) {
142
+ const selected = names.map((name) => {
143
+ const matches = catalog.filter((skill) => skill.id === name || skill.name === name);
144
+ if (matches.length === 0)
145
+ throw new Error(`Unknown skill: ${name}`);
146
+ if (matches.length > 1) {
147
+ throw new Error(`Ambiguous skill "${name}". Use one of: ${matches.map((s) => s.id).join(", ")}`);
148
+ }
149
+ return matches[0];
150
+ });
151
+ const unique = [
152
+ ...new Map(selected.map((skill) => [skill.id, skill])).values(),
153
+ ];
154
+ const installedNames = new Set();
155
+ for (const skill of unique) {
156
+ if (installedNames.has(skill.name)) {
157
+ throw new Error(`Multiple sources selected for "${skill.name}". Choose only one.`);
158
+ }
159
+ installedNames.add(skill.name);
160
+ }
161
+ return unique;
162
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, realpath } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { parseArgs, help } from "./args.js";
6
+ import { groupByCategory, loadCatalog, packageRoot } from "./catalog.js";
7
+ import { createPlan, publicPlan } from "./plan.js";
8
+ import { executePlan, INSTALLER_VERSION } from "./install.js";
9
+ import { Cancelled, choose, confirmInstall } from "./ui.js";
10
+ async function main() {
11
+ const options = parseArgs(process.argv.slice(2));
12
+ if (options.command === "help") {
13
+ console.log(help);
14
+ return;
15
+ }
16
+ if (options.command === "version") {
17
+ const pkg = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
18
+ console.log(`${pkg.name} ${pkg.version} (skills ${INSTALLER_VERSION})`);
19
+ return;
20
+ }
21
+ const catalog = await loadCatalog();
22
+ if (options.command === "list") {
23
+ if (options.json)
24
+ console.log(JSON.stringify(catalog, null, 2));
25
+ else {
26
+ for (const [category, skills] of groupByCategory(catalog)) {
27
+ console.log(`\n${category} (${skills.length} skills)`);
28
+ for (const skill of skills) {
29
+ console.log(` ${skill.name} — ${skill.description}\n Source: ${skill.source}${skill.kind === "own" ? " (own)" : ""}`);
30
+ }
31
+ }
32
+ console.log(`\n${catalog.length} skills. External content is fetched only when installing.`);
33
+ }
34
+ return;
35
+ }
36
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) &&
37
+ !options.yes &&
38
+ !options.json &&
39
+ (!process.env.CI || process.env.CI === "false");
40
+ const context = {
41
+ cwd: await realpath(process.cwd()),
42
+ home: homedir(),
43
+ env: { ...process.env },
44
+ };
45
+ const selection = await choose(options, catalog, context, interactive);
46
+ const plan = await createPlan(selection, context);
47
+ if (!options.json) {
48
+ console.log(`\nScope: ${selection.scope} (${selection.scope === "global" ? context.home : context.cwd})`);
49
+ console.log(`Agents: ${selection.agents.join(", ")}. Shared store: .agents/skills.`);
50
+ for (const item of plan.items) {
51
+ console.log(`\n${item.action.toUpperCase()} ${item.skill.id}`);
52
+ for (const path of item.paths)
53
+ console.log(` -> ${path}`);
54
+ if (item.previousSource)
55
+ console.log(` Tracked source: ${item.previousSource}`);
56
+ if (item.reason)
57
+ console.log(` ${item.reason}`);
58
+ }
59
+ }
60
+ if (options.dryRun) {
61
+ if (options.json)
62
+ console.log(JSON.stringify(publicPlan(plan), null, 2));
63
+ else
64
+ console.log("\nDry run: no downloads or writes.");
65
+ if (plan.items.some((item) => item.action === "blocked"))
66
+ process.exitCode = 1;
67
+ return;
68
+ }
69
+ const runnable = plan.items.some((item) => item.action === "install" || item.action === "overwrite");
70
+ if (runnable && !options.yes) {
71
+ if (!interactive) {
72
+ throw new Error("Use --yes to confirm a non-interactive installation, or --dry-run to preview.");
73
+ }
74
+ if (!(await confirmInstall()))
75
+ throw new Cancelled("Cancelled; nothing installed.");
76
+ }
77
+ const controller = new AbortController();
78
+ const abort = () => controller.abort();
79
+ process.once("SIGINT", abort);
80
+ process.once("SIGTERM", abort);
81
+ let results;
82
+ try {
83
+ results = await executePlan(plan, {
84
+ signal: controller.signal,
85
+ onSource: (source) => {
86
+ if (!options.json)
87
+ console.log(`\nInstalling from ${source}...`);
88
+ },
89
+ });
90
+ }
91
+ finally {
92
+ process.off("SIGINT", abort);
93
+ process.off("SIGTERM", abort);
94
+ }
95
+ if (options.json)
96
+ console.log(JSON.stringify({ results }, null, 2));
97
+ else {
98
+ console.log("");
99
+ for (const result of results) {
100
+ console.log(`${result.status.toUpperCase()} ${result.id}\n ${result.message}`);
101
+ }
102
+ const count = (status) => results.filter((result) => result.status === status).length;
103
+ console.log(`\n${count("installed")} installed, ${count("skipped")} skipped, ${count("failed")} failed.`);
104
+ }
105
+ if (controller.signal.aborted)
106
+ process.exitCode = 130;
107
+ else if (results.some((result) => result.status === "failed"))
108
+ process.exitCode = 1;
109
+ }
110
+ main().catch((error) => {
111
+ const message = error instanceof Error ? error.message : String(error);
112
+ if (process.argv.includes("--json")) {
113
+ console.log(JSON.stringify({ error: message }));
114
+ }
115
+ else {
116
+ console.error(`${error instanceof Cancelled ? "" : "Error: "}${message}`);
117
+ }
118
+ process.exitCode = error instanceof Cancelled ? 130 : 1;
119
+ });
@@ -0,0 +1,14 @@
1
+ import type { Context, InstallResult, Plan } from "./types.js";
2
+ export declare const INSTALLER_VERSION = "1.7.2";
3
+ export interface CommandResult {
4
+ code: number;
5
+ stdout: string;
6
+ stderr: string;
7
+ }
8
+ export type Runner = (args: string[], context: Context, signal?: AbortSignal) => Promise<CommandResult>;
9
+ export declare const runSkills: Runner;
10
+ export declare function executePlan(plan: Plan, { run, signal, onSource, }?: {
11
+ run?: Runner;
12
+ signal?: AbortSignal;
13
+ onSource?: (source: string) => void;
14
+ }): Promise<InstallResult[]>;