skillpm 0.0.4 → 0.0.5
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 +6 -2
- package/dist/commands/install.d.ts +1 -1
- package/dist/commands/install.js +18 -46
- package/dist/commands/uninstall.js +8 -10
- package/dist/configs/index.d.ts +9 -0
- package/dist/configs/index.js +97 -0
- package/dist/manifest/schema.d.ts +2 -4
- package/dist/scanner/index.js +14 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,12 +21,14 @@ npx skillpm list
|
|
|
21
21
|
npx skillpm init
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Or install
|
|
24
|
+
Or install the CLI globally:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
npm install -g skillpm
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
> **Note:** Skills are always workspace-local. This installs the `skillpm` CLI — not skills.
|
|
31
|
+
|
|
30
32
|
## How it works
|
|
31
33
|
|
|
32
34
|
When you run `skillpm install <skill>`:
|
|
@@ -34,7 +36,8 @@ When you run `skillpm install <skill>`:
|
|
|
34
36
|
1. **npm install** — npm handles resolution, download, lockfile, `node_modules/`
|
|
35
37
|
2. **Scan** — skillpm scans `node_modules/` for packages containing `skills/*/SKILL.md`
|
|
36
38
|
3. **Link** — for each skill found, skillpm calls [`skills`](https://www.npmjs.com/package/skills) to wire it into agent directories (Claude, Cursor, VS Code, Codex, and many more)
|
|
37
|
-
4. **
|
|
39
|
+
4. **Configs** — for each skill with a `configs/` directory, skillpm copies agent definitions, rules, and prompts into the workspace (auto-prefixed to avoid conflicts)
|
|
40
|
+
5. **MCP config** — skillpm collects `skillpm.mcpServers` from all skills (transitively) and configures each via [`add-mcp`](https://github.com/neondatabase/add-mcp)
|
|
38
41
|
|
|
39
42
|
That's it. Agents see the full skill tree with MCP servers configured.
|
|
40
43
|
|
|
@@ -49,6 +52,7 @@ skillpm doesn't reinvent anything. It orchestrates three battle-tested tools: np
|
|
|
49
52
|
| Dependency management | Standard `package.json` `dependencies` — npm handles semver, lockfiles, audit |
|
|
50
53
|
| Versioning | npm semver, `package-lock.json`, reproducible installs |
|
|
51
54
|
| Agent wiring | Links skills into agent directories via [`skills`](https://www.npmjs.com/package/skills) |
|
|
55
|
+
| Config files | Copies agent definitions, rules, and prompts from `configs/` into the workspace |
|
|
52
56
|
| MCP server config | Collects and configures MCP servers transitively via [`add-mcp`](https://github.com/neondatabase/add-mcp) |
|
|
53
57
|
|
|
54
58
|
## Commands
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare function install(args: string[], cwd: string): Promise<void>;
|
|
2
|
-
export declare function wireSkills(
|
|
2
|
+
export declare function wireSkills(cwd: string): Promise<void>;
|
package/dist/commands/install.js
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
import { npm, npx, log } from '../utils/index.js';
|
|
2
2
|
import { scanNodeModules, collectMcpServers } from '../scanner/index.js';
|
|
3
|
-
import {
|
|
4
|
-
import { promisify } from 'node:util';
|
|
5
|
-
import { dirname } from 'node:path';
|
|
6
|
-
import { homedir } from 'node:os';
|
|
7
|
-
const execFileAsync = promisify(execFile);
|
|
8
|
-
function isGlobalFlag(args) {
|
|
9
|
-
return args.includes('-g') || args.includes('--global');
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Resolve the node_modules root to scan. For global installs, uses `npm root -g`.
|
|
13
|
-
*/
|
|
14
|
-
async function resolveNodeModulesRoot(args, cwd) {
|
|
15
|
-
if (!isGlobalFlag(args))
|
|
16
|
-
return cwd;
|
|
17
|
-
const { stdout } = await execFileAsync('npm', ['root', '-g']);
|
|
18
|
-
return dirname(stdout.trim());
|
|
19
|
-
}
|
|
3
|
+
import { copyConfigs } from '../configs/index.js';
|
|
20
4
|
export async function install(args, cwd) {
|
|
5
|
+
// Reject global installs — skillpm is workspace-only
|
|
6
|
+
if (args.includes('-g') || args.includes('--global')) {
|
|
7
|
+
log.error('Global installs are not supported. skillpm works per-workspace with package.json and lockfiles.');
|
|
8
|
+
log.error('For global skills, use: npx skills add <path>');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
21
11
|
// Step 1: npm install
|
|
22
12
|
const npmArgs = ['install', ...args];
|
|
23
13
|
log.info(`Running npm ${npmArgs.join(' ')}`);
|
|
@@ -34,15 +24,11 @@ export async function install(args, cwd) {
|
|
|
34
24
|
process.exit(1);
|
|
35
25
|
}
|
|
36
26
|
// Step 2: Scan for skills and wire them
|
|
37
|
-
|
|
38
|
-
// For global installs, wire skills into user's home directory (not the npm prefix)
|
|
39
|
-
const wireTarget = isGlobalFlag(args) ? homedir() : cwd;
|
|
40
|
-
await wireSkills(scanRoot, wireTarget);
|
|
27
|
+
await wireSkills(cwd);
|
|
41
28
|
}
|
|
42
|
-
export async function wireSkills(
|
|
43
|
-
const wireCwd = wireTarget ?? scanRoot;
|
|
29
|
+
export async function wireSkills(cwd) {
|
|
44
30
|
// Scan node_modules/ for SKILL.md packages
|
|
45
|
-
const skills = await scanNodeModules(
|
|
31
|
+
const skills = await scanNodeModules(cwd);
|
|
46
32
|
if (skills.length === 0) {
|
|
47
33
|
log.info('No skill packages found in node_modules/');
|
|
48
34
|
return;
|
|
@@ -52,7 +38,7 @@ export async function wireSkills(scanRoot, wireTarget) {
|
|
|
52
38
|
for (const skill of skills) {
|
|
53
39
|
log.info(`Linking ${log.skill(skill.name, skill.version)} into agent directories`);
|
|
54
40
|
try {
|
|
55
|
-
await npx(['skills', 'add', skill.skillDir, '--all', '-y'], { cwd
|
|
41
|
+
await npx(['skills', 'add', skill.skillDir, '--all', '-y'], { cwd });
|
|
56
42
|
log.success(`Linked ${skill.name}`);
|
|
57
43
|
}
|
|
58
44
|
catch (err) {
|
|
@@ -70,7 +56,7 @@ export async function wireSkills(scanRoot, wireTarget) {
|
|
|
70
56
|
for (const server of mcpServers) {
|
|
71
57
|
log.info(`Configuring MCP server: ${server}`);
|
|
72
58
|
try {
|
|
73
|
-
await npx(['add-mcp', server, '-y'], { cwd
|
|
59
|
+
await npx(['add-mcp', server, '-y'], { cwd });
|
|
74
60
|
log.success(`Configured ${server}`);
|
|
75
61
|
}
|
|
76
62
|
catch (err) {
|
|
@@ -79,31 +65,17 @@ export async function wireSkills(scanRoot, wireTarget) {
|
|
|
79
65
|
}
|
|
80
66
|
}
|
|
81
67
|
}
|
|
82
|
-
//
|
|
83
|
-
for (const skill of skills) {
|
|
84
|
-
for (const agentFile of skill.agents) {
|
|
85
|
-
log.info(`Wiring agent from ${log.skill(skill.name, skill.version)}`);
|
|
86
|
-
try {
|
|
87
|
-
await npx(['add-agent', agentFile, '--package', skill.name], { cwd: wireCwd });
|
|
88
|
-
log.success(`Wired agent ${agentFile}`);
|
|
89
|
-
}
|
|
90
|
-
catch (err) {
|
|
91
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
92
|
-
log.warn(`Failed to wire agent: ${msg}`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// Wire prompts/instructions via add-prompt
|
|
68
|
+
// Copy configs/ files (agents, prompts, rules) into workspace
|
|
97
69
|
for (const skill of skills) {
|
|
98
|
-
|
|
99
|
-
log.info(`
|
|
70
|
+
if (skill.configsDir) {
|
|
71
|
+
log.info(`Copying config files from ${log.skill(skill.name, skill.version)}`);
|
|
100
72
|
try {
|
|
101
|
-
await
|
|
102
|
-
log.success(`
|
|
73
|
+
const copied = await copyConfigs(skill.configsDir, cwd, skill.name);
|
|
74
|
+
log.success(`Copied ${copied.length} config file(s) from ${skill.name}`);
|
|
103
75
|
}
|
|
104
76
|
catch (err) {
|
|
105
77
|
const msg = err instanceof Error ? err.message : String(err);
|
|
106
|
-
log.warn(`Failed to
|
|
78
|
+
log.warn(`Failed to copy config files from ${skill.name}: ${msg}`);
|
|
107
79
|
}
|
|
108
80
|
}
|
|
109
81
|
}
|
|
@@ -1,23 +1,21 @@
|
|
|
1
|
-
import { npm,
|
|
1
|
+
import { npm, log } from '../utils/index.js';
|
|
2
2
|
import { wireSkills } from './install.js';
|
|
3
|
+
import { removeConfigs } from '../configs/index.js';
|
|
3
4
|
export async function uninstall(args, cwd) {
|
|
4
5
|
if (args.length === 0) {
|
|
5
6
|
log.error('Usage: skillpm uninstall <skill> [skill...]');
|
|
6
7
|
process.exit(1);
|
|
7
8
|
}
|
|
8
|
-
// Clean up
|
|
9
|
+
// Clean up wired files before npm uninstall
|
|
9
10
|
for (const pkg of args) {
|
|
10
11
|
try {
|
|
11
|
-
await
|
|
12
|
+
const removed = await removeConfigs(cwd, pkg);
|
|
13
|
+
if (removed.length > 0) {
|
|
14
|
+
log.info(`Removed ${removed.length} config file(s) from ${pkg}`);
|
|
15
|
+
}
|
|
12
16
|
}
|
|
13
17
|
catch {
|
|
14
|
-
// Ignore — package may not have had
|
|
15
|
-
}
|
|
16
|
-
try {
|
|
17
|
-
await npx(['add-prompt', '--remove-package', pkg], { cwd });
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
// Ignore — package may not have had prompts
|
|
18
|
+
// Ignore — package may not have had configs
|
|
21
19
|
}
|
|
22
20
|
}
|
|
23
21
|
log.info(`Running npm uninstall ${args.join(' ')}`);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy all files from a skill's configs/ directory to the workspace,
|
|
3
|
+
* auto-prefixing filenames with the package name.
|
|
4
|
+
*/
|
|
5
|
+
export declare function copyConfigs(configsDir: string, cwd: string, packageName: string): Promise<string[]>;
|
|
6
|
+
/**
|
|
7
|
+
* Remove all config files for a package using the manifest.
|
|
8
|
+
*/
|
|
9
|
+
export declare function removeConfigs(cwd: string, packageName: string): Promise<string[]>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { readdir, copyFile, mkdir, unlink, readFile, writeFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { join, relative, dirname, basename } from 'node:path';
|
|
3
|
+
const MANIFEST_DIR = '.skillpm';
|
|
4
|
+
const MANIFEST_FILE = 'manifest.json';
|
|
5
|
+
async function readManifest(cwd) {
|
|
6
|
+
try {
|
|
7
|
+
const raw = await readFile(join(cwd, MANIFEST_DIR, MANIFEST_FILE), 'utf-8');
|
|
8
|
+
return JSON.parse(raw);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function writeManifest(cwd, manifest) {
|
|
15
|
+
const dir = join(cwd, MANIFEST_DIR);
|
|
16
|
+
await mkdir(dir, { recursive: true });
|
|
17
|
+
await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(manifest, null, 2) + '\n');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Recursively collect all files in a directory, returning paths relative to the root.
|
|
21
|
+
*/
|
|
22
|
+
async function walkDir(dir, root) {
|
|
23
|
+
root ??= dir;
|
|
24
|
+
const files = [];
|
|
25
|
+
let entries;
|
|
26
|
+
try {
|
|
27
|
+
entries = await readdir(dir);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return files;
|
|
31
|
+
}
|
|
32
|
+
for (const name of entries) {
|
|
33
|
+
const full = join(dir, name);
|
|
34
|
+
const s = await stat(full);
|
|
35
|
+
if (s.isDirectory()) {
|
|
36
|
+
files.push(...(await walkDir(full, root)));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
files.push(relative(root, full));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return files;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Auto-prefix a filename with the package name to avoid conflicts.
|
|
46
|
+
* e.g. "reviewer.md" with package "my-skill" → "my-skill--reviewer.md"
|
|
47
|
+
*/
|
|
48
|
+
function prefixFilename(relPath, packageName) {
|
|
49
|
+
const dir = dirname(relPath);
|
|
50
|
+
const file = basename(relPath);
|
|
51
|
+
const prefixed = `${packageName}--${file}`;
|
|
52
|
+
return dir === '.' ? prefixed : join(dir, prefixed);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Copy all files from a skill's configs/ directory to the workspace,
|
|
56
|
+
* auto-prefixing filenames with the package name.
|
|
57
|
+
*/
|
|
58
|
+
export async function copyConfigs(configsDir, cwd, packageName) {
|
|
59
|
+
const files = await walkDir(configsDir);
|
|
60
|
+
const copied = [];
|
|
61
|
+
for (const relPath of files) {
|
|
62
|
+
const prefixed = prefixFilename(relPath, packageName);
|
|
63
|
+
const src = join(configsDir, relPath);
|
|
64
|
+
const dest = join(cwd, prefixed);
|
|
65
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
66
|
+
await copyFile(src, dest);
|
|
67
|
+
copied.push(prefixed);
|
|
68
|
+
}
|
|
69
|
+
// Update manifest
|
|
70
|
+
const manifest = await readManifest(cwd);
|
|
71
|
+
manifest[packageName] = copied;
|
|
72
|
+
await writeManifest(cwd, manifest);
|
|
73
|
+
return copied;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Remove all config files for a package using the manifest.
|
|
77
|
+
*/
|
|
78
|
+
export async function removeConfigs(cwd, packageName) {
|
|
79
|
+
const manifest = await readManifest(cwd);
|
|
80
|
+
const files = manifest[packageName];
|
|
81
|
+
if (!files || files.length === 0)
|
|
82
|
+
return [];
|
|
83
|
+
const removed = [];
|
|
84
|
+
for (const relPath of files) {
|
|
85
|
+
try {
|
|
86
|
+
await unlink(join(cwd, relPath));
|
|
87
|
+
removed.push(relPath);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// File already gone
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
delete manifest[packageName];
|
|
94
|
+
await writeManifest(cwd, manifest);
|
|
95
|
+
return removed;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -19,8 +19,6 @@ export interface SkillInfo {
|
|
|
19
19
|
mcpServers: string[];
|
|
20
20
|
/** True if SKILL.md is at package root instead of skills/<name>/ */
|
|
21
21
|
legacy?: boolean;
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
/** Paths to prompt/instruction .md files in prompts/ */
|
|
25
|
-
prompts: string[];
|
|
22
|
+
/** Path to configs/ directory if present (mirrors workspace layout) */
|
|
23
|
+
configsDir?: string;
|
|
26
24
|
}
|
package/dist/scanner/index.js
CHANGED
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
import { readdir, access } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { readPackageJson, parseSkillpmField } from '../manifest/index.js';
|
|
4
|
-
/**
|
|
5
|
-
* Scan a directory for .md files (used for agents/ and prompts/ dirs).
|
|
6
|
-
*/
|
|
7
|
-
async function scanMdFiles(dir) {
|
|
8
|
-
let entries;
|
|
9
|
-
try {
|
|
10
|
-
entries = await readdir(dir);
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
return [];
|
|
14
|
-
}
|
|
15
|
-
return entries.filter((e) => e.endsWith('.md')).map((e) => join(dir, e));
|
|
16
|
-
}
|
|
17
4
|
/**
|
|
18
5
|
* Scan node_modules/ for packages that contain a skills/<name>/SKILL.md file.
|
|
19
6
|
* Returns metadata for each discovered skill package.
|
|
@@ -57,10 +44,22 @@ export async function scanNodeModules(cwd) {
|
|
|
57
44
|
}
|
|
58
45
|
return skills;
|
|
59
46
|
}
|
|
47
|
+
async function hasDir(dir) {
|
|
48
|
+
try {
|
|
49
|
+
await access(dir);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
60
56
|
async function tryReadSkill(pkgDir) {
|
|
61
57
|
const pkg = await readPackageJson(pkgDir);
|
|
62
58
|
if (!pkg)
|
|
63
59
|
return null;
|
|
60
|
+
// Check for configs/ directory
|
|
61
|
+
const configsDir = join(pkgDir, 'configs');
|
|
62
|
+
const hasConfigs = await hasDir(configsDir);
|
|
64
63
|
// Preferred: look for skills/*/SKILL.md
|
|
65
64
|
const skillsDir = join(pkgDir, 'skills');
|
|
66
65
|
let skillSubdirs;
|
|
@@ -79,16 +78,13 @@ async function tryReadSkill(pkgDir) {
|
|
|
79
78
|
continue;
|
|
80
79
|
}
|
|
81
80
|
const skillpm = parseSkillpmField(pkg);
|
|
82
|
-
const agents = await scanMdFiles(join(pkgDir, 'agents'));
|
|
83
|
-
const prompts = await scanMdFiles(join(pkgDir, 'prompts'));
|
|
84
81
|
return {
|
|
85
82
|
name: pkg.name,
|
|
86
83
|
version: pkg.version,
|
|
87
84
|
path: pkgDir,
|
|
88
85
|
skillDir,
|
|
89
86
|
mcpServers: skillpm?.mcpServers ?? [],
|
|
90
|
-
|
|
91
|
-
prompts,
|
|
87
|
+
configsDir: hasConfigs ? configsDir : undefined,
|
|
92
88
|
};
|
|
93
89
|
}
|
|
94
90
|
// Fallback: root SKILL.md (legacy format)
|
|
@@ -99,8 +95,6 @@ async function tryReadSkill(pkgDir) {
|
|
|
99
95
|
return null;
|
|
100
96
|
}
|
|
101
97
|
const skillpm = parseSkillpmField(pkg);
|
|
102
|
-
const agents = await scanMdFiles(join(pkgDir, 'agents'));
|
|
103
|
-
const prompts = await scanMdFiles(join(pkgDir, 'prompts'));
|
|
104
98
|
return {
|
|
105
99
|
name: pkg.name,
|
|
106
100
|
version: pkg.version,
|
|
@@ -108,8 +102,7 @@ async function tryReadSkill(pkgDir) {
|
|
|
108
102
|
skillDir: pkgDir,
|
|
109
103
|
mcpServers: skillpm?.mcpServers ?? [],
|
|
110
104
|
legacy: true,
|
|
111
|
-
|
|
112
|
-
prompts,
|
|
105
|
+
configsDir: hasConfigs ? configsDir : undefined,
|
|
113
106
|
};
|
|
114
107
|
}
|
|
115
108
|
/**
|