@liuovo/skills 0.1.1 → 0.1.2
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 +50 -149
- package/dist/args.d.ts +2 -6
- package/dist/args.js +23 -50
- package/dist/catalog.js +0 -7
- package/dist/cli.js +14 -85
- package/dist/install.d.ts +4 -13
- package/dist/install.js +50 -134
- package/dist/types.d.ts +0 -29
- package/dist/types.js +1 -6
- package/dist/ui.d.ts +2 -3
- package/dist/ui.js +18 -61
- package/package.json +32 -3
- package/dist/plan.d.ts +0 -14
- package/dist/plan.js +0 -199
package/README.md
CHANGED
|
@@ -1,173 +1,74 @@
|
|
|
1
1
|
# @liuovo/skills
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
一份精选的 Skills 清单,用交互式向导按需安装到 Codex、Claude Code 或 OpenCode。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 使用
|
|
6
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
|
-
发布后,用户入口才是:
|
|
7
|
+
需要 Node.js >= 22.20.0、pnpm,以及可访问 GitHub 的 Git / 网络环境。
|
|
39
8
|
|
|
40
9
|
```bash
|
|
41
10
|
pnpx @liuovo/skills
|
|
42
11
|
```
|
|
43
12
|
|
|
44
|
-
|
|
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>` 链接到共享目录 |
|
|
13
|
+
这里只选择清单里的 Skills;随后由原生 `skills` 处理 Agent、安装范围、确认和安装结果。输入关键词搜索,`Tab` 多选,`Enter` 确认,`Esc` 取消清单选择。多个来源会分别进入原生安装流程。
|
|
84
14
|
|
|
85
|
-
|
|
15
|
+
取消原生安装时遵循其退出码:返回非零才停止后续来源;若原生把取消视为正常结束并返回 `0`,仍会继续下一个来源。因此,在原生提示中按 `Ctrl+C` 或 `Esc` 不保证结束整轮操作。
|
|
86
16
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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,不是本包混合引用目录的入口。
|
|
17
|
+
```bash
|
|
18
|
+
# 查看清单
|
|
19
|
+
pnpx @liuovo/skills list
|
|
123
20
|
|
|
124
|
-
|
|
21
|
+
# 指定清单项,继续原生交互
|
|
22
|
+
pnpx @liuovo/skills --skill handoff research
|
|
125
23
|
|
|
126
|
-
|
|
24
|
+
# 原生参数直接透传,不限制 Agent 名称
|
|
25
|
+
pnpx @liuovo/skills --skill handoff --global --agent codex
|
|
127
26
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
- 现有底层锁文件仍可由 `skills` 的原生命令使用,但其更新行为不经过本封装的冲突检查。
|
|
131
|
-
- 没有定时镜像、自动同步删除、自动安装钩子或后台任务。
|
|
27
|
+
# -- 后的所有参数原样交给 skills add
|
|
28
|
+
pnpx @liuovo/skills --skill handoff -- --global --agent codex --copy --yes
|
|
132
29
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
pnpm build
|
|
136
|
-
pnpm pack # 构建并生成本地 tarball,不发布
|
|
30
|
+
# 只查看将执行的命令参数,不下载、不安装
|
|
31
|
+
pnpx @liuovo/skills --skill handoff --dry-run
|
|
137
32
|
```
|
|
138
33
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
升级 `skills` 依赖时,同时更新 `src/install.ts` 的版本约束,并重新核对目录布局、JSON 返回结构及锁文件版本,再运行隔离测试。不要无验证升级到浮动版本。
|
|
142
|
-
|
|
143
|
-
发布前还需要确认 npm 包名所有权、选择本项目许可证,并准备远程仓库。目前 `license: UNLICENSED`,表示本项目尚未授予开源许可,不影响引用指向上游各自的许可。
|
|
34
|
+
安装时优先使用 `PATH` 中已有的 `skills`;没有时执行 `pnpx skills@latest`。已有安装器不会被自动升级。
|
|
144
35
|
|
|
145
|
-
|
|
36
|
+
**同名 Skill 的处理遵循原生 `skills`,本工具不再默认跳过或自行保护已有目录。** 不自动添加 `--yes`;显式传入 `--yes` 可能覆盖已有内容和本地修改。原封装的 `--project`、`--overwrite` 不再作为自有选项;安装范围与覆盖行为以原生 CLI 为准。`--json` 在 `list` 中输出清单 JSON,在安装命令中则直接透传给原生。
|
|
146
37
|
|
|
147
|
-
|
|
38
|
+
原生 `skills` 也可能根据运行环境(例如编码 Agent)自行进入非交互模式,本工具不改写这些规则。
|
|
148
39
|
|
|
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. 确认包名、仓库地址和许可证,提交所有待发布改动,保持工作区干净。
|
|
40
|
+
更新、卸载也直接使用原生命令,例如:
|
|
154
41
|
|
|
155
42
|
```bash
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
# 交互选择版本并确认,然后提交、打标签、推送、发布 npm
|
|
160
|
-
pnpm release
|
|
43
|
+
pnpx skills update --global
|
|
44
|
+
pnpx skills update --project
|
|
45
|
+
pnpx skills remove handoff --global
|
|
161
46
|
```
|
|
162
47
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
48
|
+
更新可能覆盖本地修改;不会自动安装本清单新增的条目。
|
|
49
|
+
|
|
50
|
+
## Skills
|
|
51
|
+
|
|
52
|
+
当前包含 19 个外部 Skills 引用,安装时从各自上游获取,未复制上游正文。
|
|
53
|
+
|
|
54
|
+
| Skill | 用途 | 来源 |
|
|
55
|
+
| ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------ |
|
|
56
|
+
| [grill-me](https://github.com/mattpocock/skills/blob/HEAD/skills/productivity/grill-me/SKILL.md) | 通过连续追问,把模糊方案打磨清楚。 | mattpocock/skills |
|
|
57
|
+
| [grilling](https://github.com/mattpocock/skills/blob/HEAD/skills/productivity/grilling/SKILL.md) | 质询计划与决策,找出遗漏和薄弱环节。 | mattpocock/skills |
|
|
58
|
+
| [handoff](https://github.com/mattpocock/skills/blob/HEAD/skills/productivity/handoff/SKILL.md) | 把当前对话整理成可接续的交接文档。 | mattpocock/skills |
|
|
59
|
+
| [codebase-design](https://github.com/mattpocock/skills/blob/HEAD/skills/engineering/codebase-design/SKILL.md) | 用统一概念设计职责清晰、接口简洁的模块。 | mattpocock/skills |
|
|
60
|
+
| [writing-for-agents](https://github.com/mattpocock/skills/blob/HEAD/skills/productivity/writing-for-agents/SKILL.md) | 编写清晰、可执行的 Agent 文档与 Skills。 | mattpocock/skills |
|
|
61
|
+
| [to-questionnaire](https://github.com/mattpocock/skills/blob/HEAD/skills/productivity/to-questionnaire/SKILL.md) | 将难以独自定夺的决策整理成问卷,交给他人填写。 | mattpocock/skills |
|
|
62
|
+
| [code-review](https://github.com/mattpocock/skills/blob/HEAD/skills/engineering/code-review/SKILL.md) | 对照仓库规范与原始需求,审查指定基准后的改动。 | mattpocock/skills |
|
|
63
|
+
| [research](https://github.com/mattpocock/skills/blob/HEAD/skills/engineering/research/SKILL.md) | 基于高可信一手来源调研,并将结论写入仓库。 | mattpocock/skills |
|
|
64
|
+
| [improve](https://github.com/shadcn/improve/blob/HEAD/skills/improve/SKILL.md) | 审视代码库,按优先级生成可交付的改进计划。 | shadcn/improve |
|
|
65
|
+
| [dry-refactoring](https://github.com/kucherenko/jscpd/blob/HEAD/skills/dry-refactoring/SKILL.md) | 结合 jscpd 检测结果,逐步消除重复代码。 | kucherenko/jscpd |
|
|
66
|
+
| [i-have-adhd](https://github.com/ayghri/i-have-adhd/blob/HEAD/skills/i-have-adhd/SKILL.md) | 突出下一步、拆分步骤,减少阅读干扰。 | ayghri/i-have-adhd |
|
|
67
|
+
| [vue-best-practices](https://github.com/vuejs-ai/skills/blob/HEAD/skills/vue-best-practices/SKILL.md) | 用 TypeScript 和组合式 API 编写 Vue 3。 | vuejs-ai/skills |
|
|
68
|
+
| [vue-router-best-practices](https://github.com/vuejs-ai/skills/blob/HEAD/skills/vue-router-best-practices/SKILL.md) | 处理 Vue Router 路由、守卫与组件生命周期。 | vuejs-ai/skills |
|
|
69
|
+
| [vue-pinia-best-practices](https://github.com/vuejs-ai/skills/blob/HEAD/skills/vue-pinia-best-practices/SKILL.md) | 规范 Pinia 状态管理与响应式数据组织。 | vuejs-ai/skills |
|
|
70
|
+
| [vue-testing-best-practices](https://github.com/vuejs-ai/skills/blob/HEAD/skills/vue-testing-best-practices/SKILL.md) | 编写 Vue 组件测试、模拟依赖与端到端测试。 | vuejs-ai/skills |
|
|
71
|
+
| [vue-debug-guides](https://github.com/vuejs-ai/skills/blob/HEAD/skills/vue-debug-guides/SKILL.md) | 定位 Vue 运行时、异步与服务端渲染问题。 | vuejs-ai/skills |
|
|
72
|
+
| [create-adaptable-composable](https://github.com/vuejs-ai/skills/blob/HEAD/skills/create-adaptable-composable/SKILL.md) | 编写兼容普通值、ref 和 getter 的组合函数。 | vuejs-ai/skills |
|
|
73
|
+
| [frontend-design](https://github.com/anthropics/skills/blob/HEAD/skills/frontend-design/SKILL.md) | 打造有辨识度、视觉统一的前端界面。 | anthropics/skills |
|
|
74
|
+
| [antfu](https://github.com/antfu/skills/blob/HEAD/skills/antfu/SKILL.md) | 采用 Anthony Fu 的 JS/TS 工具链与工程约定。 | antfu/skills |
|
package/dist/args.d.ts
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
import { type Agent, type Scope } from "./types.js";
|
|
2
1
|
export interface Options {
|
|
3
2
|
command: "install" | "list" | "help" | "version";
|
|
4
3
|
names: string[];
|
|
5
|
-
agents: Agent[];
|
|
6
|
-
scope?: Scope;
|
|
7
4
|
all: boolean;
|
|
8
|
-
yes: boolean;
|
|
9
5
|
dryRun: boolean;
|
|
10
|
-
overwrite: boolean;
|
|
11
6
|
json: boolean;
|
|
7
|
+
nativeArgs: string[];
|
|
12
8
|
}
|
|
13
9
|
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,
|
|
10
|
+
export declare const help = "Usage: liuovo-skills [install] [catalog options] [native options]\n liuovo-skills list [--json]\n\nNo arguments: choose catalog skills, then continue in the native skills CLI.\n\n -s, --skill <names...> Select catalog names or owner/repo@name identifiers\n --all Select the entire curated catalog\n --dry-run Print command arguments only; no downloads or writes\n -h, --help Show this help\n -v, --version Show package version\n\nOther options are forwarded to skills add unchanged.\nUse -- to pass all remaining arguments directly to skills add.\nFor example: --global --agent codex, or -- --copy --yes.\nInstallation prompts, agents, scope, conflicts, and lock files belong to skills.\nNo --yes or --json is added automatically. Explicit --yes may overwrite skills.\n";
|
package/dist/args.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import { AGENTS } from "./types.js";
|
|
2
1
|
export function parseArgs(args) {
|
|
3
2
|
const options = {
|
|
4
3
|
command: "install",
|
|
5
4
|
names: [],
|
|
6
|
-
agents: [],
|
|
7
5
|
all: false,
|
|
8
|
-
yes: false,
|
|
9
6
|
dryRun: false,
|
|
10
|
-
overwrite: false,
|
|
11
7
|
json: false,
|
|
8
|
+
nativeArgs: [],
|
|
12
9
|
};
|
|
13
10
|
let i = 0;
|
|
14
11
|
if (["install", "list"].includes(args[0] ?? "")) {
|
|
@@ -17,47 +14,31 @@ export function parseArgs(args) {
|
|
|
17
14
|
}
|
|
18
15
|
for (; i < args.length; i++) {
|
|
19
16
|
const arg = args[i];
|
|
17
|
+
if (arg === "--") {
|
|
18
|
+
options.nativeArgs.push(...args.slice(i + 1));
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
20
21
|
if (arg === "--help" || arg === "-h")
|
|
21
22
|
options.command = "help";
|
|
22
23
|
else if (arg === "--version" || arg === "-v")
|
|
23
24
|
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
25
|
else if (arg === "--all")
|
|
32
26
|
options.all = true;
|
|
33
|
-
else if (arg === "--yes" || arg === "-y")
|
|
34
|
-
options.yes = true;
|
|
35
27
|
else if (arg === "--dry-run")
|
|
36
28
|
options.dryRun = true;
|
|
37
|
-
else if (arg === "--
|
|
38
|
-
options.overwrite = true;
|
|
39
|
-
else if (arg === "--json")
|
|
29
|
+
else if (arg === "--json" && options.command === "list")
|
|
40
30
|
options.json = true;
|
|
41
|
-
else if (
|
|
42
|
-
const
|
|
31
|
+
else if (arg === "--skill" || arg === "-s") {
|
|
32
|
+
const start = options.names.length;
|
|
43
33
|
while (args[i + 1] && !args[i + 1].startsWith("-")) {
|
|
44
|
-
|
|
34
|
+
options.names.push(args[++i]);
|
|
45
35
|
}
|
|
46
|
-
if (
|
|
36
|
+
if (options.names.length === start) {
|
|
47
37
|
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
38
|
}
|
|
58
39
|
}
|
|
59
40
|
else {
|
|
60
|
-
|
|
41
|
+
options.nativeArgs.push(arg);
|
|
61
42
|
}
|
|
62
43
|
}
|
|
63
44
|
if (options.all && options.names.length) {
|
|
@@ -66,34 +47,26 @@ export function parseArgs(args) {
|
|
|
66
47
|
if (options.command === "list" &&
|
|
67
48
|
(options.all ||
|
|
68
49
|
options.names.length ||
|
|
69
|
-
options.agents.length ||
|
|
70
|
-
options.scope ||
|
|
71
|
-
options.yes ||
|
|
72
50
|
options.dryRun ||
|
|
73
|
-
options.
|
|
51
|
+
options.nativeArgs.length)) {
|
|
74
52
|
throw new Error("list only accepts --json.");
|
|
75
53
|
}
|
|
76
|
-
options.agents = [...new Set(options.agents)];
|
|
77
54
|
return options;
|
|
78
55
|
}
|
|
79
|
-
export const help = `Usage: liuovo-skills [install] [options]
|
|
56
|
+
export const help = `Usage: liuovo-skills [install] [catalog options] [native options]
|
|
80
57
|
liuovo-skills list [--json]
|
|
81
58
|
|
|
82
|
-
No arguments: choose skills,
|
|
59
|
+
No arguments: choose catalog skills, then continue in the native skills CLI.
|
|
83
60
|
|
|
84
|
-
-s, --skill <names...> Select names or owner/repo@name identifiers
|
|
85
|
-
--all Select the entire curated catalog
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
61
|
+
-s, --skill <names...> Select catalog names or owner/repo@name identifiers
|
|
62
|
+
--all Select the entire curated catalog
|
|
63
|
+
--dry-run Print command arguments only; no downloads or writes
|
|
64
|
+
-h, --help Show this help
|
|
94
65
|
-v, --version Show package version
|
|
95
66
|
|
|
96
|
-
|
|
97
|
-
--
|
|
98
|
-
|
|
67
|
+
Other options are forwarded to skills add unchanged.
|
|
68
|
+
Use -- to pass all remaining arguments directly to skills add.
|
|
69
|
+
For example: --global --agent codex, or -- --copy --yes.
|
|
70
|
+
Installation prompts, agents, scope, conflicts, and lock files belong to skills.
|
|
71
|
+
No --yes or --json is added automatically. Explicit --yes may overwrite skills.
|
|
99
72
|
`;
|
package/dist/catalog.js
CHANGED
|
@@ -151,12 +151,5 @@ export function selectSkills(catalog, names) {
|
|
|
151
151
|
const unique = [
|
|
152
152
|
...new Map(selected.map((skill) => [skill.id, skill])).values(),
|
|
153
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
154
|
return unique;
|
|
162
155
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFile
|
|
3
|
-
import { homedir } from "node:os";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
4
3
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
4
|
+
import { help, parseArgs } from "./args.js";
|
|
6
5
|
import { groupByCategory, loadCatalog, packageRoot } from "./catalog.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { Cancelled, choose, confirmInstall } from "./ui.js";
|
|
6
|
+
import { buildCommands, installSkills } from "./install.js";
|
|
7
|
+
import { Cancelled, choose } from "./ui.js";
|
|
10
8
|
async function main() {
|
|
11
9
|
const options = parseArgs(process.argv.slice(2));
|
|
12
10
|
if (options.command === "help") {
|
|
@@ -15,7 +13,7 @@ async function main() {
|
|
|
15
13
|
}
|
|
16
14
|
if (options.command === "version") {
|
|
17
15
|
const pkg = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
|
|
18
|
-
console.log(`${pkg.name} ${pkg.version}
|
|
16
|
+
console.log(`${pkg.name} ${pkg.version}`);
|
|
19
17
|
return;
|
|
20
18
|
}
|
|
21
19
|
const catalog = await loadCatalog();
|
|
@@ -29,91 +27,22 @@ async function main() {
|
|
|
29
27
|
console.log(` ${skill.name} — ${skill.description}\n Source: ${skill.source}${skill.kind === "own" ? " (own)" : ""}`);
|
|
30
28
|
}
|
|
31
29
|
}
|
|
32
|
-
console.log(`\n${catalog.length} skills
|
|
30
|
+
console.log(`\n${catalog.length} skills.`);
|
|
33
31
|
}
|
|
34
32
|
return;
|
|
35
33
|
}
|
|
36
|
-
const
|
|
37
|
-
|
|
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
|
-
}
|
|
34
|
+
const skills = await choose(options, catalog, Boolean(process.stdin.isTTY && process.stdout.isTTY));
|
|
35
|
+
const commands = buildCommands(skills, options.nativeArgs);
|
|
60
36
|
if (options.dryRun) {
|
|
61
|
-
|
|
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;
|
|
37
|
+
console.log(JSON.stringify(commands.map((args) => ["skills", ...args]), null, 2));
|
|
67
38
|
return;
|
|
68
39
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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;
|
|
40
|
+
process.exitCode = await installSkills(commands, {
|
|
41
|
+
cwd: process.cwd(),
|
|
42
|
+
env: process.env,
|
|
43
|
+
});
|
|
109
44
|
}
|
|
110
45
|
main().catch((error) => {
|
|
111
|
-
|
|
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
|
-
}
|
|
46
|
+
console.error(`${error instanceof Cancelled ? "" : "Error: "}${error instanceof Error ? error.message : String(error)}`);
|
|
118
47
|
process.exitCode = error instanceof Cancelled ? 130 : 1;
|
|
119
48
|
});
|
package/dist/install.d.ts
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
import type { Context,
|
|
2
|
-
export
|
|
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>;
|
|
1
|
+
import type { Context, Skill } from "./types.js";
|
|
2
|
+
export type Runner = (args: string[], context: Context, signal?: AbortSignal) => Promise<number>;
|
|
9
3
|
export declare const runSkills: Runner;
|
|
10
|
-
export declare function
|
|
11
|
-
|
|
12
|
-
signal?: AbortSignal;
|
|
13
|
-
onSource?: (source: string) => void;
|
|
14
|
-
}): Promise<InstallResult[]>;
|
|
4
|
+
export declare function buildCommands(skills: Skill[], nativeArgs: string[]): string[][];
|
|
5
|
+
export declare function installSkills(commands: string[][], context: Context, run?: Runner): Promise<number>;
|
package/dist/install.js
CHANGED
|
@@ -1,142 +1,58 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { dirname, resolve } from "node:path";
|
|
5
|
-
import { buildAddArgs, createPlan, groupSkills } from "./plan.js";
|
|
6
|
-
import { AGENT_LABELS } from "./types.js";
|
|
7
|
-
const require = createRequire(import.meta.url);
|
|
8
|
-
export const INSTALLER_VERSION = "1.7.2";
|
|
9
|
-
export const runSkills = async (args, context, signal) => {
|
|
10
|
-
const packagePath = require.resolve("skills/package.json");
|
|
11
|
-
const pkg = JSON.parse(await readFile(packagePath, "utf8"));
|
|
12
|
-
if (pkg.version !== INSTALLER_VERSION) {
|
|
13
|
-
throw new Error(`Expected skills ${INSTALLER_VERSION}; found ${pkg.version}. Reinstall dependencies.`);
|
|
14
|
-
}
|
|
15
|
-
const entry = resolve(dirname(packagePath), pkg.bin.skills);
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { constants } from "node:os";
|
|
3
|
+
async function runCommand(command, args, context, signal) {
|
|
16
4
|
return await new Promise((accept, reject) => {
|
|
17
|
-
|
|
5
|
+
const child = spawn(command, args, {
|
|
18
6
|
cwd: context.cwd,
|
|
19
|
-
env:
|
|
20
|
-
|
|
21
|
-
maxBuffer: 8 * 1024 * 1024,
|
|
22
|
-
timeout: 180_000,
|
|
7
|
+
env: context.env,
|
|
8
|
+
stdio: "inherit",
|
|
23
9
|
...(signal ? { signal } : {}),
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
else {
|
|
29
|
-
accept({
|
|
30
|
-
code: typeof error?.code === "number" ? error.code : 0,
|
|
31
|
-
stdout,
|
|
32
|
-
stderr,
|
|
33
|
-
});
|
|
34
|
-
}
|
|
10
|
+
});
|
|
11
|
+
child.once("error", reject);
|
|
12
|
+
child.once("close", (code, childSignal) => {
|
|
13
|
+
accept(code ?? (childSignal ? 128 + constants.signals[childSignal] : 1));
|
|
35
14
|
});
|
|
36
15
|
});
|
|
37
|
-
}
|
|
38
|
-
export
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const runnable = fresh.items.filter((item) => {
|
|
54
|
-
const approved = plan.items.find((old) => old.skill.id === item.skill.id);
|
|
55
|
-
const newlyExisting = item.existing.some((path) => !approved.existing.includes(path));
|
|
56
|
-
if (item.action === "blocked" ||
|
|
57
|
-
item.action === "skip" ||
|
|
58
|
-
newlyExisting) {
|
|
59
|
-
results.push({
|
|
60
|
-
id: item.skill.id,
|
|
61
|
-
status: item.action === "skip" && !newlyExisting ? "skipped" : "failed",
|
|
62
|
-
message: newlyExisting
|
|
63
|
-
? "Destination changed after planning; rerun to review it."
|
|
64
|
-
: (item.reason ?? item.action),
|
|
65
|
-
});
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
return true;
|
|
69
|
-
});
|
|
70
|
-
if (!runnable.length)
|
|
71
|
-
continue;
|
|
72
|
-
onSource?.(source);
|
|
73
|
-
const output = await run(buildAddArgs(source, runnable.map((item) => item.skill), plan.selection), plan.context, signal);
|
|
74
|
-
let parsed;
|
|
75
|
-
try {
|
|
76
|
-
parsed = JSON.parse(output.stdout);
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
throw new Error(`Installer returned invalid JSON (exit ${output.code}). ${output.stderr.trim().slice(-1500)}`);
|
|
80
|
-
}
|
|
81
|
-
if (!Array.isArray(parsed))
|
|
82
|
-
throw new Error("Installer returned a non-array result.");
|
|
83
|
-
const entries = parsed;
|
|
84
|
-
const generalError = entries.find((entry) => entry?.status === "failed" && !entry.name)?.error;
|
|
85
|
-
for (const item of runnable) {
|
|
86
|
-
const matches = entries.filter((entry) => entry?.name === item.skill.name);
|
|
87
|
-
const result = matches.length === 1 ? matches[0] : undefined;
|
|
88
|
-
if (result?.status === "installed") {
|
|
89
|
-
const agents = result.agents;
|
|
90
|
-
if (typeof result.path !== "string" ||
|
|
91
|
-
!item.paths.includes(resolve(result.path)) ||
|
|
92
|
-
result.scope !== plan.selection.scope ||
|
|
93
|
-
!Array.isArray(agents) ||
|
|
94
|
-
!plan.selection.agents.every((agent) => agents.includes(AGENT_LABELS[agent]))) {
|
|
95
|
-
results.push({
|
|
96
|
-
id: item.skill.id,
|
|
97
|
-
status: "failed",
|
|
98
|
-
message: "Installer reported unexpected destinations or incomplete agents; inspect installed files.",
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
results.push({
|
|
103
|
-
id: item.skill.id,
|
|
104
|
-
status: "installed",
|
|
105
|
-
message: result.path,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
results.push({
|
|
111
|
-
id: item.skill.id,
|
|
112
|
-
status: "failed",
|
|
113
|
-
message: String(result?.error ??
|
|
114
|
-
generalError ??
|
|
115
|
-
`No successful result for this skill (exit ${output.code}). ${output.stderr.trim().slice(-1000)}`),
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (output.code !== 0 &&
|
|
120
|
-
!results.some((result) => skills.some((skill) => skill.id === result.id) &&
|
|
121
|
-
result.status === "failed")) {
|
|
122
|
-
results.push({
|
|
123
|
-
id: source,
|
|
124
|
-
status: "failed",
|
|
125
|
-
message: `Installer exited ${output.code}; files may already be installed. ${output.stderr.trim().slice(-1000)}`,
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
catch (error) {
|
|
130
|
-
for (const skill of skills) {
|
|
131
|
-
if (!results.some((result) => result.id === skill.id)) {
|
|
132
|
-
results.push({
|
|
133
|
-
id: skill.id,
|
|
134
|
-
status: "failed",
|
|
135
|
-
message: error instanceof Error ? error.message : String(error),
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
16
|
+
}
|
|
17
|
+
export const runSkills = async (args, context, signal) => {
|
|
18
|
+
try {
|
|
19
|
+
return await runCommand("skills", args, context, signal);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
// Only a missing executable permits fallback; never retry a started install.
|
|
23
|
+
if (error.code !== "ENOENT")
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return await runCommand("pnpx", ["skills@latest", ...args], context, signal);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code === "ENOENT") {
|
|
31
|
+
throw new Error("Neither skills nor pnpx was found on PATH. Install skills or pnpm, then retry.");
|
|
139
32
|
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
export function buildCommands(skills, nativeArgs) {
|
|
37
|
+
const groups = new Map();
|
|
38
|
+
for (const skill of skills) {
|
|
39
|
+
const names = groups.get(skill.source) ?? [];
|
|
40
|
+
names.push(skill.name);
|
|
41
|
+
groups.set(skill.source, names);
|
|
42
|
+
}
|
|
43
|
+
return [...groups].map(([source, names]) => [
|
|
44
|
+
"add",
|
|
45
|
+
source,
|
|
46
|
+
"--skill",
|
|
47
|
+
...names,
|
|
48
|
+
...nativeArgs,
|
|
49
|
+
]);
|
|
50
|
+
}
|
|
51
|
+
export async function installSkills(commands, context, run = runSkills) {
|
|
52
|
+
for (const args of commands) {
|
|
53
|
+
const code = await run(args, context);
|
|
54
|
+
if (code !== 0)
|
|
55
|
+
return code;
|
|
140
56
|
}
|
|
141
|
-
return
|
|
57
|
+
return 0;
|
|
142
58
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
export type Scope = "project" | "global";
|
|
2
|
-
export declare const AGENTS: readonly ["codex", "claude-code", "opencode"];
|
|
3
|
-
export type Agent = (typeof AGENTS)[number];
|
|
4
|
-
export declare const AGENT_LABELS: Record<Agent, string>;
|
|
5
1
|
export interface Skill {
|
|
6
2
|
id: string;
|
|
7
3
|
name: string;
|
|
@@ -13,30 +9,5 @@ export interface Skill {
|
|
|
13
9
|
}
|
|
14
10
|
export interface Context {
|
|
15
11
|
cwd: string;
|
|
16
|
-
home: string;
|
|
17
12
|
env: NodeJS.ProcessEnv;
|
|
18
13
|
}
|
|
19
|
-
export interface Selection {
|
|
20
|
-
skills: Skill[];
|
|
21
|
-
agents: Agent[];
|
|
22
|
-
scope: Scope;
|
|
23
|
-
overwrite: boolean;
|
|
24
|
-
}
|
|
25
|
-
export interface PlanItem {
|
|
26
|
-
skill: Skill;
|
|
27
|
-
paths: string[];
|
|
28
|
-
existing: string[];
|
|
29
|
-
previousSource?: string;
|
|
30
|
-
action: "install" | "overwrite" | "skip" | "blocked";
|
|
31
|
-
reason?: string;
|
|
32
|
-
}
|
|
33
|
-
export interface Plan {
|
|
34
|
-
selection: Selection;
|
|
35
|
-
context: Context;
|
|
36
|
-
items: PlanItem[];
|
|
37
|
-
}
|
|
38
|
-
export interface InstallResult {
|
|
39
|
-
id: string;
|
|
40
|
-
status: "installed" | "skipped" | "failed";
|
|
41
|
-
message: string;
|
|
42
|
-
}
|
package/dist/types.js
CHANGED
package/dist/ui.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Options } from "./args.js";
|
|
2
|
-
import
|
|
2
|
+
import type { Skill } from "./types.js";
|
|
3
3
|
export declare class Cancelled extends Error {
|
|
4
4
|
}
|
|
5
|
-
export declare function choose(options: Options, catalog: Skill[],
|
|
6
|
-
export declare function confirmInstall(): Promise<boolean>;
|
|
5
|
+
export declare function choose(options: Options, catalog: Skill[], interactive: boolean): Promise<Skill[]>;
|
package/dist/ui.js
CHANGED
|
@@ -1,68 +1,25 @@
|
|
|
1
|
-
import * as p from "@clack/prompts";
|
|
2
1
|
import { selectSkills } from "./catalog.js";
|
|
3
2
|
import { searchMultiselect } from "./picker.js";
|
|
4
|
-
import { AGENTS, AGENT_LABELS, } from "./types.js";
|
|
5
3
|
export class Cancelled extends Error {
|
|
6
4
|
}
|
|
7
|
-
function
|
|
8
|
-
if (p.isCancel(answer))
|
|
9
|
-
throw new Cancelled("Cancelled.");
|
|
10
|
-
return answer;
|
|
11
|
-
}
|
|
12
|
-
export async function choose(options, catalog, context, interactive) {
|
|
13
|
-
const missing = [
|
|
14
|
-
!options.all && !options.names.length ? "--skill <names...> or --all" : "",
|
|
15
|
-
!options.scope ? "--project or --global" : "",
|
|
16
|
-
!options.agents.length ? "--agent <agents...>" : "",
|
|
17
|
-
].filter(Boolean);
|
|
18
|
-
if (!interactive && missing.length) {
|
|
19
|
-
throw new Error(`Non-interactive mode requires: ${missing.join("; ")}.`);
|
|
20
|
-
}
|
|
5
|
+
export async function choose(options, catalog, interactive) {
|
|
21
6
|
if (!catalog.length)
|
|
22
7
|
throw new Error("The catalog is empty.");
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
value: "project",
|
|
41
|
-
label: "Current project",
|
|
42
|
-
hint: context.cwd,
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
value: "global",
|
|
46
|
-
label: "Global (current user)",
|
|
47
|
-
hint: context.home,
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
}));
|
|
51
|
-
const agents = options.agents.length
|
|
52
|
-
? options.agents
|
|
53
|
-
: value(await p.multiselect({
|
|
54
|
-
message: "Which agents? Codex and OpenCode share .agents/skills.",
|
|
55
|
-
options: AGENTS.map((agent) => ({
|
|
56
|
-
value: agent,
|
|
57
|
-
label: AGENT_LABELS[agent],
|
|
58
|
-
})),
|
|
59
|
-
required: true,
|
|
60
|
-
}));
|
|
61
|
-
return { skills, agents, scope, overwrite: options.overwrite };
|
|
62
|
-
}
|
|
63
|
-
export async function confirmInstall() {
|
|
64
|
-
return value(await p.confirm({
|
|
65
|
-
message: "Install this plan? Review upstream skills before trusting them.",
|
|
66
|
-
initialValue: false,
|
|
67
|
-
}));
|
|
8
|
+
if (options.all)
|
|
9
|
+
return selectSkills(catalog, catalog.map((skill) => skill.id));
|
|
10
|
+
if (options.names.length)
|
|
11
|
+
return selectSkills(catalog, options.names);
|
|
12
|
+
if (!interactive) {
|
|
13
|
+
throw new Error("Non-interactive catalog selection requires --skill <names...> or --all.");
|
|
14
|
+
}
|
|
15
|
+
const names = await searchMultiselect("Which skills? Type to search; Tab to select.", catalog.map((skill) => ({
|
|
16
|
+
value: skill.id,
|
|
17
|
+
label: skill.name,
|
|
18
|
+
description: skill.description,
|
|
19
|
+
category: skill.category,
|
|
20
|
+
source: skill.kind === "own" ? `${skill.source} (own)` : skill.source,
|
|
21
|
+
})));
|
|
22
|
+
if (typeof names === "symbol")
|
|
23
|
+
throw new Cancelled("Cancelled.");
|
|
24
|
+
return selectSkills(catalog, names);
|
|
68
25
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liuovo/skills",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Personal skills and curated upstream references, installed through one interactive CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@clack/core": "1.5.1",
|
|
27
27
|
"@clack/prompts": "1.8.1",
|
|
28
|
-
"skills": "1.7.2",
|
|
29
28
|
"yaml": "2.9.1"
|
|
30
29
|
},
|
|
31
30
|
"devDependencies": {
|
|
32
31
|
"@types/node": "22.20.5",
|
|
33
32
|
"bumpp": "12.3.0",
|
|
34
33
|
"prettier": "3.9.10",
|
|
34
|
+
"skills": "latest",
|
|
35
35
|
"tsx": "4.23.15",
|
|
36
36
|
"typescript": "5.9.3"
|
|
37
37
|
},
|
|
@@ -43,7 +43,10 @@
|
|
|
43
43
|
"grilling",
|
|
44
44
|
"handoff",
|
|
45
45
|
"codebase-design",
|
|
46
|
-
"writing-for-agents"
|
|
46
|
+
"writing-for-agents",
|
|
47
|
+
"to-questionnaire",
|
|
48
|
+
"code-review",
|
|
49
|
+
"research"
|
|
47
50
|
]
|
|
48
51
|
},
|
|
49
52
|
{
|
|
@@ -80,6 +83,12 @@
|
|
|
80
83
|
"skills": [
|
|
81
84
|
"frontend-design"
|
|
82
85
|
]
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"source": "antfu/skills",
|
|
89
|
+
"skills": [
|
|
90
|
+
"antfu"
|
|
91
|
+
]
|
|
83
92
|
}
|
|
84
93
|
],
|
|
85
94
|
"skillsPack": {
|
|
@@ -110,6 +119,21 @@
|
|
|
110
119
|
"path": "skills/productivity/writing-for-agents",
|
|
111
120
|
"category": "协作与表达"
|
|
112
121
|
},
|
|
122
|
+
"mattpocock/skills@to-questionnaire": {
|
|
123
|
+
"description": "将难以独自定夺的决策整理成问卷,交给他人填写。",
|
|
124
|
+
"path": "skills/productivity/to-questionnaire",
|
|
125
|
+
"category": "方案推敲"
|
|
126
|
+
},
|
|
127
|
+
"mattpocock/skills@code-review": {
|
|
128
|
+
"description": "对照仓库规范与原始需求,审查指定基准后的改动。",
|
|
129
|
+
"path": "skills/engineering/code-review",
|
|
130
|
+
"category": "代码设计与重构"
|
|
131
|
+
},
|
|
132
|
+
"mattpocock/skills@research": {
|
|
133
|
+
"description": "基于高可信一手来源调研,并将结论写入仓库。",
|
|
134
|
+
"path": "skills/engineering/research",
|
|
135
|
+
"category": "协作与表达"
|
|
136
|
+
},
|
|
113
137
|
"shadcn/improve@improve": {
|
|
114
138
|
"description": "审视代码库,按优先级生成可交付的改进计划。",
|
|
115
139
|
"path": "skills/improve",
|
|
@@ -159,6 +183,11 @@
|
|
|
159
183
|
"description": "打造有辨识度、视觉统一的前端界面。",
|
|
160
184
|
"path": "skills/frontend-design",
|
|
161
185
|
"category": "前端设计"
|
|
186
|
+
},
|
|
187
|
+
"antfu/skills@antfu": {
|
|
188
|
+
"description": "采用 Anthony Fu 的 JS/TS 工具链与工程约定。",
|
|
189
|
+
"path": "skills/antfu",
|
|
190
|
+
"category": "代码设计与重构"
|
|
162
191
|
}
|
|
163
192
|
},
|
|
164
193
|
"categories": [
|
package/dist/plan.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { Context, Plan, PlanItem, Selection, Skill } from "./types.js";
|
|
2
|
-
export declare function targetPaths(name: string, selection: Selection, context: Context): string[];
|
|
3
|
-
export declare function lockPath(selection: Selection, context: Context): string;
|
|
4
|
-
export declare function createPlan(selection: Selection, context: Context): Promise<Plan>;
|
|
5
|
-
export declare function groupSkills(skills: Skill[]): Map<string, Skill[]>;
|
|
6
|
-
export declare function buildAddArgs(source: string, skills: Skill[], selection: Selection): string[];
|
|
7
|
-
export declare function publicPlan(plan: Plan): {
|
|
8
|
-
scope: import("./types.js").Scope;
|
|
9
|
-
directory: string;
|
|
10
|
-
agents: ("codex" | "claude-code" | "opencode")[];
|
|
11
|
-
sharedDirectory: string;
|
|
12
|
-
items: PlanItem[];
|
|
13
|
-
commands: string[][];
|
|
14
|
-
};
|
package/dist/plan.js
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { lstat, readFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
-
import { validateName } from "./catalog.js";
|
|
4
|
-
async function statOrMissing(path) {
|
|
5
|
-
try {
|
|
6
|
-
return await lstat(path);
|
|
7
|
-
}
|
|
8
|
-
catch (error) {
|
|
9
|
-
if (error.code === "ENOENT")
|
|
10
|
-
return undefined;
|
|
11
|
-
throw error;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
// This adapter intentionally covers only the three agents tested with skills 1.7.2.
|
|
15
|
-
export function targetPaths(name, selection, context) {
|
|
16
|
-
validateName(name);
|
|
17
|
-
const base = selection.scope === "global" ? context.home : context.cwd;
|
|
18
|
-
const paths = [join(base, ".agents", "skills", name)];
|
|
19
|
-
if (selection.agents.includes("claude-code")) {
|
|
20
|
-
const claudeBase = selection.scope === "global"
|
|
21
|
-
? context.env.CLAUDE_CONFIG_DIR?.trim() || join(context.home, ".claude")
|
|
22
|
-
: join(context.cwd, ".claude");
|
|
23
|
-
if (!isAbsolute(claudeBase)) {
|
|
24
|
-
throw new Error("CLAUDE_CONFIG_DIR must be absolute.");
|
|
25
|
-
}
|
|
26
|
-
paths.push(join(claudeBase, "skills", name));
|
|
27
|
-
}
|
|
28
|
-
return [...new Set(paths)];
|
|
29
|
-
}
|
|
30
|
-
function legacyPaths(name, selection, context) {
|
|
31
|
-
if (selection.scope !== "global")
|
|
32
|
-
return [];
|
|
33
|
-
const paths = [];
|
|
34
|
-
if (selection.agents.includes("codex")) {
|
|
35
|
-
const base = context.env.CODEX_HOME?.trim() || join(context.home, ".codex");
|
|
36
|
-
if (!isAbsolute(base))
|
|
37
|
-
throw new Error("CODEX_HOME must be absolute.");
|
|
38
|
-
paths.push(join(base, "skills", name));
|
|
39
|
-
}
|
|
40
|
-
if (selection.agents.includes("opencode")) {
|
|
41
|
-
const xdg = context.env.XDG_CONFIG_HOME;
|
|
42
|
-
const base = xdg && isAbsolute(xdg) ? xdg : join(context.home, ".config");
|
|
43
|
-
paths.push(join(base, "opencode", "skills", name));
|
|
44
|
-
}
|
|
45
|
-
return paths;
|
|
46
|
-
}
|
|
47
|
-
export function lockPath(selection, context) {
|
|
48
|
-
if (selection.scope === "project")
|
|
49
|
-
return join(context.cwd, "skills-lock.json");
|
|
50
|
-
const stateHome = context.env.XDG_STATE_HOME;
|
|
51
|
-
if (stateHome && !isAbsolute(stateHome)) {
|
|
52
|
-
throw new Error("XDG_STATE_HOME must be absolute.");
|
|
53
|
-
}
|
|
54
|
-
return stateHome
|
|
55
|
-
? join(stateHome, "skills", ".skill-lock.json")
|
|
56
|
-
: join(context.home, ".agents", ".skill-lock.json");
|
|
57
|
-
}
|
|
58
|
-
async function unsafeParent(path) {
|
|
59
|
-
let parent = dirname(path);
|
|
60
|
-
while (true) {
|
|
61
|
-
const stat = await statOrMissing(parent);
|
|
62
|
-
if (stat?.isSymbolicLink())
|
|
63
|
-
return `Symlinked parent directory: ${parent}`;
|
|
64
|
-
if (stat && !stat.isDirectory())
|
|
65
|
-
return `Parent is not a directory: ${parent}`;
|
|
66
|
-
const next = dirname(parent);
|
|
67
|
-
if (next === parent)
|
|
68
|
-
return undefined;
|
|
69
|
-
parent = next;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
async function readLock(selection, context) {
|
|
73
|
-
const path = lockPath(selection, context);
|
|
74
|
-
const parentError = await unsafeParent(path);
|
|
75
|
-
if (parentError)
|
|
76
|
-
throw new Error(parentError);
|
|
77
|
-
const stat = await statOrMissing(path);
|
|
78
|
-
if (!stat)
|
|
79
|
-
return {};
|
|
80
|
-
if (!stat.isFile())
|
|
81
|
-
throw new Error(`Lock file is not a regular file: ${path}`);
|
|
82
|
-
let lock;
|
|
83
|
-
try {
|
|
84
|
-
lock = JSON.parse(await readFile(path, "utf8"));
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
throw new Error(`Cannot parse existing lock file; refusing to replace it: ${path}`);
|
|
88
|
-
}
|
|
89
|
-
const version = selection.scope === "global" ? 3 : 1;
|
|
90
|
-
if (lock?.version !== version ||
|
|
91
|
-
!lock.skills ||
|
|
92
|
-
typeof lock.skills !== "object" ||
|
|
93
|
-
Array.isArray(lock.skills) ||
|
|
94
|
-
Object.values(lock.skills).some((entry) => !entry || typeof entry !== "object" || Array.isArray(entry))) {
|
|
95
|
-
throw new Error(`Unsupported existing lock file; refusing to replace it: ${path}`);
|
|
96
|
-
}
|
|
97
|
-
return lock.skills;
|
|
98
|
-
}
|
|
99
|
-
export async function createPlan(selection, context) {
|
|
100
|
-
if (!selection.skills.length || !selection.agents.length) {
|
|
101
|
-
throw new Error("A plan needs at least one skill and agent.");
|
|
102
|
-
}
|
|
103
|
-
const names = new Set();
|
|
104
|
-
for (const skill of selection.skills) {
|
|
105
|
-
validateName(skill.name);
|
|
106
|
-
if (names.has(skill.name)) {
|
|
107
|
-
throw new Error(`Multiple selected skills share the name: ${skill.name}`);
|
|
108
|
-
}
|
|
109
|
-
names.add(skill.name);
|
|
110
|
-
}
|
|
111
|
-
const lock = await readLock(selection, context);
|
|
112
|
-
const items = [];
|
|
113
|
-
for (const skill of selection.skills) {
|
|
114
|
-
const paths = targetPaths(skill.name, selection, context);
|
|
115
|
-
const existing = [];
|
|
116
|
-
let blocked;
|
|
117
|
-
for (const path of paths) {
|
|
118
|
-
blocked ??= await unsafeParent(path);
|
|
119
|
-
if (await statOrMissing(path))
|
|
120
|
-
existing.push(path);
|
|
121
|
-
}
|
|
122
|
-
const legacy = legacyPaths(skill.name, selection, context).filter((path) => !paths.includes(path));
|
|
123
|
-
const legacyExisting = [];
|
|
124
|
-
for (const path of legacy) {
|
|
125
|
-
if (await statOrMissing(path))
|
|
126
|
-
legacyExisting.push(path);
|
|
127
|
-
}
|
|
128
|
-
// A stale legacy location can shadow the new canonical copy; never delete it here.
|
|
129
|
-
if (legacyExisting.length) {
|
|
130
|
-
blocked ??= `Existing agent-specific skill needs manual reconciliation: ${legacyExisting.join(", ")}`;
|
|
131
|
-
existing.push(...legacyExisting);
|
|
132
|
-
}
|
|
133
|
-
const previousSource = lock[skill.name]?.source;
|
|
134
|
-
items.push({
|
|
135
|
-
skill,
|
|
136
|
-
paths,
|
|
137
|
-
existing,
|
|
138
|
-
...(typeof previousSource === "string" ? { previousSource } : {}),
|
|
139
|
-
action: blocked
|
|
140
|
-
? "blocked"
|
|
141
|
-
: existing.length
|
|
142
|
-
? selection.overwrite
|
|
143
|
-
? "overwrite"
|
|
144
|
-
: "skip"
|
|
145
|
-
: "install",
|
|
146
|
-
...(blocked
|
|
147
|
-
? { reason: blocked }
|
|
148
|
-
: existing.length && !selection.overwrite
|
|
149
|
-
? {
|
|
150
|
-
reason: "Already exists; use --overwrite to explicitly replace it.",
|
|
151
|
-
}
|
|
152
|
-
: {}),
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
return { selection, context, items };
|
|
156
|
-
}
|
|
157
|
-
export function groupSkills(skills) {
|
|
158
|
-
const groups = new Map();
|
|
159
|
-
for (const skill of skills) {
|
|
160
|
-
const group = groups.get(skill.source) ?? [];
|
|
161
|
-
group.push(skill);
|
|
162
|
-
groups.set(skill.source, group);
|
|
163
|
-
}
|
|
164
|
-
return groups;
|
|
165
|
-
}
|
|
166
|
-
export function buildAddArgs(source, skills, selection) {
|
|
167
|
-
if (!source || source.startsWith("-") || source.includes("\0")) {
|
|
168
|
-
throw new Error("Invalid installer source.");
|
|
169
|
-
}
|
|
170
|
-
return [
|
|
171
|
-
"add",
|
|
172
|
-
source,
|
|
173
|
-
"--skill",
|
|
174
|
-
...skills.map((skill) => skill.name),
|
|
175
|
-
"--agent",
|
|
176
|
-
...selection.agents,
|
|
177
|
-
...(selection.scope === "global" ? ["--global"] : []),
|
|
178
|
-
"--yes",
|
|
179
|
-
"--json",
|
|
180
|
-
"--full-depth",
|
|
181
|
-
];
|
|
182
|
-
}
|
|
183
|
-
export function publicPlan(plan) {
|
|
184
|
-
return {
|
|
185
|
-
scope: plan.selection.scope,
|
|
186
|
-
directory: resolve(plan.selection.scope === "global" ? plan.context.home : plan.context.cwd),
|
|
187
|
-
agents: plan.selection.agents,
|
|
188
|
-
sharedDirectory: ".agents/skills",
|
|
189
|
-
items: plan.items,
|
|
190
|
-
commands: [
|
|
191
|
-
...groupSkills(plan.items
|
|
192
|
-
.filter((item) => item.action === "install" || item.action === "overwrite")
|
|
193
|
-
.map((item) => item.skill)),
|
|
194
|
-
].map(([source, skills]) => [
|
|
195
|
-
"skills",
|
|
196
|
-
...buildAddArgs(source, skills, plan.selection),
|
|
197
|
-
]),
|
|
198
|
-
};
|
|
199
|
-
}
|