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 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.4';
10
+ const VERSION = '0.0.7';
11
11
  const HELP = `
12
12
  skillpm — Agent Skill package manager
13
13
 
@@ -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
- log.info(`Linking ${log.skill(skill.name, skill.version)} into agent directories`);
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
- log.info(`Copying config files from ${log.skill(skill.name, skill.version)}`);
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) {
@@ -1,8 +1,20 @@
1
1
  /**
2
2
  * Copy all files from a skill's configs/ directory to the workspace,
3
- * auto-prefixing filenames with the package name.
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
  */
@@ -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
- * Auto-prefix a filename with the package name to avoid conflicts.
46
- * e.g. "reviewer.md" with package "my-skill" → "my-skill--reviewer.md"
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, packageName) {
64
+ function prefixFilename(relPath, prefix) {
49
65
  const dir = dirname(relPath);
50
66
  const file = basename(relPath);
51
- const prefixed = `${packageName}--${file}`;
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 with the package name.
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, packageName);
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
  }
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  export const SkillpmFieldSchema = z
3
3
  .object({
4
4
  mcpServers: z.array(z.string()).optional(),
5
+ configPrefix: z.string().optional(),
5
6
  })
6
7
  .strict()
7
8
  .optional();
@@ -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[]>;
@@ -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 skill = await tryReadSkill(pkgDir);
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 skill = await tryReadSkill(pkgDir);
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillpm",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Package manager for Agent Skills. Built on npm.",
5
5
  "type": "module",
6
6
  "bin": {