@xenonbyte/xsk 0.1.0 → 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 +46 -24
- package/README.zh-CN.md +46 -24
- package/bin/xsk.js +10 -1
- package/lib/adapters/opencode.js +7 -1
- package/lib/capability.js +29 -2
- package/lib/install.js +96 -7
- package/lib/manifest.js +21 -1
- package/lib/skills.js +16 -2
- package/lib/status.js +33 -4
- package/lib/uninstall.js +88 -10
- package/package.json +1 -1
- package/skills/archive-req/SKILL.md +11 -6
- package/skills/check/SKILL.md +1 -1
- package/skills/consume-point/SKILL.md +43 -0
- package/skills/point/SKILL.md +68 -0
- package/skills/skill-scaffold/SKILL.md +5 -2
- package/skills/write-req/SKILL.md +14 -7
- package/templates/fragments/archive-req.behavior.md +8 -3
- package/templates/fragments/archive-req.output.md +1 -1
- package/templates/fragments/archive-req.purpose.md +1 -1
- package/templates/fragments/check.behavior.md +1 -1
- package/templates/fragments/consume-point.behavior.md +13 -0
- package/templates/fragments/consume-point.output.md +1 -0
- package/templates/fragments/consume-point.purpose.md +1 -0
- package/templates/fragments/consume-point.triggers.md +5 -0
- package/templates/fragments/point.behavior.md +35 -0
- package/templates/fragments/point.output.md +1 -0
- package/templates/fragments/point.purpose.md +3 -0
- package/templates/fragments/point.triggers.md +6 -0
- package/templates/fragments/skill-scaffold.behavior.md +5 -2
- package/templates/fragments/write-req.behavior.md +11 -4
- package/templates/fragments/write-req.output.md +1 -1
- package/templates/fragments/write-req.purpose.md +1 -1
package/README.md
CHANGED
|
@@ -1,32 +1,48 @@
|
|
|
1
1
|
# xsk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**English** | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> Curate a small set of agent skills and install them across Claude Code, Codex, opencode, and Gemini, with manifest-backed safety.
|
|
6
|
+
|
|
7
|
+
[](https://nodejs.org/)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](package.json)
|
|
10
|
+
|
|
11
|
+
`xsk` (`@xenonbyte/xsk`) is a zero-dependency CLI that installs a curated set of agent skills into every supported AI coding agent, then tracks exactly what it created so uninstall removes only those files.
|
|
6
12
|
|
|
7
13
|
## Overview
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
Working across AI coding agents has two recurring frictions: 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 eight curated skills (two distilled from third parties, six original) and a CLI that installs each skill into every supported platform's skill directory, then records exactly what it created so uninstall removes only those files.
|
|
10
16
|
|
|
11
17
|
`xsk` is itself an agent-skill project and conforms to the same standard its scaffold skill enforces.
|
|
12
18
|
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Eight curated skills**, not an all-or-nothing pack: install everything, or pick per platform.
|
|
22
|
+
- **Four platforms, one shape.** Claude Code, Codex, opencode, and Gemini share a `<name>/SKILL.md` layout; opencode additionally gets directly invocable `/xsk-<name>` commands.
|
|
23
|
+
- **Manifest-backed safety.** Owned-only removal, ownership markers, atomic writes, symlink refusal, and content-hash drift detection.
|
|
24
|
+
- **Uninstall-first installs.** A reinstall resets prior owned files (pruning skills no longer installed) before regenerating, so no manual `uninstall` is needed.
|
|
25
|
+
- **User edits are never clobbered.** A modified owned file is refused and rolled back instead of being overwritten.
|
|
26
|
+
- **Zero runtime dependencies.** Pure Node.js (>= 20), CommonJS.
|
|
27
|
+
|
|
13
28
|
## Installation
|
|
14
29
|
|
|
15
30
|
```sh
|
|
16
31
|
npm install -g @xenonbyte/xsk
|
|
17
32
|
```
|
|
18
33
|
|
|
19
|
-
|
|
34
|
+
> [!IMPORTANT]
|
|
35
|
+
> Requires Node >= 20 on macOS or Linux.
|
|
20
36
|
|
|
21
37
|
## Usage
|
|
22
38
|
|
|
23
39
|
```sh
|
|
24
|
-
xsk install
|
|
40
|
+
xsk install # install every skill into every platform
|
|
25
41
|
xsk install --platform claude,codex
|
|
26
|
-
xsk status
|
|
42
|
+
xsk status # read-only: what is installed per platform
|
|
27
43
|
xsk status --json
|
|
28
|
-
xsk uninstall
|
|
29
|
-
xsk doctor
|
|
44
|
+
xsk uninstall # remove only what xsk created
|
|
45
|
+
xsk doctor # probe environment + manifest health
|
|
30
46
|
xsk version
|
|
31
47
|
xsk help
|
|
32
48
|
```
|
|
@@ -46,18 +62,21 @@ Unknown options fail loud with a non-zero exit.
|
|
|
46
62
|
|
|
47
63
|
## Skills
|
|
48
64
|
|
|
49
|
-
|
|
65
|
+
Eight skills, prefixed `xsk-`:
|
|
50
66
|
|
|
51
67
|
| Skill | Purpose |
|
|
52
68
|
|---|---|
|
|
53
69
|
| `xsk-think` | Turn a rough idea into a decision-complete plan before any code is written. Distilled from Waza `/think`. |
|
|
54
70
|
| `xsk-bypass-claude` | Set the current project to Claude Code bypass-permissions mode by writing `.claude/settings.local.json`. Claude only. |
|
|
55
71
|
| `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
|
|
57
|
-
| `xsk-archive-req` | Archive the active requirement doc into
|
|
72
|
+
| `xsk-write-req` | Convert plain-language needs into a compliant requirement doc in `.xsk/requirements/`, grounded in the current project. |
|
|
73
|
+
| `xsk-archive-req` | Archive the active requirement doc into `.xsk/requirements/archive/`. |
|
|
58
74
|
| `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`. |
|
|
75
|
+
| `xsk-point` | Research one aspect of the current project to a decision-complete landed plan and persist it as a point document in `.xsk/points/`. |
|
|
76
|
+
| `xsk-consume-point` | Fold selected `.xsk/points/` documents into one `.xsk/requirements/` doc via `xsk-write-req`, archiving consumed points write-before-remove. |
|
|
59
77
|
|
|
60
|
-
|
|
78
|
+
> [!NOTE]
|
|
79
|
+
> `xsk-bypass-claude` targets Claude Code only; `xsk install` skips it on the other three platforms.
|
|
61
80
|
|
|
62
81
|
## Platforms
|
|
63
82
|
|
|
@@ -67,10 +86,12 @@ All four platforms are full and use the same `<name>/SKILL.md` skill-directory s
|
|
|
67
86
|
|---|---|
|
|
68
87
|
| Claude Code | `~/.claude/skills/<name>/SKILL.md` |
|
|
69
88
|
| Codex | `~/.agents/skills/<name>/SKILL.md` |
|
|
70
|
-
| opencode | `~/.config/opencode/skills/<name>/SKILL.md` |
|
|
89
|
+
| opencode | `~/.config/opencode/skills/<name>/SKILL.md` (skill) and `~/.config/opencode/commands/xsk-<name>.md` (command) |
|
|
71
90
|
| Gemini | `~/.gemini/skills/<name>/SKILL.md` |
|
|
72
91
|
|
|
73
|
-
|
|
92
|
+
For opencode, `xsk install` writes both a skill directory entry and a flat `commands/xsk-<name>.md` command file for each installed skill, making each skill directly invocable as an opencode `/xsk-<name>` command. Both the skill and the command file are manifest-tracked, and uninstall removes them together.
|
|
93
|
+
|
|
94
|
+
Platform behavior was verified as of 2026-06-25 against the official docs linked from the source repository's requirement spec under `docs/`.
|
|
74
95
|
|
|
75
96
|
## Discovery aliases and duplicate skills
|
|
76
97
|
|
|
@@ -82,15 +103,16 @@ Because those aliases are cross-platform visible, a skill copied into one readab
|
|
|
82
103
|
|
|
83
104
|
## Safety
|
|
84
105
|
|
|
85
|
-
|
|
106
|
+
> [!WARNING]
|
|
107
|
+
> Installing into user home config dirs is destructive if careless. `xsk` is manifest-backed so every write is owned and reversible.
|
|
86
108
|
|
|
87
|
-
- Owned-only removal
|
|
88
|
-
- Ownership markers
|
|
89
|
-
- No symlink traversal or removal
|
|
90
|
-
- Atomic writes
|
|
91
|
-
- User edits preserved
|
|
109
|
+
- **Owned-only removal.** Uninstall removes only paths the manifest recorded.
|
|
110
|
+
- **Ownership markers.** A `.xsk-owned` marker inside each installed skill dir gates directory removal.
|
|
111
|
+
- **No symlink traversal or removal.** `xsk` refuses on encounter.
|
|
112
|
+
- **Atomic writes.** Each file is written to a temp sibling then renamed into place; a failed write restores the original.
|
|
113
|
+
- **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
114
|
|
|
93
|
-
`xsk` writes only under `~/.xsk
|
|
115
|
+
`xsk` writes only under `~/.xsk/`, the four platform skill dirs, and opencode's `~/.config/opencode/commands/` command dir.
|
|
94
116
|
|
|
95
117
|
## Development
|
|
96
118
|
|
|
@@ -100,8 +122,8 @@ npm run syntaxcheck # node --check every bin/, lib/, test/ file
|
|
|
100
122
|
npm pack --dry-run # verify package contents
|
|
101
123
|
```
|
|
102
124
|
|
|
103
|
-
The full requirement specification lives in the source repository
|
|
125
|
+
The full requirement specification lives in the source repository under `docs/`.
|
|
104
126
|
|
|
105
|
-
|
|
127
|
+
---
|
|
106
128
|
|
|
107
|
-
MIT
|
|
129
|
+
MIT licensed. See [LICENSE](LICENSE).
|
package/README.zh-CN.md
CHANGED
|
@@ -1,32 +1,48 @@
|
|
|
1
1
|
# xsk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](README.md) | **简体中文**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> 精选一小组 agent skill,以 manifest 记录作为安全保障,安装到 Claude Code、Codex、opencode 与 Gemini。
|
|
6
|
+
|
|
7
|
+
[](https://nodejs.org/)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](package.json)
|
|
10
|
+
|
|
11
|
+
`xsk`(`@xenonbyte/xsk`)是一个零依赖 CLI,把一组精选的 agent skill 安装到每个受支持的 AI coding agent,并精确记录它创建了哪些文件,使 uninstall 只移除这些文件。
|
|
6
12
|
|
|
7
13
|
## Overview
|
|
8
14
|
|
|
9
|
-
跨多个 AI coding agent 工作时有两类反复出现的摩擦:第三方 skill 包要么全装要么不装(all-or-nothing),自写的 skill 又散落各处、缺少统一的安装 / manifest / 安全方案。`xsk`
|
|
15
|
+
跨多个 AI coding agent 工作时有两类反复出现的摩擦:第三方 skill 包要么全装要么不装(all-or-nothing),自写的 skill 又散落各处、缺少统一的安装 / manifest / 安全方案。`xsk` 同时解决这两点。它内置八个精选 skill(两个从第三方蒸馏而来,六个原创),并提供一个 CLI,把每个 skill 安装到所有受支持平台的 skill 目录,再精确记录它创建了哪些文件,使 uninstall 只移除这些文件。
|
|
10
16
|
|
|
11
17
|
`xsk` 本身就是一个 agent-skill 项目,并符合它自己的 scaffold skill 所执行的同一套标准。
|
|
12
18
|
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **八个精选 skill**,不是全装或不装的整包,可全部安装,也可按平台挑选。
|
|
22
|
+
- **四个平台,同一形态。** Claude Code、Codex、opencode 与 Gemini 共用 `<name>/SKILL.md` 布局;opencode 还额外获得可直接调用的 `/xsk-<name>` 命令。
|
|
23
|
+
- **manifest 为后盾的安全。** owned-only removal、ownership markers、atomic writes、symlink refusal,以及 content-hash 漂移检测。
|
|
24
|
+
- **uninstall-first 安装。** 重装会先重置此前 owned 的文件(清理已不再安装的 skill)再生成,无需手动 `uninstall`。
|
|
25
|
+
- **绝不覆盖用户改动。** 被改过的 owned 文件会被拒绝并回滚,而不会被覆盖。
|
|
26
|
+
- **零运行时依赖。** 纯 Node.js(>= 20),CommonJS。
|
|
27
|
+
|
|
13
28
|
## Installation
|
|
14
29
|
|
|
15
30
|
```sh
|
|
16
31
|
npm install -g @xenonbyte/xsk
|
|
17
32
|
```
|
|
18
33
|
|
|
19
|
-
|
|
34
|
+
> [!IMPORTANT]
|
|
35
|
+
> 要求 macOS 或 Linux 上的 Node >= 20。
|
|
20
36
|
|
|
21
37
|
## Usage
|
|
22
38
|
|
|
23
39
|
```sh
|
|
24
|
-
xsk install
|
|
40
|
+
xsk install # 把所有 skill 安装到所有平台
|
|
25
41
|
xsk install --platform claude,codex
|
|
26
|
-
xsk status
|
|
42
|
+
xsk status # 只读:查看每个平台已安装的内容
|
|
27
43
|
xsk status --json
|
|
28
|
-
xsk uninstall
|
|
29
|
-
xsk doctor
|
|
44
|
+
xsk uninstall # 只移除 xsk 创建的内容
|
|
45
|
+
xsk doctor # 探测环境与 manifest 健康状况
|
|
30
46
|
xsk version
|
|
31
47
|
xsk help
|
|
32
48
|
```
|
|
@@ -46,18 +62,21 @@ xsk help
|
|
|
46
62
|
|
|
47
63
|
## Skills
|
|
48
64
|
|
|
49
|
-
|
|
65
|
+
共八个 skill,统一前缀 `xsk-`:
|
|
50
66
|
|
|
51
67
|
| Skill | Purpose |
|
|
52
68
|
|---|---|
|
|
53
69
|
| `xsk-think` | 把一个粗略想法在写任何代码之前变成 decision-complete 的 plan。蒸馏自 Waza `/think`。 |
|
|
54
70
|
| `xsk-bypass-claude` | 通过写 `.claude/settings.local.json` 把当前项目设为 Claude Code bypass-permissions 模式。仅 Claude。 |
|
|
55
71
|
| `xsk-skill-scaffold` | 把一个 agent-skill 项目带到 `xsk` 标准;若不是这类项目则拒绝。 |
|
|
56
|
-
| `xsk-write-req` | 把白话需求转成
|
|
57
|
-
| `xsk-archive-req` | 把当前 active 需求文档归档到
|
|
72
|
+
| `xsk-write-req` | 把白话需求转成 `.xsk/requirements/` 下合规的需求文档,并扎根于当前项目。 |
|
|
73
|
+
| `xsk-archive-req` | 把当前 active 需求文档归档到 `.xsk/requirements/archive/`。 |
|
|
58
74
|
| `xsk-check` | 在改动合入前评审:范围漂移、hard stops、证据门控的发现项,再验证后签收。蒸馏自 Waza `/check`。 |
|
|
75
|
+
| `xsk-point` | 把当前项目某一方面研究到 decision-complete 的落地方案,并作为 point 文档持久化到 `.xsk/points/`。 |
|
|
76
|
+
| `xsk-consume-point` | 通过 `xsk-write-req` 把选定的 `.xsk/points/` 文档折叠进一份 `.xsk/requirements/` 文档,以 write-before-remove 方式归档已消费的 point。 |
|
|
59
77
|
|
|
60
|
-
|
|
78
|
+
> [!NOTE]
|
|
79
|
+
> `xsk-bypass-claude` 仅面向 Claude Code;`xsk install` 会在其余三个平台跳过它。
|
|
61
80
|
|
|
62
81
|
## Platforms
|
|
63
82
|
|
|
@@ -67,10 +86,12 @@ xsk help
|
|
|
67
86
|
|---|---|
|
|
68
87
|
| Claude Code | `~/.claude/skills/<name>/SKILL.md` |
|
|
69
88
|
| Codex | `~/.agents/skills/<name>/SKILL.md` |
|
|
70
|
-
| opencode | `~/.config/opencode/skills/<name>/SKILL.md
|
|
89
|
+
| opencode | `~/.config/opencode/skills/<name>/SKILL.md`(skill)和 `~/.config/opencode/commands/xsk-<name>.md`(command) |
|
|
71
90
|
| Gemini | `~/.gemini/skills/<name>/SKILL.md` |
|
|
72
91
|
|
|
73
|
-
|
|
92
|
+
对于 opencode,`xsk install` 会为每个已安装的 skill 同时写入 skill 目录条目和平坦的 `commands/xsk-<name>.md` 命令文件,使每个 skill 都可作为 opencode `/xsk-<name>` 命令直接调用。skill 与命令文件均由 manifest 跟踪,卸载时一并移除。
|
|
93
|
+
|
|
94
|
+
平台行为依据源仓库 `docs/` 下需求规格中链接的官方文档,校验日期为 2026-06-25。
|
|
74
95
|
|
|
75
96
|
## Discovery aliases and duplicate skills
|
|
76
97
|
|
|
@@ -82,15 +103,16 @@ opencode 除了自己的 `~/.config/opencode/skills/<name>/SKILL.md`,也会读
|
|
|
82
103
|
|
|
83
104
|
## Safety
|
|
84
105
|
|
|
85
|
-
|
|
106
|
+
> [!WARNING]
|
|
107
|
+
> 向用户 home 配置目录写入若不小心具有破坏性。`xsk` 以 manifest 为后盾,使每次写入都是 owned 且可逆的。
|
|
86
108
|
|
|
87
|
-
- Owned-only removal
|
|
88
|
-
- Ownership markers
|
|
89
|
-
- No symlink traversal or removal
|
|
90
|
-
- Atomic writes
|
|
91
|
-
- User edits preserved
|
|
109
|
+
- **Owned-only removal。** uninstall 只移除 manifest 记录的路径。
|
|
110
|
+
- **Ownership markers。** 每个已安装 skill 目录内有 `.xsk-owned` 标记,目录移除需先校验该标记。
|
|
111
|
+
- **No symlink traversal or removal。** `xsk` 遇到 symlink 会拒绝。
|
|
112
|
+
- **Atomic writes。** 每个文件先写入临时同目录文件,再 rename 就位;写入失败时恢复原文件。
|
|
113
|
+
- **User edits preserved。** 若生成的文件被用户修改,uninstall 会保留它、报告 partial,并收窄保留的 manifest,使后续可继续完成。
|
|
92
114
|
|
|
93
|
-
`xsk` 只在 `~/.xsk/`
|
|
115
|
+
`xsk` 只在 `~/.xsk/`、四个平台 skill 目录,以及 opencode 的 `~/.config/opencode/commands/` command 目录下写入。
|
|
94
116
|
|
|
95
117
|
## Development
|
|
96
118
|
|
|
@@ -100,8 +122,8 @@ npm run syntaxcheck # 对 bin/、lib/、test/ 下每个文件执行 node --check
|
|
|
100
122
|
npm pack --dry-run # 校验 package 内容
|
|
101
123
|
```
|
|
102
124
|
|
|
103
|
-
|
|
125
|
+
完整需求规格见源仓库 `docs/` 目录。
|
|
104
126
|
|
|
105
|
-
|
|
127
|
+
---
|
|
106
128
|
|
|
107
|
-
MIT
|
|
129
|
+
基于 MIT 许可证发布。见 [LICENSE](LICENSE)。
|
package/bin/xsk.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const { parse } = require('../lib/input');
|
|
5
|
-
const { install } = require('../lib/install');
|
|
5
|
+
const { install, isCommandFilePath } = require('../lib/install');
|
|
6
6
|
const { uninstall } = require('../lib/uninstall');
|
|
7
7
|
const { computeStatus, render: renderStatus } = require('../lib/status');
|
|
8
8
|
const { doctor, render: renderDoctor } = require('../lib/capability');
|
|
@@ -41,8 +41,12 @@ function formatInstall(summary) {
|
|
|
41
41
|
continue;
|
|
42
42
|
}
|
|
43
43
|
const skillCount = (r.installed || []).filter((p) => p.endsWith('SKILL.md')).length;
|
|
44
|
+
const commandCount = (r.installed || []).filter((p) => isCommandFilePath(p)).length;
|
|
44
45
|
const backCount = (r.backups || []).length;
|
|
45
46
|
let line = `${platform}: installed ${skillCount} skill${skillCount === 1 ? '' : 's'}`;
|
|
47
|
+
if (commandCount > 0) {
|
|
48
|
+
line += `; installed ${commandCount} command${commandCount === 1 ? '' : 's'}`;
|
|
49
|
+
}
|
|
46
50
|
if (backCount > 0) {
|
|
47
51
|
line += `; backed up ${backCount} displaced file${backCount === 1 ? '' : 's'}`;
|
|
48
52
|
}
|
|
@@ -64,9 +68,13 @@ function formatUninstall(summary) {
|
|
|
64
68
|
continue;
|
|
65
69
|
}
|
|
66
70
|
const removedCount = (r.removed || []).filter((p) => p.endsWith('SKILL.md')).length;
|
|
71
|
+
const removedCommandCount = (r.removed || []).filter((p) => isCommandFilePath(p)).length;
|
|
67
72
|
const restoredCount = (r.restored || []).length;
|
|
68
73
|
const retainedCount = (r.retained || []).length;
|
|
69
74
|
let line = `${platform}: removed ${removedCount} skill${removedCount === 1 ? '' : 's'}`;
|
|
75
|
+
if (removedCommandCount > 0) {
|
|
76
|
+
line += `; removed ${removedCommandCount} command${removedCommandCount === 1 ? '' : 's'}`;
|
|
77
|
+
}
|
|
70
78
|
if (restoredCount > 0) {
|
|
71
79
|
line += `, restored ${restoredCount} displaced file${restoredCount === 1 ? '' : 's'}`;
|
|
72
80
|
}
|
|
@@ -102,6 +110,7 @@ function main(argv, options) {
|
|
|
102
110
|
|
|
103
111
|
const dispatchOptions = {
|
|
104
112
|
platformRoots: opts.platformRoots,
|
|
113
|
+
platformCommandsRoots: opts.platformCommandsRoots,
|
|
105
114
|
xskRoot: opts.xskRoot,
|
|
106
115
|
};
|
|
107
116
|
|
package/lib/adapters/opencode.js
CHANGED
|
@@ -11,4 +11,10 @@ function skillsRoot(options) {
|
|
|
11
11
|
return path.join(home, '.config', 'opencode', 'skills');
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
function commandsRoot(options) {
|
|
15
|
+
const opts = options || {};
|
|
16
|
+
const home = opts.home || os.homedir();
|
|
17
|
+
return path.join(home, '.config', 'opencode', 'commands');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { PLATFORM, skillsRoot, commandsRoot };
|
package/lib/capability.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
|
|
6
6
|
const { defaultXskRoot, isSafePath } = require('./manifest');
|
|
7
|
-
const { rootFor, ALL_PLATFORMS } = require('./install');
|
|
7
|
+
const { rootFor, commandsRootFor, ALL_PLATFORMS } = require('./install');
|
|
8
8
|
const { computeStatus } = require('./status');
|
|
9
9
|
|
|
10
10
|
const REQUIRED_NODE_MAJOR = 20;
|
|
@@ -77,9 +77,36 @@ function doctor(options) {
|
|
|
77
77
|
pass: isWritableDir(root),
|
|
78
78
|
detail: isWritableDir(root) ? 'writable or creatable' : 'not writable',
|
|
79
79
|
});
|
|
80
|
+
|
|
81
|
+
let commandsRoot;
|
|
82
|
+
try {
|
|
83
|
+
commandsRoot = commandsRootFor(platform, opts.platformCommandsRoots);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
checks.push({
|
|
86
|
+
name: `writable-${platform}-commands`,
|
|
87
|
+
label: `${platform} commands dir writable`,
|
|
88
|
+
pass: false,
|
|
89
|
+
detail: `could not resolve commands dir: ${e.message}`,
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (commandsRoot) {
|
|
94
|
+
const writableCommandsRoot = isWritableDir(commandsRoot);
|
|
95
|
+
checks.push({
|
|
96
|
+
name: `writable-${platform}-commands`,
|
|
97
|
+
label: `${platform} commands dir writable (${commandsRoot})`,
|
|
98
|
+
pass: writableCommandsRoot,
|
|
99
|
+
detail: writableCommandsRoot ? 'writable or creatable' : 'not writable',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
80
102
|
}
|
|
81
103
|
|
|
82
|
-
const status = computeStatus({
|
|
104
|
+
const status = computeStatus({
|
|
105
|
+
platforms,
|
|
106
|
+
xskRoot,
|
|
107
|
+
platformRoots: opts.platformRoots,
|
|
108
|
+
platformCommandsRoots: opts.platformCommandsRoots,
|
|
109
|
+
});
|
|
83
110
|
let checkedManifests = 0;
|
|
84
111
|
let invalidManifests = 0;
|
|
85
112
|
let driftedManifests = 0;
|
package/lib/install.js
CHANGED
|
@@ -29,6 +29,42 @@ function rootFor(platform, platformRoots) {
|
|
|
29
29
|
return adapter.skillsRoot();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function commandsRootFor(platform, platformCommandsRoots) {
|
|
33
|
+
if (platformCommandsRoots && platformCommandsRoots[platform]) {
|
|
34
|
+
return platformCommandsRoots[platform];
|
|
35
|
+
}
|
|
36
|
+
const adapter = require(`./adapters/${platform}.js`);
|
|
37
|
+
return typeof adapter.commandsRoot === 'function' ? adapter.commandsRoot() : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// A command file is a flat, markerless `.md` recorded in installed_paths whose
|
|
41
|
+
// basename is not the skill file (`SKILL.md`) or the ownership marker. The
|
|
42
|
+
// function set is basename-only in places, so ownership/classification keys on
|
|
43
|
+
// the basename; membership under commandsRoot is the operational-semantics check.
|
|
44
|
+
function isCommandFilePath(p) {
|
|
45
|
+
const base = path.basename(p);
|
|
46
|
+
return base !== 'SKILL.md' && base !== MARKER && base.endsWith('.md');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// opencode command body: the generated skill content (already carrying the
|
|
50
|
+
// `description:` frontmatter) plus, when the content lacks `$ARGUMENTS`, a
|
|
51
|
+
// skill-referring invocation section ending in a fenced `$ARGUMENTS` placeholder.
|
|
52
|
+
function buildCommandFileContent(skillContent) {
|
|
53
|
+
if (skillContent.includes('$ARGUMENTS')) {
|
|
54
|
+
return skillContent;
|
|
55
|
+
}
|
|
56
|
+
const prefix = skillContent.endsWith('\n') ? skillContent : `${skillContent}\n`;
|
|
57
|
+
return (
|
|
58
|
+
`${prefix}\n` +
|
|
59
|
+
'## opencode invocation arguments\n\n' +
|
|
60
|
+
'Use these arguments when running the skill above:\n\n' +
|
|
61
|
+
'```text\n' +
|
|
62
|
+
'$ARGUMENTS\n' +
|
|
63
|
+
'```\n\n' +
|
|
64
|
+
'If no arguments were supplied, follow the default usage.\n'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
32
68
|
function writeGeneratedFile(targetPath, content, label) {
|
|
33
69
|
atomicWriteFile(targetPath, content, {
|
|
34
70
|
dirLabel: `${label} dir`,
|
|
@@ -79,6 +115,7 @@ function previousInstallState(platform, xskRoot) {
|
|
|
79
115
|
backupsByTarget: new Map(),
|
|
80
116
|
installedDirs: new Set(),
|
|
81
117
|
installedFiles: new Set(),
|
|
118
|
+
installedCommands: new Set(),
|
|
82
119
|
installedHashesByTarget: new Map(),
|
|
83
120
|
};
|
|
84
121
|
let previous;
|
|
@@ -96,10 +133,11 @@ function previousInstallState(platform, xskRoot) {
|
|
|
96
133
|
if (!validate(previous, { expectedPlatform: platform })) {
|
|
97
134
|
throw new Error(`manifest for ${platform} failed shape validation; refusing to install`);
|
|
98
135
|
}
|
|
136
|
+
const installedCommands = new Set(previous.installed_paths.filter(isCommandFilePath));
|
|
99
137
|
const installedDirs = new Set(
|
|
100
138
|
previous.installed_paths.filter((p) => {
|
|
101
139
|
const base = path.basename(p);
|
|
102
|
-
return base !== 'SKILL.md' && base !== MARKER;
|
|
140
|
+
return base !== 'SKILL.md' && base !== MARKER && !isCommandFilePath(p);
|
|
103
141
|
}),
|
|
104
142
|
);
|
|
105
143
|
const installedFiles = new Set(
|
|
@@ -110,6 +148,7 @@ function previousInstallState(platform, xskRoot) {
|
|
|
110
148
|
backupsByTarget: new Map(previous.backups.map((b) => [b.target, { target: b.target, backup: b.backup }])),
|
|
111
149
|
installedDirs,
|
|
112
150
|
installedFiles,
|
|
151
|
+
installedCommands,
|
|
113
152
|
installedHashesByTarget: new Map(installedHashes.map((h) => [h.target, h.sha256])),
|
|
114
153
|
};
|
|
115
154
|
}
|
|
@@ -232,7 +271,7 @@ function restorePathState(snapshot) {
|
|
|
232
271
|
}
|
|
233
272
|
}
|
|
234
273
|
|
|
235
|
-
function capturePlatformSnapshot({ platform, skillsRoot, xskRoot, skills }) {
|
|
274
|
+
function capturePlatformSnapshot({ platform, skillsRoot, commandsRoot, xskRoot, skills }) {
|
|
236
275
|
const seen = new Set();
|
|
237
276
|
const paths = [];
|
|
238
277
|
const platformBackupDir = path.join(xskRoot, 'install', 'backups', platform);
|
|
@@ -256,6 +295,12 @@ function capturePlatformSnapshot({ platform, skillsRoot, xskRoot, skills }) {
|
|
|
256
295
|
add(path.join(skillDir, MARKER));
|
|
257
296
|
add(path.join(xskRoot, 'install', 'backups', platform, `${skill.name}.SKILL.md.bak`));
|
|
258
297
|
}
|
|
298
|
+
if (commandsRoot) {
|
|
299
|
+
add(commandsRoot);
|
|
300
|
+
for (const skill of skills) {
|
|
301
|
+
add(path.join(commandsRoot, `${skill.name}.md`));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
259
304
|
// Cover the prior manifest's owned paths too, so an uninstall-first reset of
|
|
260
305
|
// skills no longer in the install set can still be rolled back on failure.
|
|
261
306
|
let prior = null;
|
|
@@ -267,12 +312,14 @@ function capturePlatformSnapshot({ platform, skillsRoot, xskRoot, skills }) {
|
|
|
267
312
|
if (prior && typeof prior === 'object') {
|
|
268
313
|
const priorPaths = Array.isArray(prior.installed_paths) ? prior.installed_paths : [];
|
|
269
314
|
for (const p of priorPaths) {
|
|
270
|
-
|
|
315
|
+
const insideSkills = isInsideDir(p, skillsRoot);
|
|
316
|
+
const insideCommands = commandsRoot ? isInsideDir(p, commandsRoot) : false;
|
|
317
|
+
if (!insideSkills && !insideCommands) {
|
|
271
318
|
continue;
|
|
272
319
|
}
|
|
273
320
|
add(p);
|
|
274
321
|
const base = path.basename(p);
|
|
275
|
-
if (base === 'SKILL.md' || base === MARKER) {
|
|
322
|
+
if (insideSkills && (base === 'SKILL.md' || base === MARKER)) {
|
|
276
323
|
add(path.dirname(p));
|
|
277
324
|
}
|
|
278
325
|
}
|
|
@@ -299,7 +346,7 @@ function restorePlatformSnapshot(snapshot) {
|
|
|
299
346
|
}
|
|
300
347
|
}
|
|
301
348
|
|
|
302
|
-
function installPlatform({ platform, skillsRoot, xskRoot, version, skills }) {
|
|
349
|
+
function installPlatform({ platform, skillsRoot, commandsRoot, xskRoot, version, skills }) {
|
|
303
350
|
const manifest = create(platform, version);
|
|
304
351
|
const installed = [];
|
|
305
352
|
const backups = [];
|
|
@@ -314,6 +361,13 @@ function installPlatform({ platform, skillsRoot, xskRoot, version, skills }) {
|
|
|
314
361
|
|
|
315
362
|
try {
|
|
316
363
|
ensurePlatformRoot(skillsRoot);
|
|
364
|
+
if (commandsRoot) {
|
|
365
|
+
const commandsRootExisted = fs.existsSync(commandsRoot);
|
|
366
|
+
ensurePlatformRoot(commandsRoot);
|
|
367
|
+
if (!commandsRootExisted) {
|
|
368
|
+
createdThisRun.push(commandsRoot);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
317
371
|
for (const skill of skills) {
|
|
318
372
|
const built = buildSkill(skill);
|
|
319
373
|
const skillDir = path.join(skillsRoot, skill.name);
|
|
@@ -419,6 +473,36 @@ function installPlatform({ platform, skillsRoot, xskRoot, version, skills }) {
|
|
|
419
473
|
assertReusableBackupRecord(backup, backupDir);
|
|
420
474
|
backups.push({ target: backup.target, backup: backup.backup });
|
|
421
475
|
}
|
|
476
|
+
|
|
477
|
+
if (commandsRoot) {
|
|
478
|
+
const commandFile = path.join(commandsRoot, `${skill.name}.md`);
|
|
479
|
+
const commandContent = buildCommandFileContent(built.content);
|
|
480
|
+
assertNotSymlink(commandFile, 'command file');
|
|
481
|
+
const commandFileExisted = fs.existsSync(commandFile);
|
|
482
|
+
if (commandFileExisted) {
|
|
483
|
+
const stat = fs.statSync(commandFile);
|
|
484
|
+
const existingCommandContent = fs.readFileSync(commandFile);
|
|
485
|
+
// Command files carry no ownership marker, so adoption rests solely on
|
|
486
|
+
// a hash match against the prior manifest: adopt a previously-installed
|
|
487
|
+
// generated command file, refuse a user-edited or unrecorded one.
|
|
488
|
+
const adoptable = isPreviouslyInstalledGeneratedContent({
|
|
489
|
+
existingContent: existingCommandContent,
|
|
490
|
+
builtContent: commandContent,
|
|
491
|
+
previousHash: installedHashesByTarget.get(commandFile),
|
|
492
|
+
wasInstalled: previousState.installedCommands.has(commandFile),
|
|
493
|
+
});
|
|
494
|
+
if (!adoptable) {
|
|
495
|
+
throw new Error(`refusing to overwrite user-edited command file: ${commandFile}`);
|
|
496
|
+
}
|
|
497
|
+
overwrittenThisRun.push({ target: commandFile, content: existingCommandContent, mode: stat.mode });
|
|
498
|
+
}
|
|
499
|
+
writeGeneratedFile(commandFile, commandContent, 'command file');
|
|
500
|
+
if (!commandFileExisted) {
|
|
501
|
+
createdThisRun.push(commandFile);
|
|
502
|
+
}
|
|
503
|
+
installed.push(commandFile);
|
|
504
|
+
installedHashes.push({ target: commandFile, sha256: contentSha256(commandContent) });
|
|
505
|
+
}
|
|
422
506
|
}
|
|
423
507
|
|
|
424
508
|
manifest.installed_paths = installed;
|
|
@@ -444,6 +528,7 @@ function install(options) {
|
|
|
444
528
|
try {
|
|
445
529
|
for (const platform of selectedPlatforms) {
|
|
446
530
|
const skillsRoot = rootFor(platform, opts.platformRoots);
|
|
531
|
+
const commandsRoot = commandsRootFor(platform, opts.platformCommandsRoots);
|
|
447
532
|
const applicable = skillsToInstall.filter((s) => s.platforms.includes(platform));
|
|
448
533
|
if (applicable.length === 0) {
|
|
449
534
|
summary.platforms[platform] = { platform, installed: [], backups: [], manifest: null, skipped: true };
|
|
@@ -452,7 +537,7 @@ function install(options) {
|
|
|
452
537
|
// Snapshot the full platform state (including the prior manifest's owned
|
|
453
538
|
// paths) before any mutation, so the uninstall-first reset and the
|
|
454
539
|
// install roll back together on any failure.
|
|
455
|
-
const snapshot = capturePlatformSnapshot({ platform, skillsRoot, xskRoot, skills: applicable });
|
|
540
|
+
const snapshot = capturePlatformSnapshot({ platform, skillsRoot, commandsRoot, xskRoot, skills: applicable });
|
|
456
541
|
snapshots.push(snapshot);
|
|
457
542
|
|
|
458
543
|
// Uninstall-first: remove the prior owned state before regenerating, so a
|
|
@@ -463,7 +548,7 @@ function install(options) {
|
|
|
463
548
|
// retained skill in place -- re-adopting a marker-less generated file, or
|
|
464
549
|
// refusing to overwrite a user-edited owned skill (which rolls back). An
|
|
465
550
|
// invalid prior manifest is the one case we refuse outright.
|
|
466
|
-
const reset = uninstallPlatform({ platform, xskRoot, skillsRoot });
|
|
551
|
+
const reset = uninstallPlatform({ platform, xskRoot, skillsRoot, commandsRoot });
|
|
467
552
|
if (reset.invalid) {
|
|
468
553
|
throw new Error(`refusing to reinstall ${platform}: existing manifest is invalid; uninstall or fix it first`);
|
|
469
554
|
}
|
|
@@ -471,6 +556,7 @@ function install(options) {
|
|
|
471
556
|
summary.platforms[platform] = installPlatform({
|
|
472
557
|
platform,
|
|
473
558
|
skillsRoot,
|
|
559
|
+
commandsRoot,
|
|
474
560
|
xskRoot,
|
|
475
561
|
version,
|
|
476
562
|
skills: applicable,
|
|
@@ -490,6 +576,9 @@ module.exports = {
|
|
|
490
576
|
installPlatform,
|
|
491
577
|
atomicWriteFile,
|
|
492
578
|
rootFor,
|
|
579
|
+
commandsRootFor,
|
|
580
|
+
isCommandFilePath,
|
|
581
|
+
buildCommandFileContent,
|
|
493
582
|
PACKAGE_NAME,
|
|
494
583
|
MARKER,
|
|
495
584
|
ALL_PLATFORMS,
|
package/lib/manifest.js
CHANGED
|
@@ -83,15 +83,35 @@ function isInsideDir(child, parent) {
|
|
|
83
83
|
function validateOperationalSemantics(options) {
|
|
84
84
|
const opts = options || {};
|
|
85
85
|
const skillsRoot = opts.skillsRoot;
|
|
86
|
+
// Optional second owned root for platforms (opencode) that install flat command
|
|
87
|
+
// files outside skillsRoot. Platforms without a commandsRoot stay skillsRoot-only.
|
|
88
|
+
const commandsRoot = opts.commandsRoot;
|
|
86
89
|
const paths = Array.isArray(opts.manifest && opts.manifest.installed_paths) ? opts.manifest.installed_paths : [];
|
|
87
90
|
for (const p of paths) {
|
|
88
|
-
|
|
91
|
+
const isCommandPath = isManifestCommandFilePath(p);
|
|
92
|
+
const insideSkills = isInsideDir(p, skillsRoot);
|
|
93
|
+
const insideCommands = commandsRoot ? isInsideDir(p, commandsRoot) : false;
|
|
94
|
+
if (isCommandPath) {
|
|
95
|
+
if (!commandsRoot) {
|
|
96
|
+
return { valid: false, reason: `command path requires commands root: ${p}` };
|
|
97
|
+
}
|
|
98
|
+
if (!insideCommands) {
|
|
99
|
+
return { valid: false, reason: `command path escapes commands root: ${p}` };
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!insideSkills) {
|
|
89
104
|
return { valid: false, reason: `installed path escapes platform root: ${p}` };
|
|
90
105
|
}
|
|
91
106
|
}
|
|
92
107
|
return { valid: true };
|
|
93
108
|
}
|
|
94
109
|
|
|
110
|
+
function isManifestCommandFilePath(p) {
|
|
111
|
+
const base = path.basename(p);
|
|
112
|
+
return base !== 'SKILL.md' && base.endsWith('.md');
|
|
113
|
+
}
|
|
114
|
+
|
|
95
115
|
function copyInstalledHashes(records) {
|
|
96
116
|
if (!Array.isArray(records)) {
|
|
97
117
|
return [];
|
package/lib/skills.js
CHANGED
|
@@ -27,17 +27,31 @@ const skills = [
|
|
|
27
27
|
{
|
|
28
28
|
name: 'xsk-write-req',
|
|
29
29
|
description:
|
|
30
|
-
'Turn plain-language needs into a grounded requirement document in requirements/, with a self-audit gate before it is finalized.',
|
|
30
|
+
'Turn plain-language needs into a grounded requirement document in .xsk/requirements/, with a self-audit gate before it is finalized.',
|
|
31
31
|
platforms: ALL_PLATFORMS.slice(),
|
|
32
32
|
fragmentBase: 'write-req',
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
name: 'xsk-archive-req',
|
|
36
36
|
description:
|
|
37
|
-
'Archive the active requirement document into requirements/archive/ and leave zero active docs.',
|
|
37
|
+
'Archive the active requirement document into .xsk/requirements/archive/ and leave zero active docs.',
|
|
38
38
|
platforms: ALL_PLATFORMS.slice(),
|
|
39
39
|
fragmentBase: 'archive-req',
|
|
40
40
|
},
|
|
41
|
+
{
|
|
42
|
+
name: 'xsk-point',
|
|
43
|
+
description:
|
|
44
|
+
'Research one aspect into .xsk/points/ to a decision-complete landed plan using xsk-think discipline, then persist and manage it as a named point document.',
|
|
45
|
+
platforms: ALL_PLATFORMS.slice(),
|
|
46
|
+
fragmentBase: 'point',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'xsk-consume-point',
|
|
50
|
+
description:
|
|
51
|
+
'Fold selected .xsk/points into one .xsk/requirements doc via xsk-write-req, archiving consumed points write-before-remove and leaving unconsumed points active.',
|
|
52
|
+
platforms: ALL_PLATFORMS.slice(),
|
|
53
|
+
fragmentBase: 'consume-point',
|
|
54
|
+
},
|
|
41
55
|
{
|
|
42
56
|
name: 'xsk-check',
|
|
43
57
|
description:
|