skills-manifest 0.2.0 → 0.3.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/README.md +52 -6
- package/dist/cli/index.mjs +157 -3
- package/package.json +5 -3
- package/patches/skills-cli.mjs +54 -0
package/README.md
CHANGED
|
@@ -9,10 +9,47 @@
|
|
|
9
9
|
A lightweight manifest manager for [skills](https://github.com/vercel-labs/skills), enabling project-level skill synchronization and collaborative configuration.
|
|
10
10
|
|
|
11
11
|
> [!IMPORTANT]
|
|
12
|
-
> The `skills add <args>` command will not automatically update the `skills-manifest.json` file
|
|
13
|
-
> Since [skills](https://github.com/vercel-labs/skills) does not currently support project-level lock files, this project serves as a temporary solution for sharing and persisting skills across collaborative environments
|
|
12
|
+
> ~~The `skills add <args>` command will not automatically update the `skills-manifest.json` file.~~
|
|
13
|
+
> ~~Since [skills](https://github.com/vercel-labs/skills) does not currently support project-level lock files, this project serves as a temporary solution for sharing and persisting skills across collaborative environments.~~
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
> Now you can use `skills add` to add skills to the manifest.
|
|
16
|
+
|
|
17
|
+
## Init & sync with skills
|
|
18
|
+
|
|
19
|
+
Run once in your project:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx skills-manifest init
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This will:
|
|
26
|
+
|
|
27
|
+
- Install `skills` and `skills-manifest` as dev dependencies
|
|
28
|
+
- Create `skills-manifest.json` and add a `prepare` script that runs `skills-manifest install`
|
|
29
|
+
- Add `skills` to `.gitignore`
|
|
30
|
+
|
|
31
|
+
Then run:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
skills-manifest install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`install` syncs skills from the manifest and **injects a wrapper** into `node_modules/skills` so that:
|
|
38
|
+
|
|
39
|
+
| When you run | skills-manifest also runs |
|
|
40
|
+
|--------------|---------------------------|
|
|
41
|
+
| `skills add <repo> [--skill ...]` | `skills-manifest add <repo> [...]` — the repo and skills are written to `skills-manifest.json` |
|
|
42
|
+
| `skills remove <skills...>` | `skills-manifest remove` for each repo that had those skills |
|
|
43
|
+
|
|
44
|
+
To add a skill **without** updating the manifest (e.g. one-off try), use:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
skills add <repo> --skip-manifest
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Install (manual)
|
|
51
|
+
|
|
52
|
+
Alternatively, install without `init`:
|
|
16
53
|
|
|
17
54
|
```bash
|
|
18
55
|
pnpm add skills skills-manifest -D
|
|
@@ -30,9 +67,8 @@ pnpm add skills skills-manifest -D
|
|
|
30
67
|
"vercel-labs/agent-skills": {
|
|
31
68
|
"vercel-react-best-practices": true
|
|
32
69
|
},
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
70
|
+
// or array of skills
|
|
71
|
+
"https://github.com/vercel-labs": ["find-skills"]
|
|
36
72
|
}
|
|
37
73
|
}
|
|
38
74
|
```
|
|
@@ -53,6 +89,16 @@ skills
|
|
|
53
89
|
}
|
|
54
90
|
```
|
|
55
91
|
|
|
92
|
+
## AGENTS.md for AI Agents
|
|
93
|
+
|
|
94
|
+
After you have configured `skills-manifest.json` and run `skills-manifest install` to generate the skills tree, we recommend using [OpenSkills](https://github.com/numman-ali/openskills) to produce an `AGENTS.md` that AI agents can consume:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npx openskills sync
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This keeps your installed skills in sync with an `AGENTS.md` (or custom output path) so agents that read it (e.g. Claude Code, Cursor, Windsurf) can discover and use the same skills. See [OpenSkills](https://github.com/numman-ali/openskills) for options such as `--universal` and `-o <path>`.
|
|
101
|
+
|
|
56
102
|
## License
|
|
57
103
|
|
|
58
104
|
[MIT](./LICENSE) License © [Hairyf](https://github.com/hairyf)
|
package/dist/cli/index.mjs
CHANGED
|
@@ -1,16 +1,139 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
1
5
|
import { loadConfig } from "c12";
|
|
2
6
|
import { defineCommand, runMain } from "citty";
|
|
7
|
+
import { addDevDependency, installDependencies } from "nypm";
|
|
3
8
|
import { x } from "tinyexec";
|
|
4
9
|
|
|
5
10
|
//#region package.json
|
|
6
|
-
var version = "0.
|
|
11
|
+
var version = "0.3.0";
|
|
7
12
|
|
|
8
13
|
//#endregion
|
|
9
14
|
//#region src/cli/index.ts
|
|
10
|
-
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
async function patchSkillsCli() {
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
const cliPath = path.join(cwd, "node_modules", "skills", "bin", "cli.mjs");
|
|
19
|
+
try {
|
|
20
|
+
if (!(await fs.stat(cliPath)).isFile()) return;
|
|
21
|
+
} catch {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if ((await fs.readFile(cliPath, "utf-8").catch(() => "")).includes("Injected by skills-manifest")) return;
|
|
25
|
+
let wrapperPath;
|
|
26
|
+
try {
|
|
27
|
+
const pkgJsonPath = require.resolve("skills-manifest/package.json");
|
|
28
|
+
wrapperPath = path.join(path.dirname(pkgJsonPath), "patches", "skills-cli.mjs");
|
|
29
|
+
} catch {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const wrapper = await fs.readFile(wrapperPath, "utf-8");
|
|
34
|
+
await fs.writeFile(cliPath, wrapper, "utf-8");
|
|
35
|
+
} catch {
|
|
36
|
+
console.warn("skills-manifest: failed to inject wrapper. Sync add/remove with manifest will not run.");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const DEFAULT_MANIFEST = {
|
|
40
|
+
agents: ["cursor", "claude-code"],
|
|
41
|
+
skills: {}
|
|
42
|
+
};
|
|
43
|
+
const init = defineCommand({
|
|
44
|
+
meta: {
|
|
45
|
+
name: "init",
|
|
46
|
+
description: "Initialize skills configuration"
|
|
47
|
+
},
|
|
48
|
+
async run() {
|
|
49
|
+
const cwd = process.cwd();
|
|
50
|
+
await addDevDependency(["skills", "skills-manifest"], { cwd });
|
|
51
|
+
await installDependencies({ cwd });
|
|
52
|
+
await patchSkillsCli();
|
|
53
|
+
const manifestPath = path.join(cwd, "skills-manifest.json");
|
|
54
|
+
if (!await fs.stat(manifestPath).catch(() => null)) {
|
|
55
|
+
const content = {
|
|
56
|
+
$schema: "https://raw.githubusercontent.com/hairyf/skills-manifest/main/skills-manifest.schema.json",
|
|
57
|
+
...DEFAULT_MANIFEST
|
|
58
|
+
};
|
|
59
|
+
await fs.writeFile(manifestPath, JSON.stringify(content, null, 2));
|
|
60
|
+
}
|
|
61
|
+
const gitignorePath = path.join(cwd, ".gitignore");
|
|
62
|
+
const gitignore = await fs.readFile(gitignorePath, "utf-8").catch(() => "");
|
|
63
|
+
if (!gitignore.split("\n").includes("skills")) await fs.appendFile(gitignorePath, gitignore.endsWith("\n") ? "skills\n" : "\nskills\n");
|
|
64
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
65
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
|
|
66
|
+
const prepare = pkg.scripts?.prepare;
|
|
67
|
+
const cmd = "skills-manifest install";
|
|
68
|
+
if (!prepare) pkg.scripts = {
|
|
69
|
+
...pkg.scripts,
|
|
70
|
+
prepare: cmd
|
|
71
|
+
};
|
|
72
|
+
else if (!prepare.includes(cmd)) pkg.scripts.prepare = `${prepare} && ${cmd}`;
|
|
73
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2));
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
const add = defineCommand({
|
|
77
|
+
meta: {
|
|
78
|
+
name: "add",
|
|
79
|
+
description: "Add repo and skills to manifest"
|
|
80
|
+
},
|
|
81
|
+
args: { repo: {
|
|
82
|
+
type: "positional",
|
|
83
|
+
description: "Repository (e.g. owner/repo or full URL)",
|
|
84
|
+
required: true
|
|
85
|
+
} },
|
|
86
|
+
async run({ args }) {
|
|
87
|
+
const { config, configFile } = await loadConfig({ configFile: "skills-manifest" });
|
|
88
|
+
if (!configFile) throw new Error("skills-manifest.json not found. Run `skills-manifest init` first.");
|
|
89
|
+
const repo = args.repo;
|
|
90
|
+
const skills = (args._ ?? []).slice(1);
|
|
91
|
+
const existing = config.skills[repo];
|
|
92
|
+
const existingList = Array.isArray(existing) ? existing : typeof existing === "object" && existing !== null ? Object.keys(existing).filter((k) => existing[k]) : [];
|
|
93
|
+
const skillsToAdd = [...new Set([...existingList, ...skills])];
|
|
94
|
+
config.skills[repo] = skillsToAdd;
|
|
95
|
+
const content = JSON.stringify(config, null, 2);
|
|
96
|
+
await fs.writeFile(configFile, content);
|
|
97
|
+
console.log(`Added ${repo} to skills-manifest.json`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
const remove = defineCommand({
|
|
101
|
+
meta: {
|
|
102
|
+
name: "remove",
|
|
103
|
+
description: "Remove repo or skills from manifest"
|
|
104
|
+
},
|
|
105
|
+
args: { repo: {
|
|
106
|
+
type: "positional",
|
|
107
|
+
description: "Repository (e.g. owner/repo or full URL)",
|
|
108
|
+
required: true
|
|
109
|
+
} },
|
|
110
|
+
async run({ args }) {
|
|
111
|
+
const { config, configFile } = await loadConfig({ configFile: "skills-manifest" });
|
|
112
|
+
if (!configFile) throw new Error("skills-manifest.json not found. Run `skills-manifest init` first.");
|
|
113
|
+
const repo = args.repo;
|
|
114
|
+
const skillsToRemove = (args._ ?? []).slice(1);
|
|
115
|
+
if (!(repo in config.skills)) throw new Error(`Repo ${repo} not found in manifest`);
|
|
116
|
+
if (skillsToRemove.length === 0) {
|
|
117
|
+
delete config.skills[repo];
|
|
118
|
+
console.log(`Removed ${repo} from skills-manifest.json`);
|
|
119
|
+
} else {
|
|
120
|
+
const existing = config.skills[repo];
|
|
121
|
+
const skillsToKeep = (Array.isArray(existing) ? existing : typeof existing === "object" && existing !== null ? Object.keys(existing).filter((k) => existing[k]) : []).filter((s) => !skillsToRemove.includes(s));
|
|
122
|
+
if (skillsToKeep.length === 0) {
|
|
123
|
+
delete config.skills[repo];
|
|
124
|
+
console.log(`Removed ${repo} from skills-manifest.json`);
|
|
125
|
+
} else {
|
|
126
|
+
config.skills[repo] = skillsToKeep;
|
|
127
|
+
console.log(`Removed skills from ${repo} in skills-manifest.json`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const content = JSON.stringify(config, null, 2);
|
|
131
|
+
await fs.writeFile(configFile, content);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
const install = defineCommand({
|
|
11
135
|
meta: {
|
|
12
136
|
name: "install",
|
|
13
|
-
version,
|
|
14
137
|
description: "Install skills"
|
|
15
138
|
},
|
|
16
139
|
async run() {
|
|
@@ -38,6 +161,37 @@ runMain(defineCommand({
|
|
|
38
161
|
"--yes"
|
|
39
162
|
], { nodeOptions: { stdio: "inherit" } });
|
|
40
163
|
}
|
|
164
|
+
const TARGET_MAP = {
|
|
165
|
+
cursor: ".cursor",
|
|
166
|
+
opencode: ".opencode"
|
|
167
|
+
};
|
|
168
|
+
const agents = path.join(process.cwd(), ".agents");
|
|
169
|
+
const fixedAgents = config.agents.filter((a) => a in TARGET_MAP);
|
|
170
|
+
for (const agent of fixedAgents) {
|
|
171
|
+
const dest = path.join(process.cwd(), TARGET_MAP[agent]);
|
|
172
|
+
await fs.rm(dest, {
|
|
173
|
+
recursive: true,
|
|
174
|
+
force: true
|
|
175
|
+
});
|
|
176
|
+
await fs.cp(agents, dest, {
|
|
177
|
+
recursive: true,
|
|
178
|
+
verbatimSymlinks: false
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
await patchSkillsCli();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
runMain(defineCommand({
|
|
185
|
+
meta: {
|
|
186
|
+
name: "skills-manifest",
|
|
187
|
+
version,
|
|
188
|
+
description: "A lightweight manifest manager for skills"
|
|
189
|
+
},
|
|
190
|
+
subCommands: {
|
|
191
|
+
add,
|
|
192
|
+
init,
|
|
193
|
+
install,
|
|
194
|
+
remove
|
|
41
195
|
}
|
|
42
196
|
}));
|
|
43
197
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skills-manifest",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "A lightweight manifest manager for skills, enabling project-level skill synchronization and collaborative configuration.",
|
|
6
6
|
"author": "Hairyf <wwu710632@gmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"bin",
|
|
31
|
-
"dist"
|
|
31
|
+
"dist",
|
|
32
|
+
"patches"
|
|
32
33
|
],
|
|
33
34
|
"peerDependencies": {
|
|
34
35
|
"skills": "^1.2.0"
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
"dependencies": {
|
|
37
38
|
"c12": "^3.3.3",
|
|
38
39
|
"citty": "^0.2.0",
|
|
40
|
+
"nypm": "^0.6.4",
|
|
39
41
|
"tinyexec": "^1.0.2"
|
|
40
42
|
},
|
|
41
43
|
"devDependencies": {
|
|
@@ -56,7 +58,7 @@
|
|
|
56
58
|
"vitest": "^4.0.15",
|
|
57
59
|
"vitest-package-exports": "^0.1.1",
|
|
58
60
|
"yaml": "^2.8.2",
|
|
59
|
-
"skills-manifest": "0.
|
|
61
|
+
"skills-manifest": "0.3.0"
|
|
60
62
|
},
|
|
61
63
|
"simple-git-hooks": {
|
|
62
64
|
"pre-commit": "pnpm i --frozen-lockfile --ignore-scripts --offline && npx lint-staged"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Injected by skills-manifest install: sync add/remove with skills-manifest.json
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { dirname, join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const originalCli = join(__dirname, '..', 'dist', 'cli.mjs')
|
|
10
|
+
const cwd = process.cwd()
|
|
11
|
+
|
|
12
|
+
function runSkillsManifest(args) {
|
|
13
|
+
const inNodeModules = join(cwd, 'node_modules', 'skills-manifest', 'bin')
|
|
14
|
+
const inCwd = join(cwd, 'bin')
|
|
15
|
+
const binDir = existsSync(inNodeModules) ? inNodeModules : inCwd
|
|
16
|
+
const entry = existsSync(join(binDir, 'index.dev.mjs')) ? join(binDir, 'index.dev.mjs') : join(binDir, 'index.mjs')
|
|
17
|
+
if (!existsSync(entry)) return
|
|
18
|
+
const r = spawnSync(process.execPath, [entry, ...args], { stdio: 'inherit', cwd })
|
|
19
|
+
if (r.status !== 0) process.exit(r.status ?? 1)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const argv = process.argv.slice(2)
|
|
23
|
+
const skipManifest = argv.includes('--skip-manifest')
|
|
24
|
+
const forwardArgs = argv.filter(a => a !== '--skip-manifest')
|
|
25
|
+
const cmd = forwardArgs[0]
|
|
26
|
+
|
|
27
|
+
const result = spawnSync(process.execPath, [originalCli, ...forwardArgs], { stdio: 'inherit', cwd })
|
|
28
|
+
if (result.status !== 0) process.exit(result.status ?? 1)
|
|
29
|
+
|
|
30
|
+
if (cmd === 'add' && !skipManifest) {
|
|
31
|
+
const rest = forwardArgs.slice(1)
|
|
32
|
+
let repo = ''
|
|
33
|
+
const skills = []
|
|
34
|
+
for (let i = 0; i < rest.length; i++) {
|
|
35
|
+
if (rest[i] === '--skill' && rest[i + 1]) { skills.push(rest[i + 1]); i++; continue }
|
|
36
|
+
if (!rest[i].startsWith('-') && !repo) { repo = rest[i]; continue }
|
|
37
|
+
}
|
|
38
|
+
if (repo) runSkillsManifest(['add', repo, ...skills])
|
|
39
|
+
} else if (cmd === 'remove') {
|
|
40
|
+
const rest = forwardArgs.slice(1)
|
|
41
|
+
const skillNames = []
|
|
42
|
+
for (const a of rest) { if (a.startsWith('-')) break; skillNames.push(a) }
|
|
43
|
+
if (skillNames.length === 0) process.exit(0)
|
|
44
|
+
const manifestPath = join(cwd, 'skills-manifest.json')
|
|
45
|
+
if (!existsSync(manifestPath)) process.exit(0)
|
|
46
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'))
|
|
47
|
+
const repos = manifest.skills || {}
|
|
48
|
+
for (const [repo, list] of Object.entries(repos)) {
|
|
49
|
+
const arr = Array.isArray(list) ? list : (typeof list === 'object' && list !== null ? Object.keys(list).filter(k => list[k]) : [])
|
|
50
|
+
const toRemove = skillNames.filter(s => arr.includes(s))
|
|
51
|
+
if (toRemove.length > 0) runSkillsManifest(['remove', repo, ...toRemove])
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
process.exit(0)
|