@xenonbyte/xsk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/README.zh-CN.md +107 -0
- package/bin/xsk.js +164 -0
- package/lib/adapters/claude.js +14 -0
- package/lib/adapters/codex.js +14 -0
- package/lib/adapters/gemini.js +14 -0
- package/lib/adapters/opencode.js +14 -0
- package/lib/capability.js +126 -0
- package/lib/content-hash.js +13 -0
- package/lib/generator.js +44 -0
- package/lib/input.js +93 -0
- package/lib/install.js +496 -0
- package/lib/manifest.js +288 -0
- package/lib/ownership.js +85 -0
- package/lib/skills.js +58 -0
- package/lib/status.js +171 -0
- package/lib/uninstall.js +577 -0
- package/package.json +36 -0
- package/shared/skill-common.md +6 -0
- package/skills/archive-req/SKILL.md +37 -0
- package/skills/bypass-claude/SKILL.md +53 -0
- package/skills/check/SKILL.md +73 -0
- package/skills/skill-scaffold/SKILL.md +56 -0
- package/skills/think/SKILL.md +56 -0
- package/skills/write-req/SKILL.md +63 -0
- package/templates/fragments/archive-req.behavior.md +7 -0
- package/templates/fragments/archive-req.output.md +1 -0
- package/templates/fragments/archive-req.purpose.md +1 -0
- package/templates/fragments/archive-req.triggers.md +5 -0
- package/templates/fragments/bypass-claude.behavior.md +23 -0
- package/templates/fragments/bypass-claude.output.md +1 -0
- package/templates/fragments/bypass-claude.purpose.md +1 -0
- package/templates/fragments/bypass-claude.triggers.md +5 -0
- package/templates/fragments/check.behavior.md +29 -0
- package/templates/fragments/check.output.md +13 -0
- package/templates/fragments/check.purpose.md +3 -0
- package/templates/fragments/check.triggers.md +5 -0
- package/templates/fragments/skill-scaffold.behavior.md +24 -0
- package/templates/fragments/skill-scaffold.output.md +3 -0
- package/templates/fragments/skill-scaffold.purpose.md +1 -0
- package/templates/fragments/skill-scaffold.triggers.md +5 -0
- package/templates/fragments/think.behavior.md +15 -0
- package/templates/fragments/think.output.md +9 -0
- package/templates/fragments/think.purpose.md +3 -0
- package/templates/fragments/think.triggers.md +6 -0
- package/templates/fragments/write-req.behavior.md +33 -0
- package/templates/fragments/write-req.output.md +1 -0
- package/templates/fragments/write-req.purpose.md +1 -0
- package/templates/fragments/write-req.triggers.md +5 -0
- package/templates/skill.md.tmpl +22 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Xenon Byte
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# xsk
|
|
2
|
+
|
|
3
|
+
Agent skill aggregator. `xsk` curates a small set of agent skills and installs them across Claude Code, Codex, opencode, and Gemini with manifest-backed safety.
|
|
4
|
+
|
|
5
|
+
Package: `@xenonbyte/xsk` · Binary: `xsk` · Runtime: Node >= 20, CommonJS, zero third-party runtime dependencies.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
Two recurring frictions when working across AI coding agents: third-party skill packs are all-or-nothing, and self-authored skills are scattered with no shared install/manifest/safety story. `xsk` solves both. It ships six curated skills (two distilled from third parties, four original) and a CLI that installs each skill into every supported platform's skill directory, then tracks exactly what it created so uninstall removes only those files.
|
|
10
|
+
|
|
11
|
+
`xsk` is itself an agent-skill project and conforms to the same standard its scaffold skill enforces.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install -g @xenonbyte/xsk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires Node >= 20 on macOS or Linux.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
xsk install # install every skill into every platform
|
|
25
|
+
xsk install --platform claude,codex
|
|
26
|
+
xsk status # read-only: what is installed per platform
|
|
27
|
+
xsk status --json
|
|
28
|
+
xsk uninstall # remove only what xsk created
|
|
29
|
+
xsk doctor # probe environment + manifest health
|
|
30
|
+
xsk version
|
|
31
|
+
xsk help
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
| Command | Purpose |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `version` (also `--version` / `-v`) | Print the package version. |
|
|
39
|
+
| `help` (also `--help` / `-h`, and on no args) | Print the user command list. |
|
|
40
|
+
| `install [--platform <list>]` | Generate and install skills, uninstall-first: a reinstall resets prior owned files (pruning skills no longer installed) before regenerating, so no manual `uninstall` is needed; user-edited owned files are still refused and rolled back. `--platform` is comma-separated, defaults to all four platforms. Unknown or duplicate values are rejected. |
|
|
41
|
+
| `uninstall [--platform <list>]` | Remove only the manifest-recorded generated files. |
|
|
42
|
+
| `status [--json]` | Read-only per-platform report: `ok`, `drift`, or `invalid`. Validates manifest shape, not just parse success. |
|
|
43
|
+
| `doctor [--json]` | Read-only probe of Node version, target-dir writability, and manifest validity. Pass/fail per check. No capability claims. |
|
|
44
|
+
|
|
45
|
+
Unknown options fail loud with a non-zero exit.
|
|
46
|
+
|
|
47
|
+
## Skills
|
|
48
|
+
|
|
49
|
+
Six skills, prefixed `xsk-`:
|
|
50
|
+
|
|
51
|
+
| Skill | Purpose |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `xsk-think` | Turn a rough idea into a decision-complete plan before any code is written. Distilled from Waza `/think`. |
|
|
54
|
+
| `xsk-bypass-claude` | Set the current project to Claude Code bypass-permissions mode by writing `.claude/settings.local.json`. Claude only. |
|
|
55
|
+
| `xsk-skill-scaffold` | Bring an agent-skill project up to the `xsk` standard, or refuse if it is not one. |
|
|
56
|
+
| `xsk-write-req` | Convert plain-language needs into a compliant requirement doc in `requirements/`, grounded in the current project. |
|
|
57
|
+
| `xsk-archive-req` | Archive the active requirement doc into `requirements/archive/`. |
|
|
58
|
+
| `xsk-check` | Review a code change before it ships: scope drift, hard stops, evidence-gated findings, then verify and sign off. Distilled from Waza `/check`. |
|
|
59
|
+
|
|
60
|
+
`xsk-bypass-claude` targets Claude Code only; `xsk install` skips it on the other three platforms.
|
|
61
|
+
|
|
62
|
+
## Platforms
|
|
63
|
+
|
|
64
|
+
All four platforms are full and use the same `<name>/SKILL.md` skill-directory shape.
|
|
65
|
+
|
|
66
|
+
| Platform | Install location |
|
|
67
|
+
|---|---|
|
|
68
|
+
| Claude Code | `~/.claude/skills/<name>/SKILL.md` |
|
|
69
|
+
| Codex | `~/.agents/skills/<name>/SKILL.md` |
|
|
70
|
+
| opencode | `~/.config/opencode/skills/<name>/SKILL.md` |
|
|
71
|
+
| Gemini | `~/.gemini/skills/<name>/SKILL.md` |
|
|
72
|
+
|
|
73
|
+
Platform behavior is verified as of 2026-06-25 against the official docs linked from the source repository's `docs/REQUIREMENTS.md`.
|
|
74
|
+
|
|
75
|
+
## Discovery aliases and duplicate skills
|
|
76
|
+
|
|
77
|
+
opencode also reads `~/.claude/skills/<name>/SKILL.md` and `~/.agents/skills/<name>/SKILL.md` in addition to its native `~/.config/opencode/skills/<name>/SKILL.md`. Gemini also reads `~/.agents/skills/<name>/SKILL.md` as an alias for `~/.gemini/skills/<name>/SKILL.md`.
|
|
78
|
+
|
|
79
|
+
Because those aliases are cross-platform visible, a skill copied into one readable alias directory can also be discovered by another agent. `xsk` still writes one manifest-owned copy per selected platform, so each platform copy stays independently uninstallable even when another platform can also see an alias copy.
|
|
80
|
+
|
|
81
|
+
`xsk-bypass-claude` remains Claude Code-only. If another agent discovers it through an alias, the skill body refuses or stops inertly outside Claude Code instead of applying Claude-specific behavior.
|
|
82
|
+
|
|
83
|
+
## Safety
|
|
84
|
+
|
|
85
|
+
Installing into user home config dirs is destructive if careless. `xsk` is manifest-backed:
|
|
86
|
+
|
|
87
|
+
- Owned-only removal. Uninstall removes only paths the manifest recorded.
|
|
88
|
+
- Ownership markers. A `.xsk-owned` marker inside each installed skill dir gates directory removal.
|
|
89
|
+
- No symlink traversal or removal. `xsk` refuses on encounter.
|
|
90
|
+
- Atomic writes. Each file is written to a temp sibling then renamed into place; a failed write restores the original.
|
|
91
|
+
- User edits preserved. If a generated file was user-modified, uninstall keeps it, reports a partial result, and narrows the retained manifest so a later run can finish.
|
|
92
|
+
|
|
93
|
+
`xsk` writes only under `~/.xsk/` and the four platform skill dirs.
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
npm test # full node:test suite
|
|
99
|
+
npm run syntaxcheck # node --check every bin/, lib/, test/ file
|
|
100
|
+
npm pack --dry-run # verify package contents
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The full requirement specification lives in the source repository at `docs/REQUIREMENTS.md`.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# xsk
|
|
2
|
+
|
|
3
|
+
Agent skill aggregator(智能体技能聚合器)。`xsk` 精选了一小组 agent skill,并以 manifest 记录作为安全保障,将它们安装到 Claude Code、Codex、opencode 与 Gemini 四个平台。
|
|
4
|
+
|
|
5
|
+
Package: `@xenonbyte/xsk` · Binary: `xsk` · Runtime: Node >= 20, CommonJS, 零第三方运行时依赖。
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
跨多个 AI coding agent 工作时有两类反复出现的摩擦:第三方 skill 包要么全装要么不装(all-or-nothing),自写的 skill 又散落各处、缺少统一的安装 / manifest / 安全方案。`xsk` 同时解决这两点。它内置六个精选 skill(两个从第三方蒸馏而来,四个原创),并提供一个 CLI,把每个 skill 安装到所有受支持平台的 skill 目录,再精确记录它创建了哪些文件,使 uninstall 只移除这些文件。
|
|
10
|
+
|
|
11
|
+
`xsk` 本身就是一个 agent-skill 项目,并符合它自己的 scaffold skill 所执行的同一套标准。
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install -g @xenonbyte/xsk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
要求 macOS 或 Linux 上的 Node >= 20。
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
xsk install # 把所有 skill 安装到所有平台
|
|
25
|
+
xsk install --platform claude,codex
|
|
26
|
+
xsk status # 只读:查看每个平台已安装的内容
|
|
27
|
+
xsk status --json
|
|
28
|
+
xsk uninstall # 只移除 xsk 创建的内容
|
|
29
|
+
xsk doctor # 探测环境与 manifest 健康状况
|
|
30
|
+
xsk version
|
|
31
|
+
xsk help
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
| Command | Purpose |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `version`(亦作 `--version` / `-v`) | 打印 package version。 |
|
|
39
|
+
| `help`(亦作 `--help` / `-h`,无参数时同样触发) | 打印用户命令清单。 |
|
|
40
|
+
| `install [--platform <list>]` | 生成并安装 skill,默认 uninstall-first:重装会先重置此前 owned 的文件(清理已不再安装的 skill)再生成,无需手动 `uninstall`;用户改过的 owned 文件仍会被拒绝并回滚。`--platform` 以逗号分隔,缺省为全部四个平台;未知或重复的值会被拒绝。 |
|
|
41
|
+
| `uninstall [--platform <list>]` | 仅移除 manifest 记录的生成文件。 |
|
|
42
|
+
| `status [--json]` | 每个平台的只读报告:`ok`、`drift` 或 `invalid`。校验 manifest 形状,而非仅判断能否解析。 |
|
|
43
|
+
| `doctor [--json]` | 对 Node 版本、目标目录可写性、manifest 有效性做只读探测;逐项给出 pass/fail,不声明任何能力。 |
|
|
44
|
+
|
|
45
|
+
未知选项会 fail loud 并以非零码退出。
|
|
46
|
+
|
|
47
|
+
## Skills
|
|
48
|
+
|
|
49
|
+
共六个 skill,统一前缀 `xsk-`:
|
|
50
|
+
|
|
51
|
+
| Skill | Purpose |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `xsk-think` | 把一个粗略想法在写任何代码之前变成 decision-complete 的 plan。蒸馏自 Waza `/think`。 |
|
|
54
|
+
| `xsk-bypass-claude` | 通过写 `.claude/settings.local.json` 把当前项目设为 Claude Code bypass-permissions 模式。仅 Claude。 |
|
|
55
|
+
| `xsk-skill-scaffold` | 把一个 agent-skill 项目带到 `xsk` 标准;若不是这类项目则拒绝。 |
|
|
56
|
+
| `xsk-write-req` | 把白话需求转成 `requirements/` 下合规的需求文档,并扎根于当前项目。 |
|
|
57
|
+
| `xsk-archive-req` | 把当前 active 需求文档归档到 `requirements/archive/`。 |
|
|
58
|
+
| `xsk-check` | 在改动合入前评审:范围漂移、hard stops、证据门控的发现项,再验证后签收。蒸馏自 Waza `/check`。 |
|
|
59
|
+
|
|
60
|
+
`xsk-bypass-claude` 仅面向 Claude Code;`xsk install` 会在其余三个平台跳过它。
|
|
61
|
+
|
|
62
|
+
## Platforms
|
|
63
|
+
|
|
64
|
+
四个平台均为 full,并使用同一种 `<name>/SKILL.md` skill-directory 形态。
|
|
65
|
+
|
|
66
|
+
| Platform | Install location |
|
|
67
|
+
|---|---|
|
|
68
|
+
| Claude Code | `~/.claude/skills/<name>/SKILL.md` |
|
|
69
|
+
| Codex | `~/.agents/skills/<name>/SKILL.md` |
|
|
70
|
+
| opencode | `~/.config/opencode/skills/<name>/SKILL.md` |
|
|
71
|
+
| Gemini | `~/.gemini/skills/<name>/SKILL.md` |
|
|
72
|
+
|
|
73
|
+
平台行为依据源仓库 `docs/REQUIREMENTS.md` 中链接的官方文档,校验日期为 2026-06-25。
|
|
74
|
+
|
|
75
|
+
## Discovery aliases and duplicate skills
|
|
76
|
+
|
|
77
|
+
opencode 除了自己的 `~/.config/opencode/skills/<name>/SKILL.md`,也会读取 `~/.claude/skills/<name>/SKILL.md` 与 `~/.agents/skills/<name>/SKILL.md`。Gemini 也会把 `~/.agents/skills/<name>/SKILL.md` 当作 `~/.gemini/skills/<name>/SKILL.md` 的 alias 一并发现。
|
|
78
|
+
|
|
79
|
+
因为这些 alias 具有 cross-platform visibility,一个放进可读 alias 目录的 skill 也可能被另一个 agent 发现。`xsk` 仍然会按所选平台各写入一份由 manifest 跟踪的 owned 副本,因此即使别的平台也能看见 alias 副本,每个平台自己的副本依然可以 independently uninstallable。
|
|
80
|
+
|
|
81
|
+
`xsk-bypass-claude` 仍然是 Claude Code-only。若别的 agent 通过 alias 发现它,skill body 会在 Claude Code 之外直接 refuse,或 inertly stop,而不会执行 Claude 专属行为。
|
|
82
|
+
|
|
83
|
+
## Safety
|
|
84
|
+
|
|
85
|
+
向用户 home 配置目录写入若不小心具有破坏性。`xsk` 以 manifest 为后盾:
|
|
86
|
+
|
|
87
|
+
- Owned-only removal。uninstall 只移除 manifest 记录的路径。
|
|
88
|
+
- Ownership markers。每个已安装 skill 目录内有 `.xsk-owned` 标记,目录移除需先校验该标记。
|
|
89
|
+
- No symlink traversal or removal。`xsk` 遇到 symlink 会拒绝。
|
|
90
|
+
- Atomic writes。每个文件先写入临时同目录文件,再 rename 就位;写入失败时恢复原文件。
|
|
91
|
+
- User edits preserved。若生成的文件被用户修改,uninstall 会保留它、报告 partial,并收窄保留的 manifest,使后续可继续完成。
|
|
92
|
+
|
|
93
|
+
`xsk` 只在 `~/.xsk/` 与四个平台 skill 目录下写入。
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
npm test # 完整 node:test 套件
|
|
99
|
+
npm run syntaxcheck # 对 bin/、lib/、test/ 下每个文件执行 node --check
|
|
100
|
+
npm pack --dry-run # 校验 package 内容
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
完整需求规格见源仓库中的 `docs/REQUIREMENTS.md`。
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
package/bin/xsk.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { parse } = require('../lib/input');
|
|
5
|
+
const { install } = require('../lib/install');
|
|
6
|
+
const { uninstall } = require('../lib/uninstall');
|
|
7
|
+
const { computeStatus, render: renderStatus } = require('../lib/status');
|
|
8
|
+
const { doctor, render: renderDoctor } = require('../lib/capability');
|
|
9
|
+
|
|
10
|
+
const VERSION = require('../package.json').version;
|
|
11
|
+
|
|
12
|
+
const HELP = `xsk ${VERSION} - agent skill aggregator
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
xsk <command> [options]
|
|
16
|
+
|
|
17
|
+
Commands:
|
|
18
|
+
install [--platform <list>] Generate and install skills (uninstall-first: a
|
|
19
|
+
reinstall resets prior owned files, no manual
|
|
20
|
+
uninstall needed). --platform is comma-separated,
|
|
21
|
+
defaults to all four platforms.
|
|
22
|
+
uninstall [--platform <list>] Remove only the manifest-recorded generated files.
|
|
23
|
+
status [--json] Read-only per-platform report: ok, drift, invalid,
|
|
24
|
+
or not-installed.
|
|
25
|
+
doctor [--json] Probe Node version, target-dir writability, and
|
|
26
|
+
manifest validity. Pass/fail per check.
|
|
27
|
+
version Print the xsk version (also --version / -v).
|
|
28
|
+
help Print this help (also --help / -h, and on no args).
|
|
29
|
+
|
|
30
|
+
Platforms: claude, codex, opencode, gemini.
|
|
31
|
+
|
|
32
|
+
Unknown options fail loud with a non-zero exit.
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
function formatInstall(summary) {
|
|
36
|
+
const lines = [];
|
|
37
|
+
for (const platform of Object.keys(summary.platforms)) {
|
|
38
|
+
const r = summary.platforms[platform];
|
|
39
|
+
if (r.skipped) {
|
|
40
|
+
lines.push(`${platform}: no applicable skills (skipped)`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const skillCount = (r.installed || []).filter((p) => p.endsWith('SKILL.md')).length;
|
|
44
|
+
const backCount = (r.backups || []).length;
|
|
45
|
+
let line = `${platform}: installed ${skillCount} skill${skillCount === 1 ? '' : 's'}`;
|
|
46
|
+
if (backCount > 0) {
|
|
47
|
+
line += `; backed up ${backCount} displaced file${backCount === 1 ? '' : 's'}`;
|
|
48
|
+
}
|
|
49
|
+
lines.push(line);
|
|
50
|
+
}
|
|
51
|
+
return lines.join('\n') + '\n';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function formatUninstall(summary) {
|
|
55
|
+
const lines = [];
|
|
56
|
+
for (const platform of Object.keys(summary.platforms)) {
|
|
57
|
+
const r = summary.platforms[platform];
|
|
58
|
+
if (r.nothingInstalled) {
|
|
59
|
+
lines.push(`${platform}: nothing installed`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (r.invalid) {
|
|
63
|
+
lines.push(`${platform}: manifest invalid - ${r.error}`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const removedCount = (r.removed || []).filter((p) => p.endsWith('SKILL.md')).length;
|
|
67
|
+
const restoredCount = (r.restored || []).length;
|
|
68
|
+
const retainedCount = (r.retained || []).length;
|
|
69
|
+
let line = `${platform}: removed ${removedCount} skill${removedCount === 1 ? '' : 's'}`;
|
|
70
|
+
if (restoredCount > 0) {
|
|
71
|
+
line += `, restored ${restoredCount} displaced file${restoredCount === 1 ? '' : 's'}`;
|
|
72
|
+
}
|
|
73
|
+
if (r.partial) {
|
|
74
|
+
line += `; retained ${retainedCount} user-edited file${retainedCount === 1 ? '' : 's'} (partial)`;
|
|
75
|
+
}
|
|
76
|
+
if ((r.skipped || []).length) {
|
|
77
|
+
line += `; skipped ${(r.skipped || []).length} unowned dir(s)`;
|
|
78
|
+
}
|
|
79
|
+
if ((r.refused || []).length) {
|
|
80
|
+
line += `; refused ${(r.refused || []).length} symlink(s)`;
|
|
81
|
+
}
|
|
82
|
+
if (r.partial && r.error) {
|
|
83
|
+
line += ` - ${r.error}`;
|
|
84
|
+
}
|
|
85
|
+
lines.push(line);
|
|
86
|
+
}
|
|
87
|
+
return lines.join('\n') + '\n';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function main(argv, options) {
|
|
91
|
+
const opts = options || {};
|
|
92
|
+
const out = opts.stdout || process.stdout;
|
|
93
|
+
const err = opts.stderr || process.stderr;
|
|
94
|
+
|
|
95
|
+
let parsed;
|
|
96
|
+
try {
|
|
97
|
+
parsed = parse(argv);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
err.write(`xsk: ${e.message}\n`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const dispatchOptions = {
|
|
104
|
+
platformRoots: opts.platformRoots,
|
|
105
|
+
xskRoot: opts.xskRoot,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
switch (parsed.command) {
|
|
109
|
+
case 'version':
|
|
110
|
+
out.write(`${VERSION}\n`);
|
|
111
|
+
return 0;
|
|
112
|
+
case 'help':
|
|
113
|
+
out.write(HELP);
|
|
114
|
+
return 0;
|
|
115
|
+
case 'install': {
|
|
116
|
+
try {
|
|
117
|
+
const summary = install(
|
|
118
|
+
Object.assign({ platforms: parsed.platforms }, dispatchOptions),
|
|
119
|
+
);
|
|
120
|
+
out.write(formatInstall(summary));
|
|
121
|
+
return 0;
|
|
122
|
+
} catch (e) {
|
|
123
|
+
err.write(`xsk install failed: ${e.message}\n`);
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
case 'uninstall': {
|
|
128
|
+
let summary;
|
|
129
|
+
try {
|
|
130
|
+
summary = uninstall(
|
|
131
|
+
Object.assign({ platforms: parsed.platforms }, dispatchOptions),
|
|
132
|
+
);
|
|
133
|
+
} catch (e) {
|
|
134
|
+
err.write(`xsk uninstall failed: ${e.message}\n`);
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
out.write(formatUninstall(summary));
|
|
138
|
+
return summary.exitCode;
|
|
139
|
+
}
|
|
140
|
+
case 'status': {
|
|
141
|
+
const result = computeStatus(
|
|
142
|
+
Object.assign({ platforms: parsed.platforms }, dispatchOptions),
|
|
143
|
+
);
|
|
144
|
+
out.write(renderStatus(result, { json: parsed.json, platforms: parsed.platforms }) + '\n');
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
case 'doctor': {
|
|
148
|
+
const result = doctor(
|
|
149
|
+
Object.assign({ platforms: parsed.platforms }, dispatchOptions),
|
|
150
|
+
);
|
|
151
|
+
out.write(renderDoctor(result, { json: parsed.json }) + '\n');
|
|
152
|
+
return result.allPass ? 0 : 1;
|
|
153
|
+
}
|
|
154
|
+
default:
|
|
155
|
+
err.write(`xsk: unknown command\n`);
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = { main, HELP, VERSION, formatInstall, formatUninstall };
|
|
161
|
+
|
|
162
|
+
if (require.main === module) {
|
|
163
|
+
process.exit(main(process.argv.slice(2)));
|
|
164
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
|
|
6
|
+
const PLATFORM = 'claude';
|
|
7
|
+
|
|
8
|
+
function skillsRoot(options) {
|
|
9
|
+
const opts = options || {};
|
|
10
|
+
const home = opts.home || os.homedir();
|
|
11
|
+
return path.join(home, '.claude', 'skills');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { PLATFORM, skillsRoot };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
|
|
6
|
+
const PLATFORM = 'codex';
|
|
7
|
+
|
|
8
|
+
function skillsRoot(options) {
|
|
9
|
+
const opts = options || {};
|
|
10
|
+
const home = opts.home || os.homedir();
|
|
11
|
+
return path.join(home, '.agents', 'skills');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { PLATFORM, skillsRoot };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
|
|
6
|
+
const PLATFORM = 'gemini';
|
|
7
|
+
|
|
8
|
+
function skillsRoot(options) {
|
|
9
|
+
const opts = options || {};
|
|
10
|
+
const home = opts.home || os.homedir();
|
|
11
|
+
return path.join(home, '.gemini', 'skills');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { PLATFORM, skillsRoot };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
|
|
6
|
+
const PLATFORM = 'opencode';
|
|
7
|
+
|
|
8
|
+
function skillsRoot(options) {
|
|
9
|
+
const opts = options || {};
|
|
10
|
+
const home = opts.home || os.homedir();
|
|
11
|
+
return path.join(home, '.config', 'opencode', 'skills');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { PLATFORM, skillsRoot };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const { defaultXskRoot, isSafePath } = require('./manifest');
|
|
7
|
+
const { rootFor, ALL_PLATFORMS } = require('./install');
|
|
8
|
+
const { computeStatus } = require('./status');
|
|
9
|
+
|
|
10
|
+
const REQUIRED_NODE_MAJOR = 20;
|
|
11
|
+
|
|
12
|
+
function nodeMajor() {
|
|
13
|
+
return Number.parseInt(String(process.versions.node).split('.')[0], 10) || 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isWritableDir(dir) {
|
|
17
|
+
try {
|
|
18
|
+
if (!isSafePath(dir, 'skill dir')) return false;
|
|
19
|
+
if (fs.existsSync(dir)) {
|
|
20
|
+
const stat = fs.lstatSync(dir);
|
|
21
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return false;
|
|
22
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
let p = dir;
|
|
26
|
+
while (p && p !== path.dirname(p) && !fs.existsSync(p)) {
|
|
27
|
+
p = path.dirname(p);
|
|
28
|
+
}
|
|
29
|
+
if (!p) return false;
|
|
30
|
+
const stat = fs.lstatSync(p);
|
|
31
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return false;
|
|
32
|
+
fs.accessSync(p, fs.constants.W_OK);
|
|
33
|
+
return true;
|
|
34
|
+
} catch (e) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function doctor(options) {
|
|
40
|
+
const opts = options || {};
|
|
41
|
+
const platforms = opts.platforms || ALL_PLATFORMS;
|
|
42
|
+
const xskRoot = opts.xskRoot || defaultXskRoot();
|
|
43
|
+
|
|
44
|
+
const checks = [];
|
|
45
|
+
|
|
46
|
+
checks.push({
|
|
47
|
+
name: 'node-version',
|
|
48
|
+
label: `Node >= ${REQUIRED_NODE_MAJOR}`,
|
|
49
|
+
pass: nodeMajor() >= REQUIRED_NODE_MAJOR,
|
|
50
|
+
detail: `running Node ${process.versions.node}`,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const writableXskRoot = isWritableDir(xskRoot);
|
|
54
|
+
checks.push({
|
|
55
|
+
name: 'writable-xsk-root',
|
|
56
|
+
label: `~/.xsk writable (${xskRoot})`,
|
|
57
|
+
pass: writableXskRoot,
|
|
58
|
+
detail: writableXskRoot ? 'writable or creatable' : 'not writable',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
for (const platform of platforms) {
|
|
62
|
+
let root;
|
|
63
|
+
try {
|
|
64
|
+
root = rootFor(platform, opts.platformRoots);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
checks.push({
|
|
67
|
+
name: `writable-${platform}`,
|
|
68
|
+
label: `${platform} skill dir writable`,
|
|
69
|
+
pass: false,
|
|
70
|
+
detail: `could not resolve skill dir: ${e.message}`,
|
|
71
|
+
});
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
checks.push({
|
|
75
|
+
name: `writable-${platform}`,
|
|
76
|
+
label: `${platform} skill dir writable (${root})`,
|
|
77
|
+
pass: isWritableDir(root),
|
|
78
|
+
detail: isWritableDir(root) ? 'writable or creatable' : 'not writable',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const status = computeStatus({ platforms, xskRoot, platformRoots: opts.platformRoots });
|
|
83
|
+
let checkedManifests = 0;
|
|
84
|
+
let invalidManifests = 0;
|
|
85
|
+
let driftedManifests = 0;
|
|
86
|
+
for (const platform of platforms) {
|
|
87
|
+
const entry = status.platforms[platform];
|
|
88
|
+
if (!entry || entry.state === 'not-installed') continue;
|
|
89
|
+
checkedManifests += 1;
|
|
90
|
+
if (entry.state === 'invalid') {
|
|
91
|
+
invalidManifests += 1;
|
|
92
|
+
} else if (entry.state === 'drift') {
|
|
93
|
+
driftedManifests += 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const manifestPass = invalidManifests === 0 && driftedManifests === 0;
|
|
97
|
+
checks.push({
|
|
98
|
+
name: 'manifest-valid',
|
|
99
|
+
label: 'manifests valid',
|
|
100
|
+
pass: manifestPass,
|
|
101
|
+
detail:
|
|
102
|
+
checkedManifests === 0
|
|
103
|
+
? 'no manifests present'
|
|
104
|
+
: manifestPass
|
|
105
|
+
? `${checkedManifests} manifest(s) valid`
|
|
106
|
+
: `${invalidManifests} invalid, ${driftedManifests} drifted of ${checkedManifests} manifest(s)`,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const allPass = checks.every((c) => c.pass);
|
|
110
|
+
return { checks, allPass };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function render(result, options) {
|
|
114
|
+
const opts = options || {};
|
|
115
|
+
if (opts.json) {
|
|
116
|
+
return JSON.stringify(result, null, 2);
|
|
117
|
+
}
|
|
118
|
+
const lines = [];
|
|
119
|
+
for (const c of result.checks) {
|
|
120
|
+
lines.push(`[${c.pass ? 'PASS' : 'FAIL'}] ${c.label} - ${c.detail}`);
|
|
121
|
+
}
|
|
122
|
+
lines.push(result.allPass ? 'doctor: all checks passed' : 'doctor: one or more checks failed');
|
|
123
|
+
return lines.join('\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { doctor, render, nodeMajor, isWritableDir, REQUIRED_NODE_MAJOR };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
|
|
5
|
+
function contentSha256(content) {
|
|
6
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isSha256(value) {
|
|
10
|
+
return typeof value === 'string' && /^[0-9a-f]{64}$/.test(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = { contentSha256, isSha256 };
|
package/lib/generator.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const PKG_ROOT = path.join(__dirname, '..');
|
|
7
|
+
const SECTIONS = ['purpose', 'triggers', 'behavior', 'output'];
|
|
8
|
+
|
|
9
|
+
function render(template, values) {
|
|
10
|
+
return template.replace(/\{\{(\w+)\}\}/g, (m, k) =>
|
|
11
|
+
Object.prototype.hasOwnProperty.call(values, k) ? String(values[k]) : m,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readTrim(p) {
|
|
16
|
+
return fs.readFileSync(p, 'utf8').replace(/\s+$/, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildSkill(skill, options) {
|
|
20
|
+
const opts = options || {};
|
|
21
|
+
const templatesDir = opts.templatesDir || path.join(PKG_ROOT, 'templates');
|
|
22
|
+
const sharedDir = opts.sharedDir || path.join(PKG_ROOT, 'shared');
|
|
23
|
+
|
|
24
|
+
const values = {
|
|
25
|
+
NAME: skill.name,
|
|
26
|
+
DESCRIPTION: skill.description,
|
|
27
|
+
SHARED: readTrim(path.join(sharedDir, 'skill-common.md')),
|
|
28
|
+
};
|
|
29
|
+
for (const sec of SECTIONS) {
|
|
30
|
+
const fragPath = path.join(templatesDir, 'fragments', `${skill.fragmentBase}.${sec}.md`);
|
|
31
|
+
values[sec.toUpperCase()] = readTrim(fragPath);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const tmpl = fs.readFileSync(path.join(templatesDir, 'skill.md.tmpl'), 'utf8');
|
|
35
|
+
const content = render(tmpl, values);
|
|
36
|
+
return { name: skill.name, description: skill.description, content };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildAll(options) {
|
|
40
|
+
const { skills } = require('./skills');
|
|
41
|
+
return skills.map((s) => buildSkill(s, options));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { render, buildSkill, buildAll, PKG_ROOT, SECTIONS };
|