@biggora/claude-plugins 1.2.2 → 1.3.0

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.
Files changed (84) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/registry/registry.json +15 -0
  4. package/specs/coding.md +6 -0
  5. package/src/commands/skills/add.js +63 -7
  6. package/src/commands/skills/list.js +23 -52
  7. package/src/commands/skills/remove.js +26 -27
  8. package/src/commands/skills/resolve.js +155 -0
  9. package/src/commands/skills/update.js +58 -74
  10. package/src/skills/nest-best-practices/SKILL.md +251 -0
  11. package/src/skills/nest-best-practices/references/best-practices-request-lifecycle.md +158 -0
  12. package/src/skills/nest-best-practices/references/cli-monorepo.md +106 -0
  13. package/src/skills/nest-best-practices/references/cli-overview.md +157 -0
  14. package/src/skills/nest-best-practices/references/core-controllers.md +165 -0
  15. package/src/skills/nest-best-practices/references/core-dependency-injection.md +179 -0
  16. package/src/skills/nest-best-practices/references/core-middleware.md +139 -0
  17. package/src/skills/nest-best-practices/references/core-modules.md +138 -0
  18. package/src/skills/nest-best-practices/references/core-providers.md +188 -0
  19. package/src/skills/nest-best-practices/references/faq-raw-body-hybrid.md +122 -0
  20. package/src/skills/nest-best-practices/references/fundamentals-circular-dependency.md +89 -0
  21. package/src/skills/nest-best-practices/references/fundamentals-custom-decorators.md +107 -0
  22. package/src/skills/nest-best-practices/references/fundamentals-dynamic-modules.md +125 -0
  23. package/src/skills/nest-best-practices/references/fundamentals-exception-filters.md +202 -0
  24. package/src/skills/nest-best-practices/references/fundamentals-execution-context.md +107 -0
  25. package/src/skills/nest-best-practices/references/fundamentals-guards.md +136 -0
  26. package/src/skills/nest-best-practices/references/fundamentals-interceptors.md +187 -0
  27. package/src/skills/nest-best-practices/references/fundamentals-lazy-loading.md +89 -0
  28. package/src/skills/nest-best-practices/references/fundamentals-lifecycle-events.md +87 -0
  29. package/src/skills/nest-best-practices/references/fundamentals-module-reference.md +107 -0
  30. package/src/skills/nest-best-practices/references/fundamentals-pipes.md +197 -0
  31. package/src/skills/nest-best-practices/references/fundamentals-provider-scopes.md +92 -0
  32. package/src/skills/nest-best-practices/references/fundamentals-testing.md +142 -0
  33. package/src/skills/nest-best-practices/references/graphql-overview.md +233 -0
  34. package/src/skills/nest-best-practices/references/graphql-resolvers-mutations.md +199 -0
  35. package/src/skills/nest-best-practices/references/graphql-scalars-unions-enums.md +180 -0
  36. package/src/skills/nest-best-practices/references/graphql-subscriptions.md +228 -0
  37. package/src/skills/nest-best-practices/references/microservices-grpc.md +175 -0
  38. package/src/skills/nest-best-practices/references/microservices-overview.md +221 -0
  39. package/src/skills/nest-best-practices/references/microservices-transports.md +119 -0
  40. package/src/skills/nest-best-practices/references/openapi-swagger.md +207 -0
  41. package/src/skills/nest-best-practices/references/recipes-authentication.md +97 -0
  42. package/src/skills/nest-best-practices/references/recipes-cqrs.md +176 -0
  43. package/src/skills/nest-best-practices/references/recipes-crud-generator.md +87 -0
  44. package/src/skills/nest-best-practices/references/recipes-documentation.md +93 -0
  45. package/src/skills/nest-best-practices/references/recipes-mongoose.md +153 -0
  46. package/src/skills/nest-best-practices/references/recipes-prisma.md +98 -0
  47. package/src/skills/nest-best-practices/references/recipes-terminus.md +148 -0
  48. package/src/skills/nest-best-practices/references/recipes-typeorm.md +122 -0
  49. package/src/skills/nest-best-practices/references/security-authorization.md +196 -0
  50. package/src/skills/nest-best-practices/references/security-cors-helmet-rate-limiting.md +204 -0
  51. package/src/skills/nest-best-practices/references/security-encryption-hashing.md +93 -0
  52. package/src/skills/nest-best-practices/references/techniques-caching.md +142 -0
  53. package/src/skills/nest-best-practices/references/techniques-compression-streaming-sse.md +194 -0
  54. package/src/skills/nest-best-practices/references/techniques-configuration.md +132 -0
  55. package/src/skills/nest-best-practices/references/techniques-database.md +153 -0
  56. package/src/skills/nest-best-practices/references/techniques-events.md +163 -0
  57. package/src/skills/nest-best-practices/references/techniques-fastify.md +137 -0
  58. package/src/skills/nest-best-practices/references/techniques-file-upload.md +140 -0
  59. package/src/skills/nest-best-practices/references/techniques-http-module.md +176 -0
  60. package/src/skills/nest-best-practices/references/techniques-logging.md +146 -0
  61. package/src/skills/nest-best-practices/references/techniques-mvc-serve-static.md +132 -0
  62. package/src/skills/nest-best-practices/references/techniques-queues.md +162 -0
  63. package/src/skills/nest-best-practices/references/techniques-serialization.md +158 -0
  64. package/src/skills/nest-best-practices/references/techniques-sessions-cookies.md +167 -0
  65. package/src/skills/nest-best-practices/references/techniques-task-scheduling.md +166 -0
  66. package/src/skills/nest-best-practices/references/techniques-validation.md +126 -0
  67. package/src/skills/nest-best-practices/references/techniques-versioning.md +153 -0
  68. package/src/skills/nest-best-practices/references/websockets-advanced.md +96 -0
  69. package/src/skills/nest-best-practices/references/websockets-gateways.md +215 -0
  70. package/src/skills/typescript-expert/SKILL.md +145 -0
  71. package/src/skills/typescript-expert/commands/typescript-fix.md +65 -0
  72. package/src/skills/typescript-expert/references/advanced-conditional-types.md +190 -0
  73. package/src/skills/typescript-expert/references/advanced-decorators.md +243 -0
  74. package/src/skills/typescript-expert/references/advanced-mapped-types.md +223 -0
  75. package/src/skills/typescript-expert/references/advanced-template-literals.md +209 -0
  76. package/src/skills/typescript-expert/references/advanced-type-guards.md +308 -0
  77. package/src/skills/typescript-expert/references/best-practices-patterns.md +313 -0
  78. package/src/skills/typescript-expert/references/best-practices-performance.md +185 -0
  79. package/src/skills/typescript-expert/references/best-practices-tsconfig.md +242 -0
  80. package/src/skills/typescript-expert/references/core-generics.md +246 -0
  81. package/src/skills/typescript-expert/references/core-interfaces-types.md +231 -0
  82. package/src/skills/typescript-expert/references/core-type-system.md +261 -0
  83. package/src/skills/typescript-expert/references/core-utility-types.md +235 -0
  84. package/src/skills/typescript-expert/references/features-ts5x.md +370 -0
package/README.md CHANGED
@@ -67,6 +67,7 @@ claude-plugins skills remove commafeed-api
67
67
  | `vite-best-practices` | frontend | Vite 8 build tool best practices — configuration, Rolldown/Oxc migration, plugin API, SSR, library mode, and virtual modules |
68
68
  | `google-merchant-api` | workflow | Work with Google Merchant Center APIs — Merchant API (v1) and Content API for Shopping (v2.1) for products, inventory, promotions, and reports |
69
69
  | `lv-aggregators-api` | workflow | Generate and manage XML product feeds for Latvian price comparison platforms — Salidzini.lv and KurPirkt.lv integration |
70
+ | `typescript-expert` | code-quality | TypeScript type system, generics, utility types, advanced patterns, tsconfig, and TS 5.x features — includes `/typescript-fix` slash command |
70
71
  | `youtube-thumbnail` | other | Generates professional YouTube thumbnails in 11 strategic styles with auto-detection of AI image backends and Pillow compositing |
71
72
 
72
73
  #### Install Examples
@@ -90,6 +91,7 @@ npx skills add https://github.com/biggora/claude-plugins-registry --skill tailwi
90
91
  npx skills add https://github.com/biggora/claude-plugins-registry --skill vite-best-practices
91
92
  npx skills add https://github.com/biggora/claude-plugins-registry --skill google-merchant-api
92
93
  npx skills add https://github.com/biggora/claude-plugins-registry --skill lv-aggregators-api
94
+ npx skills add https://github.com/biggora/claude-plugins-registry --skill typescript-expert
93
95
  ```
94
96
 
95
97
  ## Plugins
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biggora/claude-plugins",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "CLI marketplace for discovering, installing, and managing Claude Code plugins",
5
5
  "keywords": [
6
6
  "claude",
@@ -314,6 +314,21 @@
314
314
  "commands": [],
315
315
  "category": "workflow",
316
316
  "type": "skill"
317
+ },
318
+ {
319
+ "name": "typescript-expert",
320
+ "version": "1.0.0",
321
+ "description": "TypeScript language expertise — type system, generics, utility types, advanced type patterns, tsconfig configuration, and TS 5.x features with /typescript-fix slash command for auto-resolving type errors",
322
+ "author": {
323
+ "name": "biggora",
324
+ "url": "https://github.com/biggora"
325
+ },
326
+ "repository": "https://github.com/biggora/claude-plugins-registry",
327
+ "keywords": ["typescript", "types", "generics", "tsconfig", "type-checking", "code-quality"],
328
+ "license": "MIT",
329
+ "commands": ["/typescript-fix"],
330
+ "category": "code-quality",
331
+ "type": "skill"
317
332
  }
318
333
  ]
319
334
  }
package/specs/coding.md CHANGED
@@ -13,6 +13,7 @@ salidzini + kurpirkt
13
13
  https://www.salidzini.lv/shops_info.php
14
14
  https://www.kurpirkt.lv/veikaliem.php
15
15
 
16
+ nest--best-practices
16
17
 
17
18
  add vite-best-practices, tailwindcss-best-practices skills to registry/registry.json and README.md
18
19
 
@@ -20,5 +21,10 @@ create skill google-merchant-api in src/skills/google-merchant-api. The skill sh
20
21
 
21
22
  create skill lv-aggregators-api in src/skills/lv-aggregators-api. The skill should include knowledge of how to work with Aggregators APIs, how to integrate them into projects, formats and data types, etc. Aggregators Docs: https://www.salidzini.lv/shops_info.php, https://www.kurpirkt.lv/veikaliem.php
22
23
 
24
+ create skill typescript-expert src/skills/typescript-expert. The skill should include knowledge of TypeScript language features, best practices, type system, advanced types, generics, utility types, and how to use TypeScript effectively in projects.
23
25
 
26
+ create skill eslint-expert src/skills/eslint-expert. The skill should include knowledge of ESLint configuration, rules, plugins, and how to use ESLint effectively in projects for code quality and consistency.
27
+
28
+ create slash command typescript-fix for the TypeScript expert (typescript-expert) skill. with some content like this:
29
+ "You are a TypeScript expert with deep knowledge of complex typing patterns, modern JavaScript (ES6+), ESLint configuration, and enterprise-grade development practices. You specialize in building reliable, maintainable, and performant TypeScript solutions for complex business problems, with a solid understanding of JavaScript fundamentals and code quality tools. Read package.json to identify the command to run the linter. Use the `typescript-expert` skill to run `npm run typecheck`, `pnpm run typecheck`, or `tsc --noEmit` in the project directory and resolve TypeScript errors."
24
30
 
@@ -2,9 +2,10 @@ import { execFileSync } from 'node:child_process';
2
2
  import { existsSync, readFileSync, readdirSync, cpSync, rmSync, mkdirSync, writeFileSync, statSync } from 'node:fs';
3
3
  import { join, basename } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
- import { getSkillsDir } from '../../config.js';
5
+ import { getSkillsDir, getPluginsDir } from '../../config.js';
6
6
  import { fetchRegistry, findPlugin } from '../../registry.js';
7
7
  import { log, spinner } from '../../utils.js';
8
+ import { hasPluginComponents, detectComponents, findInstalledSkill } from './resolve.js';
8
9
 
9
10
  export function parseFrontmatter(content) {
10
11
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
@@ -142,18 +143,30 @@ export async function add(source, options = {}) {
142
143
  const frontmatter = parseFrontmatter(skillMd);
143
144
  const skillName = frontmatter.name || options.skill || basename(targetDir === tmpDir ? repoUrl.replace(/\.git$/, '').split('/').pop() : targetDir);
144
145
 
145
- const skillsDir = getSkillsDir();
146
- const dest = join(skillsDir, skillName);
146
+ // Detect plugin components (commands, hooks, agents) alongside SKILL.md
147
+ const components = detectComponents(targetDir);
148
+ const installAsPlugin = hasPluginComponents(targetDir);
147
149
 
148
- if (existsSync(dest)) {
149
- log.warn(`Skill "${skillName}" is already installed at ${dest}`);
150
+ const destDir = installAsPlugin ? getPluginsDir() : getSkillsDir();
151
+ const dest = join(destDir, skillName);
152
+
153
+ // Check both locations for existing installation
154
+ const existing = findInstalledSkill(skillName);
155
+ if (existing) {
156
+ log.warn(`Skill "${skillName}" is already installed at ${existing.dir}`);
150
157
  log.dim(`Run "claude-plugins skills update ${skillName}" to update it`);
151
158
  rmSync(tmpDir, { recursive: true, force: true });
152
159
  return;
153
160
  }
154
161
 
155
- // Copy skill files
156
- const spin2 = spinner(`Installing skill "${skillName}"...`);
162
+ if (existsSync(dest)) {
163
+ log.warn(`"${skillName}" already exists at ${dest}`);
164
+ rmSync(tmpDir, { recursive: true, force: true });
165
+ return;
166
+ }
167
+
168
+ const label = installAsPlugin ? 'skill + components' : 'skill';
169
+ const spin2 = spinner(`Installing ${label} "${skillName}"...`);
157
170
  spin2.start();
158
171
 
159
172
  try {
@@ -165,6 +178,25 @@ export async function add(source, options = {}) {
165
178
  rmSync(gitInDest, { recursive: true, force: true });
166
179
  }
167
180
 
181
+ // Generate .claude-plugin/plugin.json if installing as plugin and none exists
182
+ if (installAsPlugin && !existsSync(join(dest, '.claude-plugin', 'plugin.json'))) {
183
+ const pluginDir = join(dest, '.claude-plugin');
184
+ mkdirSync(pluginDir, { recursive: true });
185
+
186
+ const pluginJson = {
187
+ name: skillName,
188
+ version: '1.0.0',
189
+ description: frontmatter.description || `Skill: ${skillName}`,
190
+ commands: listCommandFiles(dest),
191
+ _generatedBy: 'claude-plugins skills add',
192
+ };
193
+
194
+ writeFileSync(
195
+ join(pluginDir, 'plugin.json'),
196
+ JSON.stringify(pluginJson, null, 2)
197
+ );
198
+ }
199
+
168
200
  // Write origin metadata
169
201
  writeFileSync(
170
202
  join(dest, '.origin.json'),
@@ -172,6 +204,8 @@ export async function add(source, options = {}) {
172
204
  {
173
205
  repository: repoUrl,
174
206
  skill: options.skill || null,
207
+ installedAs: installAsPlugin ? 'plugin-skill' : 'skill',
208
+ components,
175
209
  installedAt: new Date().toISOString(),
176
210
  },
177
211
  null,
@@ -184,6 +218,13 @@ export async function add(source, options = {}) {
184
218
  if (frontmatter.description) {
185
219
  log.dim(` ${frontmatter.description}`);
186
220
  }
221
+
222
+ if (installAsPlugin) {
223
+ const extras = components.filter((c) => c !== 'skill');
224
+ log.info(`Detected: ${components.join(', ')}`);
225
+ log.dim(`Installed as plugin so Claude Code discovers ${extras.join(', ')}`);
226
+ }
227
+
187
228
  log.dim('\nRestart Claude Code to load the skill.');
188
229
  } catch (err) {
189
230
  spin2.fail(`Failed to install skill "${skillName}"`);
@@ -192,3 +233,18 @@ export async function add(source, options = {}) {
192
233
  rmSync(tmpDir, { recursive: true, force: true });
193
234
  }
194
235
  }
236
+
237
+ /**
238
+ * List command file names (without extension) from a commands/ directory.
239
+ */
240
+ function listCommandFiles(dir) {
241
+ const cmdsDir = join(dir, 'commands');
242
+ if (!existsSync(cmdsDir)) return [];
243
+ try {
244
+ return readdirSync(cmdsDir)
245
+ .filter((f) => f.endsWith('.md'))
246
+ .map((f) => `/${f.replace(/\.md$/, '')}`);
247
+ } catch {
248
+ return [];
249
+ }
250
+ }
@@ -1,52 +1,23 @@
1
- import { readdirSync, existsSync, readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import chalk from 'chalk';
4
- import { getSkillsDir } from '../../config.js';
5
- import { log, formatTable, truncate } from '../../utils.js';
6
- import { parseFrontmatter } from './add.js';
7
-
8
- export async function list() {
9
- const skillsDir = getSkillsDir();
10
- const entries = readdirSync(skillsDir, { withFileTypes: true }).filter(
11
- (e) => e.isDirectory()
12
- );
13
-
14
- if (!entries.length) {
15
- log.info('No skills installed');
16
- log.dim('Run "claude-plugins skills add <source>" to install a skill');
17
- return;
18
- }
19
-
20
- console.log(chalk.bold(`\n ${entries.length} skill${entries.length === 1 ? '' : 's'} installed\n`));
21
-
22
- const rows = entries.map((entry) => {
23
- const dir = join(skillsDir, entry.name);
24
- const skillMdPath = join(dir, 'SKILL.md');
25
- const originPath = join(dir, '.origin.json');
26
- let description = '';
27
- let repo = '-';
28
-
29
- if (existsSync(skillMdPath)) {
30
- try {
31
- const fm = parseFrontmatter(readFileSync(skillMdPath, 'utf-8'));
32
- description = fm.description || '';
33
- } catch {
34
- // ignore
35
- }
36
- }
37
-
38
- if (existsSync(originPath)) {
39
- try {
40
- const origin = JSON.parse(readFileSync(originPath, 'utf-8'));
41
- repo = origin.repository || '-';
42
- } catch {
43
- // ignore
44
- }
45
- }
46
-
47
- return [entry.name, truncate(description, 50), truncate(repo, 40)];
48
- });
49
-
50
- formatTable(rows, ['Name', 'Description', 'Repository']);
51
- console.log();
52
- }
1
+ import chalk from 'chalk';
2
+ import { log, formatTable, truncate } from '../../utils.js';
3
+ import { listAllSkills } from './resolve.js';
4
+
5
+ export async function list() {
6
+ const skills = listAllSkills();
7
+
8
+ if (!skills.length) {
9
+ log.info('No skills installed');
10
+ log.dim('Run "claude-plugins skills add <source>" to install a skill');
11
+ return;
12
+ }
13
+
14
+ console.log(chalk.bold(`\n ${skills.length} skill${skills.length === 1 ? '' : 's'} installed\n`));
15
+
16
+ const rows = skills.map(({ name, meta, location }) => {
17
+ const type = location === 'plugins' ? chalk.cyan('plugin') : chalk.dim('skill');
18
+ return [name, truncate(meta.description, 45), type, truncate(meta.repository, 35)];
19
+ });
20
+
21
+ formatTable(rows, ['Name', 'Description', 'Type', 'Repository']);
22
+ console.log();
23
+ }
@@ -1,27 +1,26 @@
1
- import { existsSync, rmSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { getSkillsDir } from '../../config.js';
4
- import { log, spinner } from '../../utils.js';
5
-
6
- export async function remove(name) {
7
- const dest = join(getSkillsDir(), name);
8
-
9
- if (!existsSync(dest)) {
10
- log.error(`Skill "${name}" is not installed`);
11
- log.dim('Run "claude-plugins skills list" to see installed skills');
12
- process.exit(1);
13
- }
14
-
15
- const spin = spinner(`Removing skill "${name}"...`);
16
- spin.start();
17
-
18
- try {
19
- rmSync(dest, { recursive: true, force: true });
20
- spin.succeed(`Removed skill "${name}"`);
21
- log.dim('Restart Claude Code to apply changes.');
22
- } catch (err) {
23
- spin.fail(`Failed to remove skill "${name}"`);
24
- log.error(err.message);
25
- process.exit(1);
26
- }
27
- }
1
+ import { rmSync } from 'node:fs';
2
+ import { log, spinner } from '../../utils.js';
3
+ import { findInstalledSkill } from './resolve.js';
4
+
5
+ export async function remove(name) {
6
+ const found = findInstalledSkill(name);
7
+
8
+ if (!found) {
9
+ log.error(`Skill "${name}" is not installed`);
10
+ log.dim('Run "claude-plugins skills list" to see installed skills');
11
+ process.exit(1);
12
+ }
13
+
14
+ const spin = spinner(`Removing skill "${name}"...`);
15
+ spin.start();
16
+
17
+ try {
18
+ rmSync(found.dir, { recursive: true, force: true });
19
+ spin.succeed(`Removed skill "${name}" from ${found.location}`);
20
+ log.dim('Restart Claude Code to apply changes.');
21
+ } catch (err) {
22
+ spin.fail(`Failed to remove skill "${name}"`);
23
+ log.error(err.message);
24
+ process.exit(1);
25
+ }
26
+ }
@@ -0,0 +1,155 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { getSkillsDir, getPluginsDir } from '../../config.js';
4
+ import { parseFrontmatter } from './add.js';
5
+
6
+ /**
7
+ * Plugin component directories that Claude Code auto-discovers.
8
+ * When a skill repo contains any of these, it must be installed
9
+ * as a plugin so Claude Code can find them.
10
+ */
11
+ export const PLUGIN_COMPONENT_DIRS = ['commands', 'hooks', 'agents'];
12
+
13
+ /**
14
+ * Check if a directory contains plugin components (commands, hooks, agents)
15
+ * beyond just SKILL.md + references.
16
+ */
17
+ export function hasPluginComponents(dir) {
18
+ return PLUGIN_COMPONENT_DIRS.some((name) => {
19
+ const p = join(dir, name);
20
+ return existsSync(p);
21
+ });
22
+ }
23
+
24
+ /**
25
+ * Detect which plugin components exist in a directory.
26
+ * Returns an array of found component names.
27
+ */
28
+ export function detectComponents(dir) {
29
+ const found = [];
30
+ if (existsSync(join(dir, 'SKILL.md'))) found.push('skill');
31
+ for (const name of PLUGIN_COMPONENT_DIRS) {
32
+ if (existsSync(join(dir, name))) found.push(name);
33
+ }
34
+ return found;
35
+ }
36
+
37
+ /**
38
+ * Read .origin.json from a skill/plugin directory.
39
+ */
40
+ export function readOrigin(dir) {
41
+ const originPath = join(dir, '.origin.json');
42
+ if (!existsSync(originPath)) return null;
43
+ try {
44
+ return JSON.parse(readFileSync(originPath, 'utf-8'));
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Read skill metadata from a directory (SKILL.md frontmatter + .origin.json).
52
+ */
53
+ export function readSkillMeta(dir, dirName) {
54
+ const skillMdPath = join(dir, 'SKILL.md');
55
+ const meta = { name: dirName, description: '', repository: '-' };
56
+
57
+ if (existsSync(skillMdPath)) {
58
+ try {
59
+ const fm = parseFrontmatter(readFileSync(skillMdPath, 'utf-8'));
60
+ meta.name = fm.name || dirName;
61
+ meta.description = fm.description || '';
62
+ } catch {
63
+ // ignore
64
+ }
65
+ }
66
+
67
+ const origin = readOrigin(dir);
68
+ if (origin) {
69
+ meta.repository = origin.repository || '-';
70
+ meta.installedAs = origin.installedAs || 'skill';
71
+ meta.skill = origin.skill || null;
72
+ }
73
+
74
+ return meta;
75
+ }
76
+
77
+ /**
78
+ * Find a skill by name across both ~/.claude/skills/ and ~/.claude/plugins/.
79
+ * Returns { dir, location } or null.
80
+ */
81
+ export function findInstalledSkill(name) {
82
+ const skillsDir = getSkillsDir();
83
+ const pluginsDir = getPluginsDir();
84
+
85
+ // Check ~/.claude/skills/ first
86
+ const skillPath = join(skillsDir, name);
87
+ if (existsSync(skillPath) && existsSync(join(skillPath, 'SKILL.md'))) {
88
+ return { dir: skillPath, location: 'skills' };
89
+ }
90
+
91
+ // Check ~/.claude/plugins/ for skills installed as plugins
92
+ const pluginPath = join(pluginsDir, name);
93
+ if (existsSync(pluginPath)) {
94
+ const origin = readOrigin(pluginPath);
95
+ if (origin && origin.installedAs === 'plugin-skill') {
96
+ return { dir: pluginPath, location: 'plugins' };
97
+ }
98
+ // Also check if it has a SKILL.md (might be installed via skills add)
99
+ if (existsSync(join(pluginPath, 'SKILL.md'))) {
100
+ return { dir: pluginPath, location: 'plugins' };
101
+ }
102
+ }
103
+
104
+ return null;
105
+ }
106
+
107
+ /**
108
+ * List all installed skills from both directories.
109
+ * Returns array of { name, dir, location, meta }.
110
+ */
111
+ export function listAllSkills() {
112
+ const skillsDir = getSkillsDir();
113
+ const pluginsDir = getPluginsDir();
114
+ const results = [];
115
+
116
+ // Skills from ~/.claude/skills/
117
+ try {
118
+ const entries = readdirSync(skillsDir, { withFileTypes: true });
119
+ for (const entry of entries) {
120
+ if (!entry.isDirectory()) continue;
121
+ const dir = join(skillsDir, entry.name);
122
+ if (!existsSync(join(dir, 'SKILL.md'))) continue;
123
+ results.push({
124
+ name: entry.name,
125
+ dir,
126
+ location: 'skills',
127
+ meta: readSkillMeta(dir, entry.name),
128
+ });
129
+ }
130
+ } catch {
131
+ // directory might not exist yet
132
+ }
133
+
134
+ // Skills from ~/.claude/plugins/ (installed as plugin-skill)
135
+ try {
136
+ const entries = readdirSync(pluginsDir, { withFileTypes: true });
137
+ for (const entry of entries) {
138
+ if (!entry.isDirectory()) continue;
139
+ const dir = join(pluginsDir, entry.name);
140
+ const origin = readOrigin(dir);
141
+ if (origin && origin.installedAs === 'plugin-skill') {
142
+ results.push({
143
+ name: entry.name,
144
+ dir,
145
+ location: 'plugins',
146
+ meta: readSkillMeta(dir, entry.name),
147
+ });
148
+ }
149
+ }
150
+ } catch {
151
+ // directory might not exist yet
152
+ }
153
+
154
+ return results;
155
+ }
@@ -1,74 +1,58 @@
1
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { getSkillsDir } from '../../config.js';
4
- import { log, spinner } from '../../utils.js';
5
- import { add } from './add.js';
6
- import { remove } from './remove.js';
7
-
8
- function readOrigin(skillDir) {
9
- const originPath = join(skillDir, '.origin.json');
10
- if (!existsSync(originPath)) return null;
11
- try {
12
- return JSON.parse(readFileSync(originPath, 'utf-8'));
13
- } catch {
14
- return null;
15
- }
16
- }
17
-
18
- async function updateOne(name) {
19
- const skillsDir = getSkillsDir();
20
- const dest = join(skillsDir, name);
21
-
22
- if (!existsSync(dest)) {
23
- log.error(`Skill "${name}" is not installed`);
24
- return false;
25
- }
26
-
27
- const origin = readOrigin(dest);
28
- if (!origin || !origin.repository) {
29
- log.warn(`Skill "${name}" has no origin metadata, skipping`);
30
- return false;
31
- }
32
-
33
- const spin = spinner(`Updating skill "${name}"...`);
34
- spin.start();
35
- spin.stop();
36
-
37
- try {
38
- // Remove and re-add
39
- await remove(name);
40
- await add(origin.repository, { skill: origin.skill || undefined });
41
- return true;
42
- } catch (err) {
43
- log.error(`Failed to update skill "${name}": ${err.message}`);
44
- return false;
45
- }
46
- }
47
-
48
- export async function update(name) {
49
- if (name) {
50
- await updateOne(name);
51
- return;
52
- }
53
-
54
- const skillsDir = getSkillsDir();
55
- const entries = readdirSync(skillsDir, { withFileTypes: true }).filter(
56
- (e) => e.isDirectory()
57
- );
58
-
59
- if (!entries.length) {
60
- log.info('No skills installed');
61
- return;
62
- }
63
-
64
- log.info(`Updating ${entries.length} skill${entries.length === 1 ? '' : 's'}...\n`);
65
-
66
- let updated = 0;
67
- for (const entry of entries) {
68
- if (await updateOne(entry.name)) updated++;
69
- }
70
-
71
- console.log();
72
- log.success(`${updated}/${entries.length} skills updated`);
73
- log.dim('Restart Claude Code to apply changes.');
74
- }
1
+ import { log, spinner } from '../../utils.js';
2
+ import { findInstalledSkill, listAllSkills, readOrigin } from './resolve.js';
3
+ import { add } from './add.js';
4
+ import { remove } from './remove.js';
5
+
6
+ async function updateOne(name) {
7
+ const found = findInstalledSkill(name);
8
+
9
+ if (!found) {
10
+ log.error(`Skill "${name}" is not installed`);
11
+ return false;
12
+ }
13
+
14
+ const origin = readOrigin(found.dir);
15
+ if (!origin || !origin.repository) {
16
+ log.warn(`Skill "${name}" has no origin metadata, skipping`);
17
+ return false;
18
+ }
19
+
20
+ const spin = spinner(`Updating skill "${name}"...`);
21
+ spin.start();
22
+ spin.stop();
23
+
24
+ try {
25
+ // Remove and re-add
26
+ await remove(name);
27
+ await add(origin.repository, { skill: origin.skill || undefined });
28
+ return true;
29
+ } catch (err) {
30
+ log.error(`Failed to update skill "${name}": ${err.message}`);
31
+ return false;
32
+ }
33
+ }
34
+
35
+ export async function update(name) {
36
+ if (name) {
37
+ await updateOne(name);
38
+ return;
39
+ }
40
+
41
+ const skills = listAllSkills();
42
+
43
+ if (!skills.length) {
44
+ log.info('No skills installed');
45
+ return;
46
+ }
47
+
48
+ log.info(`Updating ${skills.length} skill${skills.length === 1 ? '' : 's'}...\n`);
49
+
50
+ let updated = 0;
51
+ for (const skill of skills) {
52
+ if (await updateOne(skill.name)) updated++;
53
+ }
54
+
55
+ console.log();
56
+ log.success(`${updated}/${skills.length} skills updated`);
57
+ log.dim('Restart Claude Code to apply changes.');
58
+ }