skillpm 0.0.5 → 0.0.7
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 +23 -0
- package/dist/cli.js +1 -1
- package/dist/commands/install.js +5 -3
- package/dist/configs/index.d.ts +14 -2
- package/dist/configs/index.js +38 -9
- package/dist/manifest/schema.d.ts +13 -0
- package/dist/manifest/schema.js +1 -0
- package/dist/scanner/index.d.ts +2 -0
- package/dist/scanner/index.js +25 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,6 +70,29 @@ skillpm doesn't reinvent anything. It orchestrates three battle-tested tools: np
|
|
|
70
70
|
|
|
71
71
|
Aliases: `i` for `install`, `rm`/`remove` for `uninstall`, `ls` for `list`.
|
|
72
72
|
|
|
73
|
+
## Monorepo / npm workspace support
|
|
74
|
+
|
|
75
|
+
If your repo is an **npm workspace monorepo** where each skill is a first-party package (e.g. `skills/<name>/` entries in the root `package.json` workspaces field), npm installs them as symlinks inside `node_modules/`:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
node_modules/
|
|
79
|
+
@org/
|
|
80
|
+
my-skill → ../../skills/my-skill ← symlink
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`skillpm sync` (and `skillpm install`) automatically detects these symlinks and treats them as workspace packages:
|
|
84
|
+
|
|
85
|
+
- Configs are copied from the symlinked skill's `configs/` directory into the workspace root, exactly as for externally installed skills.
|
|
86
|
+
- Workspace packages are identified in log output: `Linking workspace package @org/my-skill@1.0.0`.
|
|
87
|
+
|
|
88
|
+
This lets contributors regenerate deployed copies (agent definitions, prompts, rules) by running:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
skillpm sync
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
No manual copy steps needed. Commit the regenerated files as usual.
|
|
95
|
+
|
|
73
96
|
## Creating a skill
|
|
74
97
|
|
|
75
98
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { publish } from './commands/publish.js';
|
|
|
7
7
|
import { sync } from './commands/sync.js';
|
|
8
8
|
import { mcp } from './commands/mcp.js';
|
|
9
9
|
import { log } from './utils/index.js';
|
|
10
|
-
const VERSION = '0.0.
|
|
10
|
+
const VERSION = '0.0.7';
|
|
11
11
|
const HELP = `
|
|
12
12
|
skillpm — Agent Skill package manager
|
|
13
13
|
|
package/dist/commands/install.js
CHANGED
|
@@ -36,7 +36,8 @@ export async function wireSkills(cwd) {
|
|
|
36
36
|
log.info(`Found ${skills.length} skill package(s)`);
|
|
37
37
|
// Wire each skill into agent directories via skills CLI
|
|
38
38
|
for (const skill of skills) {
|
|
39
|
-
|
|
39
|
+
const label = skill.workspace ? `workspace package ${log.skill(skill.name, skill.version)}` : log.skill(skill.name, skill.version);
|
|
40
|
+
log.info(`Linking ${label} into agent directories`);
|
|
40
41
|
try {
|
|
41
42
|
await npx(['skills', 'add', skill.skillDir, '--all', '-y'], { cwd });
|
|
42
43
|
log.success(`Linked ${skill.name}`);
|
|
@@ -68,9 +69,10 @@ export async function wireSkills(cwd) {
|
|
|
68
69
|
// Copy configs/ files (agents, prompts, rules) into workspace
|
|
69
70
|
for (const skill of skills) {
|
|
70
71
|
if (skill.configsDir) {
|
|
71
|
-
|
|
72
|
+
const label = skill.workspace ? `workspace package ${log.skill(skill.name, skill.version)}` : log.skill(skill.name, skill.version);
|
|
73
|
+
log.info(`Copying config files from ${label}`);
|
|
72
74
|
try {
|
|
73
|
-
const copied = await copyConfigs(skill.configsDir, cwd, skill.name);
|
|
75
|
+
const copied = await copyConfigs(skill.configsDir, cwd, skill.name, skill.configPrefix);
|
|
74
76
|
log.success(`Copied ${copied.length} config file(s) from ${skill.name}`);
|
|
75
77
|
}
|
|
76
78
|
catch (err) {
|
package/dist/configs/index.d.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Copy all files from a skill's configs/ directory to the workspace,
|
|
3
|
-
* auto-prefixing filenames
|
|
3
|
+
* auto-prefixing filenames to avoid conflicts between installed skills.
|
|
4
|
+
*
|
|
5
|
+
* The prefix used is, in priority order:
|
|
6
|
+
* 1. `configPrefix` argument (from skillpm.configPrefix in package.json)
|
|
7
|
+
* 2. De-scoped package name (strips "@scope/" from scoped packages)
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* packageName="@mcaps/spt-iq-consumption", configPrefix="consumption"
|
|
11
|
+
* → "consumption-briefing.md"
|
|
12
|
+
* packageName="@mcaps/spt-iq-consumption", no configPrefix
|
|
13
|
+
* → "spt-iq-consumption-briefing.md"
|
|
14
|
+
* packageName="my-skill", no configPrefix
|
|
15
|
+
* → "my-skill-briefing.md"
|
|
4
16
|
*/
|
|
5
|
-
export declare function copyConfigs(configsDir: string, cwd: string, packageName: string): Promise<string[]>;
|
|
17
|
+
export declare function copyConfigs(configsDir: string, cwd: string, packageName: string, configPrefix?: string): Promise<string[]>;
|
|
6
18
|
/**
|
|
7
19
|
* Remove all config files for a package using the manifest.
|
|
8
20
|
*/
|
package/dist/configs/index.js
CHANGED
|
@@ -2,6 +2,10 @@ import { readdir, copyFile, mkdir, unlink, readFile, writeFile, stat } from 'nod
|
|
|
2
2
|
import { join, relative, dirname, basename } from 'node:path';
|
|
3
3
|
const MANIFEST_DIR = '.skillpm';
|
|
4
4
|
const MANIFEST_FILE = 'manifest.json';
|
|
5
|
+
/** Normalize path separators to forward slashes for cross-platform consistency. */
|
|
6
|
+
function normalizePath(p) {
|
|
7
|
+
return p.replace(/\\/g, '/');
|
|
8
|
+
}
|
|
5
9
|
async function readManifest(cwd) {
|
|
6
10
|
try {
|
|
7
11
|
const raw = await readFile(join(cwd, MANIFEST_DIR, MANIFEST_FILE), 'utf-8');
|
|
@@ -36,30 +40,55 @@ async function walkDir(dir, root) {
|
|
|
36
40
|
files.push(...(await walkDir(full, root)));
|
|
37
41
|
}
|
|
38
42
|
else {
|
|
39
|
-
files.push(relative(root, full));
|
|
43
|
+
files.push(normalizePath(relative(root, full)));
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
return files;
|
|
43
47
|
}
|
|
44
48
|
/**
|
|
45
|
-
*
|
|
46
|
-
* e.g. "
|
|
49
|
+
* Strip npm scope from a package name.
|
|
50
|
+
* e.g. "@mcaps/spt-iq-consumption" → "spt-iq-consumption"
|
|
51
|
+
* "spt-iq-consumption" → "spt-iq-consumption"
|
|
52
|
+
*/
|
|
53
|
+
function stripScope(packageName) {
|
|
54
|
+
if (packageName.startsWith('@')) {
|
|
55
|
+
const slash = packageName.indexOf('/');
|
|
56
|
+
return slash >= 0 ? packageName.slice(slash + 1) : packageName;
|
|
57
|
+
}
|
|
58
|
+
return packageName;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Auto-prefix a filename with the resolved prefix to avoid conflicts.
|
|
62
|
+
* e.g. "reviewer.md" with prefix "my-skill" → "my-skill-reviewer.md"
|
|
47
63
|
*/
|
|
48
|
-
function prefixFilename(relPath,
|
|
64
|
+
function prefixFilename(relPath, prefix) {
|
|
49
65
|
const dir = dirname(relPath);
|
|
50
66
|
const file = basename(relPath);
|
|
51
|
-
const prefixed = `${
|
|
52
|
-
return dir === '.' ? prefixed : join(dir, prefixed);
|
|
67
|
+
const prefixed = `${prefix}-${file}`;
|
|
68
|
+
return normalizePath(dir === '.' ? prefixed : join(dir, prefixed));
|
|
53
69
|
}
|
|
54
70
|
/**
|
|
55
71
|
* Copy all files from a skill's configs/ directory to the workspace,
|
|
56
|
-
* auto-prefixing filenames
|
|
72
|
+
* auto-prefixing filenames to avoid conflicts between installed skills.
|
|
73
|
+
*
|
|
74
|
+
* The prefix used is, in priority order:
|
|
75
|
+
* 1. `configPrefix` argument (from skillpm.configPrefix in package.json)
|
|
76
|
+
* 2. De-scoped package name (strips "@scope/" from scoped packages)
|
|
77
|
+
*
|
|
78
|
+
* Examples:
|
|
79
|
+
* packageName="@mcaps/spt-iq-consumption", configPrefix="consumption"
|
|
80
|
+
* → "consumption-briefing.md"
|
|
81
|
+
* packageName="@mcaps/spt-iq-consumption", no configPrefix
|
|
82
|
+
* → "spt-iq-consumption-briefing.md"
|
|
83
|
+
* packageName="my-skill", no configPrefix
|
|
84
|
+
* → "my-skill-briefing.md"
|
|
57
85
|
*/
|
|
58
|
-
export async function copyConfigs(configsDir, cwd, packageName) {
|
|
86
|
+
export async function copyConfigs(configsDir, cwd, packageName, configPrefix) {
|
|
87
|
+
const prefix = configPrefix ?? stripScope(packageName);
|
|
59
88
|
const files = await walkDir(configsDir);
|
|
60
89
|
const copied = [];
|
|
61
90
|
for (const relPath of files) {
|
|
62
|
-
const prefixed = prefixFilename(relPath,
|
|
91
|
+
const prefixed = prefixFilename(relPath, prefix);
|
|
63
92
|
const src = join(configsDir, relPath);
|
|
64
93
|
const dest = join(cwd, prefixed);
|
|
65
94
|
await mkdir(dirname(dest), { recursive: true });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export declare const SkillpmFieldSchema: z.ZodOptional<z.ZodObject<{
|
|
3
3
|
mcpServers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
4
|
+
configPrefix: z.ZodOptional<z.ZodString>;
|
|
4
5
|
}, z.core.$strict>>;
|
|
5
6
|
export type SkillpmField = z.infer<typeof SkillpmFieldSchema>;
|
|
6
7
|
export interface SkillPackageJson {
|
|
@@ -21,4 +22,16 @@ export interface SkillInfo {
|
|
|
21
22
|
legacy?: boolean;
|
|
22
23
|
/** Path to configs/ directory if present (mirrors workspace layout) */
|
|
23
24
|
configsDir?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Optional prefix override for config file naming.
|
|
27
|
+
* When set, used instead of the (de-scoped) package name.
|
|
28
|
+
* e.g. configPrefix: "consumption" → "consumption--briefing.md"
|
|
29
|
+
* Declared via skillpm.configPrefix in package.json.
|
|
30
|
+
*/
|
|
31
|
+
configPrefix?: string;
|
|
32
|
+
/**
|
|
33
|
+
* True when the package was found via a symlink in node_modules/ — typically
|
|
34
|
+
* an npm workspace package (first-party skill in the same monorepo).
|
|
35
|
+
*/
|
|
36
|
+
workspace?: boolean;
|
|
24
37
|
}
|
package/dist/manifest/schema.js
CHANGED
package/dist/scanner/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { SkillInfo } from '../manifest/schema.js';
|
|
2
2
|
/**
|
|
3
3
|
* Scan node_modules/ for packages that contain a skills/<name>/SKILL.md file.
|
|
4
|
+
* Also follows symlinks — npm workspace packages appear as symlinks inside
|
|
5
|
+
* node_modules/ and are flagged with `workspace: true` on the returned SkillInfo.
|
|
4
6
|
* Returns metadata for each discovered skill package.
|
|
5
7
|
*/
|
|
6
8
|
export declare function scanNodeModules(cwd: string): Promise<SkillInfo[]>;
|
package/dist/scanner/index.js
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
import { readdir, access } from 'node:fs/promises';
|
|
1
|
+
import { readdir, access, lstat } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { readPackageJson, parseSkillpmField } from '../manifest/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Return true if the given path is a symbolic link (or on Windows, a junction).
|
|
6
|
+
* Does NOT follow the link — uses lstat so the link itself is inspected.
|
|
7
|
+
*/
|
|
8
|
+
async function isSymlink(p) {
|
|
9
|
+
try {
|
|
10
|
+
const s = await lstat(p);
|
|
11
|
+
return s.isSymbolicLink();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
4
17
|
/**
|
|
5
18
|
* Scan node_modules/ for packages that contain a skills/<name>/SKILL.md file.
|
|
19
|
+
* Also follows symlinks — npm workspace packages appear as symlinks inside
|
|
20
|
+
* node_modules/ and are flagged with `workspace: true` on the returned SkillInfo.
|
|
6
21
|
* Returns metadata for each discovered skill package.
|
|
7
22
|
*/
|
|
8
23
|
export async function scanNodeModules(cwd) {
|
|
@@ -30,14 +45,16 @@ export async function scanNodeModules(cwd) {
|
|
|
30
45
|
}
|
|
31
46
|
for (const scopedEntry of scopedEntries) {
|
|
32
47
|
const pkgDir = join(scopeDir, scopedEntry);
|
|
33
|
-
const
|
|
48
|
+
const symlink = await isSymlink(pkgDir);
|
|
49
|
+
const skill = await tryReadSkill(pkgDir, symlink);
|
|
34
50
|
if (skill)
|
|
35
51
|
skills.push(skill);
|
|
36
52
|
}
|
|
37
53
|
}
|
|
38
54
|
else {
|
|
39
55
|
const pkgDir = join(nodeModulesDir, entry);
|
|
40
|
-
const
|
|
56
|
+
const symlink = await isSymlink(pkgDir);
|
|
57
|
+
const skill = await tryReadSkill(pkgDir, symlink);
|
|
41
58
|
if (skill)
|
|
42
59
|
skills.push(skill);
|
|
43
60
|
}
|
|
@@ -53,7 +70,7 @@ async function hasDir(dir) {
|
|
|
53
70
|
return false;
|
|
54
71
|
}
|
|
55
72
|
}
|
|
56
|
-
async function tryReadSkill(pkgDir) {
|
|
73
|
+
async function tryReadSkill(pkgDir, workspace = false) {
|
|
57
74
|
const pkg = await readPackageJson(pkgDir);
|
|
58
75
|
if (!pkg)
|
|
59
76
|
return null;
|
|
@@ -85,6 +102,8 @@ async function tryReadSkill(pkgDir) {
|
|
|
85
102
|
skillDir,
|
|
86
103
|
mcpServers: skillpm?.mcpServers ?? [],
|
|
87
104
|
configsDir: hasConfigs ? configsDir : undefined,
|
|
105
|
+
configPrefix: skillpm?.configPrefix,
|
|
106
|
+
workspace: workspace || undefined,
|
|
88
107
|
};
|
|
89
108
|
}
|
|
90
109
|
// Fallback: root SKILL.md (legacy format)
|
|
@@ -103,6 +122,8 @@ async function tryReadSkill(pkgDir) {
|
|
|
103
122
|
mcpServers: skillpm?.mcpServers ?? [],
|
|
104
123
|
legacy: true,
|
|
105
124
|
configsDir: hasConfigs ? configsDir : undefined,
|
|
125
|
+
configPrefix: skillpm?.configPrefix,
|
|
126
|
+
workspace: workspace || undefined,
|
|
106
127
|
};
|
|
107
128
|
}
|
|
108
129
|
/**
|