dogsbay 0.2.0-beta.6 → 0.2.0-beta.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.
@@ -17,7 +17,7 @@
17
17
  * See plans/dogsbay-agent-skills.md for the four-tier ownership
18
18
  * model and how this fits.
19
19
  */
20
- import { existsSync, mkdirSync, symlinkSync, unlinkSync, writeFileSync, readlinkSync } from "node:fs";
20
+ import { existsSync, lstatSync, mkdirSync, readdirSync, symlinkSync, unlinkSync, writeFileSync, statSync, readlinkSync } from "node:fs";
21
21
  import { dirname, join, relative, resolve } from "node:path";
22
22
  import { fileURLToPath } from "node:url";
23
23
  import pc from "picocolors";
@@ -25,15 +25,22 @@ const SUPPORTED_AGENTS = ["claude", "cursor"];
25
25
  const AGENT_TARGETS = {
26
26
  claude: {
27
27
  name: "claude",
28
- path: ".claude/skills/dogsbay",
29
- label: "Claude Code (.claude/skills/dogsbay/)",
28
+ skillsDir: ".claude/skills",
29
+ label: "Claude Code",
30
30
  },
31
31
  cursor: {
32
32
  name: "cursor",
33
- path: ".cursor/rules/dogsbay",
34
- label: "Cursor (.cursor/rules/dogsbay/)",
33
+ skillsDir: ".cursor/rules",
34
+ label: "Cursor",
35
35
  },
36
36
  };
37
+ /**
38
+ * Prefix every Dogsbay-shipped skill so it doesn't collide with
39
+ * the user's own skills under the same agent's discovery path.
40
+ */
41
+ const PLATFORM_PREFIX = "dogsbay-";
42
+ const SITE_PREFIX = "dogsbay-site-";
43
+ const PLUGIN_PREFIX = "dogsbay-plugin-";
37
44
  export async function agentInstall(cwd, options) {
38
45
  const projectRoot = resolve(cwd || ".");
39
46
  // Resolve the bundled platform-skills directory. We're running
@@ -99,13 +106,58 @@ The agent loader checks overrides first.
99
106
  if (!existsSync(pluginsDir)) {
100
107
  mkdirSync(pluginsDir, { recursive: true });
101
108
  }
102
- // 2. Per-agent discovery symlinks.
109
+ // 2. Per-agent discovery — symlink each skill INDIVIDUALLY at
110
+ // the top level of the agent's skills dir. Claude Code (and
111
+ // similar harnesses) discover skills as direct children of
112
+ // .claude/skills/, each containing a SKILL.md. Nested
113
+ // .claude/skills/dogsbay/platform/<skill>/ doesn't get scanned.
103
114
  for (const agent of agents) {
104
115
  const target = AGENT_TARGETS[agent];
105
- const agentLink = join(projectRoot, target.path);
106
- mkdirSync(dirname(agentLink), { recursive: true });
107
- refreshSymlink(agentLink, skillsDir);
108
- console.log(pc.green(` ✓ ${target.label} .dogsbay/skills/`));
116
+ const agentSkillsDir = join(projectRoot, target.skillsDir);
117
+ mkdirSync(agentSkillsDir, { recursive: true });
118
+ let installed = 0;
119
+ let removed = 0;
120
+ // Platform skills: dogsbay-<name>
121
+ for (const entry of readSkillDirs(join(skillsDir, "platform"))) {
122
+ const dest = join(agentSkillsDir, `${PLATFORM_PREFIX}${entry.name}`);
123
+ refreshSymlink(dest, entry.path);
124
+ installed++;
125
+ }
126
+ // Site skills: dogsbay-site-<name> (skipping overrides/ and the README)
127
+ for (const entry of readSkillDirs(join(skillsDir, "site"))) {
128
+ if (entry.name === "overrides")
129
+ continue;
130
+ const dest = join(agentSkillsDir, `${SITE_PREFIX}${entry.name}`);
131
+ refreshSymlink(dest, entry.path);
132
+ installed++;
133
+ }
134
+ // Plugin skills: dogsbay-plugin-<name>
135
+ for (const entry of readSkillDirs(join(skillsDir, "plugins"))) {
136
+ const dest = join(agentSkillsDir, `${PLUGIN_PREFIX}${entry.name}`);
137
+ refreshSymlink(dest, entry.path);
138
+ installed++;
139
+ }
140
+ // Sweep any stale dogsbay-* symlinks whose target no longer
141
+ // exists (e.g. user uninstalled a plugin, or a platform skill
142
+ // was renamed across CLI versions).
143
+ if (existsSync(agentSkillsDir)) {
144
+ for (const name of readdirSync(agentSkillsDir)) {
145
+ if (!name.startsWith("dogsbay-"))
146
+ continue;
147
+ const path = join(agentSkillsDir, name);
148
+ if (isBrokenSymlink(path)) {
149
+ try {
150
+ unlinkSync(path);
151
+ removed++;
152
+ }
153
+ catch { /* ignore */ }
154
+ }
155
+ }
156
+ }
157
+ console.log(pc.green(` ✓ ${target.label}: ${installed} skills under ${target.skillsDir}/`));
158
+ if (removed > 0) {
159
+ console.log(pc.gray(` (cleaned up ${removed} stale symlink${removed === 1 ? "" : "s"})`));
160
+ }
109
161
  }
110
162
  console.log("");
111
163
  console.log(pc.cyan("Next:"));
@@ -180,6 +232,34 @@ function printDetected(projectRoot) {
180
232
  console.log("");
181
233
  console.log(`Supported agents: ${SUPPORTED_AGENTS.join(", ")}`);
182
234
  }
235
+ /**
236
+ * List each subdirectory of `dir` that looks like a skill — i.e.
237
+ * contains a SKILL.md file. Returns an empty array if `dir`
238
+ * doesn't exist or has no skill subdirs.
239
+ */
240
+ function readSkillDirs(dir) {
241
+ if (!existsSync(dir))
242
+ return [];
243
+ const out = [];
244
+ for (const name of readdirSync(dir)) {
245
+ if (name.startsWith("."))
246
+ continue;
247
+ const path = join(dir, name);
248
+ let isDir = false;
249
+ try {
250
+ isDir = statSync(path).isDirectory();
251
+ }
252
+ catch {
253
+ isDir = false;
254
+ }
255
+ if (!isDir)
256
+ continue;
257
+ if (!existsSync(join(path, "SKILL.md")))
258
+ continue;
259
+ out.push({ name, path });
260
+ }
261
+ return out;
262
+ }
183
263
  /**
184
264
  * Replace any existing entry at `linkPath` with a fresh symlink
185
265
  * pointing at `target`. Idempotent: if the link already points at
@@ -205,12 +285,21 @@ function refreshSymlink(linkPath, target) {
205
285
  }
206
286
  symlinkSync(target, linkPath, "dir");
207
287
  }
288
+ /**
289
+ * True only when `p` IS a symlink AND its target doesn't exist.
290
+ * Used by the cleanup pass to remove links pointing at gone paths
291
+ * (e.g. an uninstalled plugin or a renamed platform skill across
292
+ * CLI versions).
293
+ */
208
294
  function isBrokenSymlink(p) {
209
295
  try {
210
- readlinkSync(p);
211
- return true; // it's a symlink, regardless of target validity
296
+ const lst = lstatSync(p);
297
+ if (!lst.isSymbolicLink())
298
+ return false;
212
299
  }
213
300
  catch {
214
301
  return false;
215
302
  }
303
+ // It's a symlink. existsSync follows the link; false → target gone.
304
+ return !existsSync(p);
216
305
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dogsbay",
3
- "version": "0.2.0-beta.6",
3
+ "version": "0.2.0-beta.7",
4
4
  "description": "CLI for Dogsbay — scaffold, build, and serve documentation sites with markdown / MkDocs / Obsidian / OpenAPI sources",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,14 +31,14 @@
31
31
  "picocolors": "^1.1.0",
32
32
  "prompts": "^2.4.2",
33
33
  "yaml": "^2.8.3",
34
- "@dogsbay/format-astro": "0.2.0-beta.6",
35
- "@dogsbay/format-obsidian": "0.2.0-beta.6",
36
- "@dogsbay/format-mkdocs": "0.2.0-beta.6",
37
- "@dogsbay/format-mdx": "0.2.0-beta.6",
38
- "@dogsbay/format-starlight": "0.2.0-beta.6",
39
- "@dogsbay/format-dogsbay-md": "0.2.0-beta.6",
40
- "@dogsbay/format-openapi": "0.2.0-beta.6",
41
- "@dogsbay/types": "0.2.0-beta.6"
34
+ "@dogsbay/format-mkdocs": "0.2.0-beta.7",
35
+ "@dogsbay/format-astro": "0.2.0-beta.7",
36
+ "@dogsbay/format-mdx": "0.2.0-beta.7",
37
+ "@dogsbay/format-obsidian": "0.2.0-beta.7",
38
+ "@dogsbay/format-starlight": "0.2.0-beta.7",
39
+ "@dogsbay/format-dogsbay-md": "0.2.0-beta.7",
40
+ "@dogsbay/format-openapi": "0.2.0-beta.7",
41
+ "@dogsbay/types": "0.2.0-beta.7"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^22.0.0",