@trendai-crem/claude-skills 0.10.0 → 0.10.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.
Files changed (3) hide show
  1. package/cli.js +100 -6
  2. package/marketplace.json +1 -2
  3. package/package.json +1 -1
package/cli.js CHANGED
@@ -13,6 +13,8 @@ const EXTERNAL_SOURCES = [
13
13
  { repo: 'obra/superpowers', flags: ['--all'], label: 'superpowers' },
14
14
  ];
15
15
 
16
+ const MANIFEST_PATH = join(homedir(), '.claude', 'claude-skills-manifest.json');
17
+
16
18
  const run = (args, label) => {
17
19
  try {
18
20
  execFileSync('npx', args, { stdio: 'inherit' });
@@ -25,19 +27,36 @@ const run = (args, label) => {
25
27
 
26
28
  console.log('Installing team skills...\n');
27
29
 
28
- // 1. External skills — failures are non-fatal
29
- const externalResults = EXTERNAL_SOURCES.map(({ repo, flags, label }) =>
30
- ({ label, ok: run(['skills', 'add', repo, ...flags, '-g', '-y'], label) })
31
- );
32
-
33
30
  // 2. Team skills — required, overrides same-named externals
34
31
  const teamSkills = readdirSync(join(__dir, 'skills'), { withFileTypes: true })
35
32
  .filter(e => e.isDirectory())
36
33
  .map(e => e.name)
37
34
  .sort();
38
35
 
36
+ // Compute desired skills for reconcile (query external sources before installing)
37
+ const externalSkillNames = EXTERNAL_SOURCES.flatMap(({ repo, flags }) =>
38
+ listExternalSkillNames(repo, flags) ?? []
39
+ );
40
+ const desiredSkills = new Set([...teamSkills, ...externalSkillNames]);
41
+
42
+ // Reconcile stale skills via manifest (before installing, so removals show first)
43
+ const skillManifest = readJsonObject(MANIFEST_PATH, {}, 'claude-skills-manifest.json') ?? {};
44
+ const previousSkills = new Set(Array.isArray(skillManifest.skills) ? skillManifest.skills : []);
45
+ const staleSkillResults = reconcileSkills(desiredSkills, previousSkills);
46
+
47
+ // 1. External skills — failures are non-fatal
48
+ const externalResults = EXTERNAL_SOURCES.map(({ repo, flags, label }) =>
49
+ ({ label, ok: run(['skills', 'add', repo, ...flags, '-g', '-y'], label) })
50
+ );
51
+
39
52
  const teamOk = run(['skills', 'add', __dir, '--all', '-g', '-y'], 'team skills');
40
53
 
54
+ // Write skills portion of manifest (merge to preserve plugins field written later)
55
+ const tmpSkillManifest = join(tmpdir(), `claude-skills-manifest-${process.pid}-skills.json`);
56
+ const mergedSkillManifest = { ...skillManifest, skills: [...desiredSkills] };
57
+ writeFileSync(tmpSkillManifest, JSON.stringify(mergedSkillManifest, null, 2) + '\n');
58
+ renameSync(tmpSkillManifest, MANIFEST_PATH);
59
+
41
60
  // 3. Marketplace plugins — failures are non-fatal
42
61
  let marketplaceResults = [];
43
62
  try {
@@ -48,6 +67,7 @@ try {
48
67
 
49
68
  // Summary
50
69
  console.log('\nResults:');
70
+ staleSkillResults.forEach(({ skill, action, ok }) => console.log(` ${ok ? '✓' : '✗'} ${skill} (${action})`));
51
71
  externalResults.forEach(({ label, ok }) => console.log(` ${ok ? '✓' : '✗'} ${label}`));
52
72
  if (teamOk) {
53
73
  teamSkills.forEach(name => console.log(` ✓ ${name}`));
@@ -90,6 +110,39 @@ function readJsonObject(filePath, fallback, label) {
90
110
  return fallback;
91
111
  }
92
112
 
113
+ // Lists skill names available in an external repo without installing (uses --list flag).
114
+ // Returns null if the query fails — caller should skip reconcile for that source.
115
+ function listExternalSkillNames(repo, flags) {
116
+ try {
117
+ const output = execFileSync('npx', ['skills', 'add', repo, ...flags, '-l'], {
118
+ encoding: 'utf8',
119
+ stdio: ['pipe', 'pipe', 'pipe'],
120
+ });
121
+ return output.split('\n')
122
+ .filter(l => /^│ [a-z]/.test(l))
123
+ .map(l => l.replace(/^│\s+/, '').trim());
124
+ } catch {
125
+ return null; // can't determine list — reconcile skipped for this source
126
+ }
127
+ }
128
+
129
+ // Removes skills that claude-skills previously managed but are no longer in any source.
130
+ // Only touches skills in our manifest — leaves user-created skills alone.
131
+ function reconcileSkills(desiredSkills, previousSkills) {
132
+ const staleSkills = [...previousSkills].filter(s => !desiredSkills.has(s));
133
+ if (staleSkills.length === 0) return [];
134
+
135
+ console.log('\nRemoving stale skills (removed from config)...\n');
136
+ try {
137
+ execFileSync('npx', ['skills', 'remove', ...staleSkills, '-g', '-y'], { stdio: 'inherit' });
138
+ return staleSkills.map(skill => ({ skill, action: 'uninstall', ok: true }));
139
+ } catch (error) {
140
+ const exitInfo = error.status != null ? `exit ${error.status}` : error.code ?? error.message;
141
+ console.error(`\nFailed to remove stale skills (${exitInfo})`);
142
+ return staleSkills.map(skill => ({ skill, action: 'uninstall', ok: false }));
143
+ }
144
+ }
145
+
93
146
  // Extracts the registered source for a marketplace from known_marketplaces.json.
94
147
  // Handles: plain string, object with string .source, and GitHub registry format
95
148
  // { source: { source: "github", repo: "org/repo" } }.
@@ -233,7 +286,48 @@ function installMarketplacePlugins() {
233
286
  ? rawInstalled
234
287
  : {};
235
288
 
236
- return marketplaces.flatMap(entry => installFromMarketplace(entry, known, installed));
289
+ const desiredKeys = new Set(
290
+ marketplaces.flatMap(e => (e?.plugins ?? []).map(p => `${p}@${e.name}`))
291
+ );
292
+
293
+ // Reconcile: uninstall plugins WE previously managed that are no longer in marketplace.json.
294
+ // Uses a manifest so we only remove what claude-skills installed — not user-installed plugins.
295
+ const manifest = readJsonObject(MANIFEST_PATH, {}, 'claude-skills-manifest.json');
296
+ const previousKeys = new Set(Array.isArray(manifest.plugins) ? manifest.plugins : []);
297
+ const uninstallResults = reconcileMarketplacePlugins(desiredKeys, previousKeys, installed);
298
+
299
+ // Update manifest — merge to preserve other fields (e.g. skills written by reconcileSkills)
300
+ const tmpManifest = join(tmpdir(), `claude-skills-manifest-${process.pid}.json`);
301
+ const updatedManifest = { ...(readJsonObject(MANIFEST_PATH, {}) ?? {}), plugins: [...desiredKeys] };
302
+ writeFileSync(tmpManifest, JSON.stringify(updatedManifest, null, 2) + '\n');
303
+ renameSync(tmpManifest, MANIFEST_PATH);
304
+
305
+ const installResults = marketplaces.flatMap(entry => installFromMarketplace(entry, known, installed));
306
+ return [...uninstallResults, ...installResults];
307
+ }
308
+
309
+ // Uninstalls plugins that claude-skills previously managed but are no longer in marketplace.json.
310
+ // Only touches plugins in our manifest — leaves user-installed plugins alone.
311
+ function reconcileMarketplacePlugins(desiredKeys, previousKeys, installed) {
312
+ const staleKeys = [...previousKeys].filter(key =>
313
+ !desiredKeys.has(key) && Object.hasOwn(installed, key)
314
+ );
315
+
316
+ if (staleKeys.length === 0) return [];
317
+
318
+ console.log('\nRemoving stale marketplace plugins (removed from marketplace.json)...\n');
319
+ return staleKeys.map(key => {
320
+ const atIdx = key.lastIndexOf('@');
321
+ const plugin = atIdx !== -1 ? key.slice(0, atIdx) : key;
322
+ try {
323
+ execFileSync('claude', ['plugin', 'uninstall', key], { stdio: 'inherit' });
324
+ return { plugin, action: 'uninstall', ok: true };
325
+ } catch (error) {
326
+ const exitInfo = error.status != null ? `exit ${error.status}` : error.code ?? error.message;
327
+ console.error(`\nFailed to uninstall ${key} (${exitInfo})`);
328
+ return { plugin, action: 'uninstall', ok: false };
329
+ }
330
+ });
237
331
  }
238
332
 
239
333
  function setupAutoUpdate() {
package/marketplace.json CHANGED
@@ -8,8 +8,7 @@
8
8
  "atlassian-tools",
9
9
  "google-style-guides",
10
10
  "l2-automation",
11
- "service-doc-generator",
12
- "claude-on-teams"
11
+ "service-doc-generator"
13
12
  ]
14
13
  },
15
14
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trendai-crem/claude-skills",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Claude Code skills installer for the trendai-crem team",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {