skills-manifest 0.2.1 → 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 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
- ## Install
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
@@ -52,6 +89,16 @@ skills
52
89
  }
53
90
  ```
54
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
+
55
102
  ## License
56
103
 
57
104
  [MIT](./LICENSE) License © [Hairyf](https://github.com/hairyf)
@@ -1,19 +1,139 @@
1
+ import { createRequire } from "node:module";
1
2
  import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import process from "node:process";
4
5
  import { loadConfig } from "c12";
5
6
  import { defineCommand, runMain } from "citty";
7
+ import { addDevDependency, installDependencies } from "nypm";
6
8
  import { x } from "tinyexec";
7
9
 
8
10
  //#region package.json
9
- var version = "0.2.1";
11
+ var version = "0.3.0";
10
12
 
11
13
  //#endregion
12
14
  //#region src/cli/index.ts
13
- runMain(defineCommand({
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({
14
135
  meta: {
15
136
  name: "install",
16
- version,
17
137
  description: "Install skills"
18
138
  },
19
139
  async run() {
@@ -58,6 +178,20 @@ runMain(defineCommand({
58
178
  verbatimSymlinks: false
59
179
  });
60
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
61
195
  }
62
196
  }));
63
197
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "skills-manifest",
3
3
  "type": "module",
4
- "version": "0.2.1",
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.2.1"
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)