@botdocs/cli 0.9.0 → 0.9.1

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.
@@ -280,12 +280,26 @@ export const DETECTORS = {
280
280
  scanPaths: () => [],
281
281
  },
282
282
  codex: {
283
+ // Codex skills are nested SKILL.md directories, mirroring claude:
284
+ // ~/.codex/skills/<slug>/SKILL.md (developers.openai.com/codex/skills).
283
285
  pathPrefix: 'codex/',
284
- extensions: ['.md'],
285
- nested: false,
286
- slugFor: (abs) => path.basename(abs).replace(/\.md$/, '') || null,
287
- canonicalFilename: (slug) => `codex/${slug}.md`,
288
- scanPaths: (_homeDir, projectRoot, isGitRepo) => isGitRepo ? [path.join(projectRoot, '.codex', 'skills')] : [],
286
+ extensions: ['/SKILL.md'],
287
+ nested: true,
288
+ slugFor: (abs, root) => {
289
+ const rel = path.relative(root, abs).split(path.sep).join('/');
290
+ if (!rel.endsWith('/SKILL.md'))
291
+ return null;
292
+ const parts = rel.split('/');
293
+ if (parts.length < 2)
294
+ return null;
295
+ return parts[parts.length - 2] ?? null;
296
+ },
297
+ canonicalFilename: (slug) => `codex/${slug}/SKILL.md`,
298
+ // Global only — Codex skills live under ~/.codex/skills/.
299
+ scanPaths: (homeDir) => [path.join(homeDir, '.codex', 'skills')],
300
+ includeAdjacent: true,
301
+ skillRoot: (abs) => path.dirname(abs),
302
+ canonicalAdjacentFilename: (slug, relPath) => `codex/${slug}/${relPath}`,
289
303
  },
290
304
  copilot: {
291
305
  pathPrefix: 'copilot/instructions/',
@@ -297,36 +311,90 @@ export const DETECTORS = {
297
311
  scanPaths: (_homeDir, projectRoot, isGitRepo) => isGitRepo ? [path.join(projectRoot, '.github', 'instructions')] : [],
298
312
  },
299
313
  windsurf: {
314
+ // Windsurf reads project rules from <proj>/.windsurf/rules/<slug>.md
315
+ // (docs.windsurf.com). Flat .md rule files, project-scoped (git repo).
300
316
  pathPrefix: 'windsurf/rules/',
301
317
  extensions: ['.md'],
302
318
  nested: false,
303
319
  slugFor: (abs) => path.basename(abs).replace(/\.md$/, '') || null,
304
320
  canonicalFilename: (slug) => `windsurf/rules/${slug}.md`,
305
- scanPaths: (_homeDir, projectRoot, isGitRepo) => isGitRepo ? [path.join(projectRoot, '.codeium', 'windsurf-rules')] : [],
321
+ scanPaths: (_homeDir, projectRoot, isGitRepo) => isGitRepo ? [path.join(projectRoot, '.windsurf', 'rules')] : [],
306
322
  },
307
323
  gemini: {
324
+ // Gemini CLI has NO per-skill file directory. It uses hierarchical
325
+ // GEMINI.md context files (~/.gemini/GEMINI.md global, ./GEMINI.md
326
+ // project) — there's nothing to auto-discover and nowhere to drop a
327
+ // per-skill file. The entry stays present so the ecosystem still exists
328
+ // for compile/variants, but discovery returns nothing (like chatgpt) and
329
+ // install routes it to `manual` (see detectDestination).
308
330
  pathPrefix: 'gemini/instructions/',
309
331
  extensions: ['.md'],
310
332
  nested: false,
311
333
  slugFor: (abs) => path.basename(abs).replace(/\.md$/, '') || null,
312
334
  canonicalFilename: (slug) => `gemini/instructions/${slug}.md`,
313
- scanPaths: (homeDir) => [path.join(homeDir, '.gemini', 'instructions')],
335
+ // No canonical on-disk location — nothing to discover.
336
+ scanPaths: () => [],
314
337
  },
315
338
  antigravity: {
339
+ // Antigravity skills are nested SKILL.md directories, mirroring claude:
340
+ // ~/.gemini/antigravity/skills/<slug>/SKILL.md (global) and
341
+ // <proj>/.agent/skills/<slug>/SKILL.md (project)
342
+ // (antigravity.google/docs/skills + Google Codelabs). Keep the existing
343
+ // `antigravity/skills/` canonical prefix, now nested-with-SKILL.md.
316
344
  pathPrefix: 'antigravity/skills/',
317
- extensions: ['.md'],
318
- nested: false,
319
- slugFor: (abs) => path.basename(abs).replace(/\.md$/, '') || null,
320
- canonicalFilename: (slug) => `antigravity/skills/${slug}.md`,
321
- scanPaths: (homeDir) => [path.join(homeDir, '.gemini', 'antigravity', 'skills')],
345
+ extensions: ['/SKILL.md'],
346
+ nested: true,
347
+ slugFor: (abs, root) => {
348
+ const rel = path.relative(root, abs).split(path.sep).join('/');
349
+ if (!rel.endsWith('/SKILL.md'))
350
+ return null;
351
+ const parts = rel.split('/');
352
+ if (parts.length < 2)
353
+ return null;
354
+ return parts[parts.length - 2] ?? null;
355
+ },
356
+ canonicalFilename: (slug) => `antigravity/skills/${slug}/SKILL.md`,
357
+ // Global ~/.gemini/antigravity/skills always; project <proj>/.agent/skills
358
+ // only inside a git repo.
359
+ scanPaths: (homeDir, projectRoot, isGitRepo) => {
360
+ const paths = [path.join(homeDir, '.gemini', 'antigravity', 'skills')];
361
+ if (isGitRepo)
362
+ paths.push(path.join(projectRoot, '.agent', 'skills'));
363
+ return paths;
364
+ },
365
+ includeAdjacent: true,
366
+ skillRoot: (abs) => path.dirname(abs),
367
+ canonicalAdjacentFilename: (slug, relPath) => `antigravity/skills/${slug}/${relPath}`,
322
368
  },
323
369
  opencode: {
324
- pathPrefix: 'opencode/instructions/',
325
- extensions: ['.md'],
326
- nested: false,
327
- slugFor: (abs) => path.basename(abs).replace(/\.md$/, '') || null,
328
- canonicalFilename: (slug) => `opencode/instructions/${slug}.md`,
329
- scanPaths: (homeDir) => [path.join(homeDir, '.config', 'opencode', 'instructions')],
370
+ // OpenCode skills are nested SKILL.md directories, mirroring claude:
371
+ // ~/.config/opencode/skills/<slug>/SKILL.md (global) and
372
+ // <proj>/.opencode/skills/<slug>/SKILL.md (project)
373
+ // (opencode.ai/docs/skills).
374
+ pathPrefix: 'opencode/',
375
+ extensions: ['/SKILL.md'],
376
+ nested: true,
377
+ slugFor: (abs, root) => {
378
+ const rel = path.relative(root, abs).split(path.sep).join('/');
379
+ if (!rel.endsWith('/SKILL.md'))
380
+ return null;
381
+ const parts = rel.split('/');
382
+ if (parts.length < 2)
383
+ return null;
384
+ return parts[parts.length - 2] ?? null;
385
+ },
386
+ canonicalFilename: (slug) => `opencode/${slug}/SKILL.md`,
387
+ // Global ~/.config/opencode/skills always; project <proj>/.opencode/skills
388
+ // only inside a git repo.
389
+ scanPaths: (homeDir, projectRoot, isGitRepo) => {
390
+ const paths = [path.join(homeDir, '.config', 'opencode', 'skills')];
391
+ if (isGitRepo)
392
+ paths.push(path.join(projectRoot, '.opencode', 'skills'));
393
+ return paths;
394
+ },
395
+ includeAdjacent: true,
396
+ skillRoot: (abs) => path.dirname(abs),
397
+ canonicalAdjacentFilename: (slug, relPath) => `opencode/${slug}/${relPath}`,
330
398
  },
331
399
  };
332
400
  export const SUPPORTED_TOOLS = Object.keys(DETECTORS);
package/dist/index.js CHANGED
@@ -106,7 +106,7 @@ program
106
106
  });
107
107
  program
108
108
  .command('install <ref>')
109
- .description('Install a skill or bundle locally (skills go to ~/.claude/skills/, project files to .cursor/rules/, .codex/skills/, .github/instructions/, etc.)')
109
+ .description('Install a skill or bundle locally (skills go to ~/.claude/skills/, project files to .cursor/rules/, .github/instructions/, .windsurf/rules/, etc.)')
110
110
  .option('--project <dir>', 'Override the project root used for project-local files')
111
111
  .option('--flat', 'Skip the {scope} subdirectory in install paths (collision-prone, not recommended)')
112
112
  .option('--clean', 'Wipe-and-reinstall instead of additive')
@@ -47,10 +47,24 @@ export function detectDestination(srcRelative, ctx) {
47
47
  };
48
48
  }
49
49
  if (src.startsWith('codex/')) {
50
- return {
51
- kind: 'project',
52
- dest: path.join(ctx.projectDir, '.codex', 'skills', path.basename(src)),
53
- };
50
+ // Codex skills are nested SKILL.md directories at
51
+ // ~/.codex/skills/<slug>/SKILL.md (developers.openai.com/codex/skills).
52
+ // New canonical form: codex/<slug>/SKILL.md (+ adjacent files). Strip the
53
+ // `codex/<slug>/` prefix and keep the relpath under ~/.codex/skills/<slug>/.
54
+ //
55
+ // Backward-compat: BotDocs published before this fix stored flat
56
+ // `codex/<slug>.md`. We detect the flat form (no inner slash) and route it
57
+ // to the new nested destination ~/.codex/skills/<slug>/SKILL.md so already-
58
+ // published docs still install where Codex reads them.
59
+ const remainder = src.slice('codex/'.length);
60
+ const codexBase = path.join(ctx.homeDir, '.codex', 'skills');
61
+ if (!remainder.includes('/')) {
62
+ // Flat legacy form `codex/<slug>.md` → nested SKILL.md.
63
+ const slug = remainder.replace(/\.md$/, '');
64
+ return { kind: 'global', dest: path.join(codexBase, slug, 'SKILL.md') };
65
+ }
66
+ const finalName = remainder.replace(/^[^/]+\//, '');
67
+ return { kind: 'global', dest: path.join(codexBase, ctx.slug, finalName) };
54
68
  }
55
69
  if (src.startsWith('copilot/instructions/')) {
56
70
  // GitHub Copilot custom instructions live in .github/instructions/
@@ -61,36 +75,66 @@ export function detectDestination(srcRelative, ctx) {
61
75
  };
62
76
  }
63
77
  if (src.startsWith('windsurf/rules/')) {
64
- // Windsurf (Codeium) reads project rules from .codeium/windsurf-rules/
65
- // (https://docs.codeium.com/windsurf/cascade#windsurfrules).
78
+ // Windsurf reads project rules from <proj>/.windsurf/rules/<slug>.md
79
+ // (docs.windsurf.com). Flat .md rule, project-scoped. The canonical form
80
+ // is already flat, so basename() is the right leaf either way.
66
81
  return {
67
82
  kind: 'project',
68
- dest: path.join(ctx.projectDir, '.codeium', 'windsurf-rules', path.basename(src)),
83
+ dest: path.join(ctx.projectDir, '.windsurf', 'rules', path.basename(src)),
69
84
  };
70
85
  }
71
- if (src.startsWith('gemini/instructions/')) {
72
- // Gemini CLI reads global instructions from ~/.gemini/instructions/
73
- // (https://github.com/google-gemini/gemini-cli).
74
- return {
75
- kind: 'global',
76
- dest: path.join(ctx.homeDir, '.gemini', 'instructions', path.basename(src)),
77
- };
86
+ if (src.startsWith('gemini/')) {
87
+ // Gemini CLI has NO per-skill file directory — it uses hierarchical
88
+ // GEMINI.md context files (~/.gemini/GEMINI.md global, ./GEMINI.md
89
+ // project). There's no real path to write to, so route to `manual` (like
90
+ // chatgpt): install surfaces the content for the user to paste into their
91
+ // GEMINI.md or @import it, rather than fabricating ~/.gemini/instructions/.
92
+ return { kind: 'manual', dest: src };
78
93
  }
79
94
  if (src.startsWith('antigravity/skills/')) {
80
- // Google Antigravity reads skills from ~/.gemini/antigravity/skills/
81
- // (shares the gemini config tree).
82
- return {
83
- kind: 'global',
84
- dest: path.join(ctx.homeDir, '.gemini', 'antigravity', 'skills', path.basename(src)),
85
- };
95
+ // Antigravity skills are nested SKILL.md directories
96
+ // (antigravity.google/docs/skills). Project: <proj>/.agent/skills/<slug>/…
97
+ // Global mirror: ~/.gemini/antigravity/skills/<slug>/… We prefer the
98
+ // project destination when a project dir applies (matching cursor/codex-
99
+ // commands which default to project).
100
+ //
101
+ // New canonical form: antigravity/skills/<slug>/SKILL.md (+ adjacent).
102
+ // Strip the `antigravity/skills/<slug>/` prefix, keep the relpath.
103
+ //
104
+ // Backward-compat: docs published before this fix stored flat
105
+ // `antigravity/skills/<slug>.md`. Detect the flat form and route it to the
106
+ // new nested destination so already-published docs still install.
107
+ const remainder = src.slice('antigravity/skills/'.length);
108
+ const agentBase = path.join(ctx.projectDir, '.agent', 'skills');
109
+ if (!remainder.includes('/')) {
110
+ const slug = remainder.replace(/\.md$/, '');
111
+ return { kind: 'project', dest: path.join(agentBase, slug, 'SKILL.md') };
112
+ }
113
+ const finalName = remainder.replace(/^[^/]+\//, '');
114
+ return { kind: 'project', dest: path.join(agentBase, ctx.slug, finalName) };
86
115
  }
87
- if (src.startsWith('opencode/instructions/')) {
88
- // OpenCode (SST) reads instructions from ~/.config/opencode/instructions/
89
- // (https://github.com/sst/opencode).
90
- return {
91
- kind: 'global',
92
- dest: path.join(ctx.homeDir, '.config', 'opencode', 'instructions', path.basename(src)),
93
- };
116
+ if (src.startsWith('opencode/')) {
117
+ // OpenCode skills are nested SKILL.md directories (opencode.ai/docs/skills).
118
+ // Project: <proj>/.opencode/skills/<slug>/… Global mirror:
119
+ // ~/.config/opencode/skills/<slug>/… We prefer the project destination
120
+ // when a project dir applies (matching cursor/codex-commands).
121
+ //
122
+ // New canonical form: opencode/<slug>/SKILL.md (+ adjacent). Strip the
123
+ // `opencode/<slug>/` prefix, keep the relpath.
124
+ //
125
+ // Backward-compat: docs published before this fix stored flat
126
+ // `opencode/instructions/<slug>.md`. Detect that legacy prefix and route
127
+ // it to the new nested destination so already-published docs still install.
128
+ const opencodeBase = path.join(ctx.projectDir, '.opencode', 'skills');
129
+ if (src.startsWith('opencode/instructions/')) {
130
+ const slug = path.basename(src).replace(/\.md$/, '');
131
+ return { kind: 'project', dest: path.join(opencodeBase, slug, 'SKILL.md') };
132
+ }
133
+ const remainder = src.slice('opencode/'.length);
134
+ const finalName = remainder.includes('/')
135
+ ? remainder.replace(/^[^/]+\//, '')
136
+ : remainder;
137
+ return { kind: 'project', dest: path.join(opencodeBase, ctx.slug, finalName) };
94
138
  }
95
139
  if (src.startsWith('chatgpt/')) {
96
140
  return { kind: 'manual', dest: src };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botdocs/cli",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "CLI for BotDocs — author, publish, install, and sync agent skills across Claude, Claude Code, Cursor, Codex, ChatGPT, Windsurf, Copilot, Gemini, Antigravity, and OpenCode.",
5
5
  "keywords": [
6
6
  "botdocs",