@trendai-crem/claude-skills 0.10.0 → 1.0.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.
package/cli.js CHANGED
@@ -1,265 +1,110 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execFileSync } from 'child_process';
4
- import { readFileSync, readdirSync, writeFileSync, copyFileSync, mkdirSync, existsSync } from 'fs';
4
+ import { readFileSync, writeFileSync, copyFileSync, mkdirSync, existsSync } from 'fs';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { dirname, join } from 'path';
7
7
  import { homedir, tmpdir } from 'os';
8
8
  import { renameSync } from 'fs';
9
9
 
10
- const __dir = dirname(fileURLToPath(import.meta.url));
11
-
12
- const EXTERNAL_SOURCES = [
13
- { repo: 'obra/superpowers', flags: ['--all'], label: 'superpowers' },
14
- ];
15
-
16
- const run = (args, label) => {
17
- try {
18
- execFileSync('npx', args, { stdio: 'inherit' });
19
- return true;
20
- } catch (error) {
21
- console.error(`\nFailed: ${label} (exit ${error.status})`);
22
- return false;
23
- }
24
- };
10
+ import { HANDLERS } from './lib/handlers/index.js';
11
+ import { loadSources } from './lib/config.js';
12
+ import { loadManifest, writeManifest } from './lib/manifest.js';
13
+ import { printSummary } from './lib/utils.js';
25
14
 
26
- console.log('Installing team skills...\n');
15
+ const __dir = dirname(fileURLToPath(import.meta.url));
16
+ const MANIFEST_PATH = join(homedir(), '.claude', 'claude-skills-manifest.json');
27
17
 
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
- );
18
+ // ── Orchestrator ──────────────────────────────────────────────────────────────
32
19
 
33
- // 2. Team skills — required, overrides same-named externals
34
- const teamSkills = readdirSync(join(__dir, 'skills'), { withFileTypes: true })
35
- .filter(e => e.isDirectory())
36
- .map(e => e.name)
37
- .sort();
20
+ console.log('Installing claude-skills...\n');
38
21
 
39
- const teamOk = run(['skills', 'add', __dir, '--all', '-g', '-y'], 'team skills');
22
+ const sources = loadSources(join(__dir, 'sources.json'));
23
+ const manifest = loadManifest(MANIFEST_PATH, join(__dir, 'skills'));
24
+ const allResults = [];
40
25
 
41
- // 3. Marketplace plugins failures are non-fatal
42
- let marketplaceResults = [];
43
- try {
44
- marketplaceResults = installMarketplacePlugins();
45
- } catch (err) {
46
- console.error(`\nmarketplace plugin installation failed unexpectedly: ${err.message}`);
47
- }
26
+ for (const entry of sources) {
27
+ if (!Object.hasOwn(HANDLERS, entry.type)) {
28
+ console.warn(`\nWARN: Unknown source type "${entry.type}" (label: ${entry.label}), skipping`);
29
+ continue;
30
+ }
48
31
 
49
- // Summary
50
- console.log('\nResults:');
51
- externalResults.forEach(({ label, ok }) => console.log(` ${ok ? '✓' : '✗'} ${label}`));
52
- if (teamOk) {
53
- teamSkills.forEach(name => console.log(` ✓ ${name}`));
54
- } else {
55
- console.log(` ✗ team skills`);
56
- }
57
- marketplaceResults.forEach(({ plugin, action, ok }) =>
58
- console.log(` ${ok ? '✓' : '✗'} ${plugin} (${action})`)
59
- );
32
+ const handler = HANDLERS[entry.type];
33
+ const { label } = entry;
60
34
 
61
- if (!teamOk) {
62
- console.error('\nFATAL: Team skills installation failed.');
63
- process.exit(1);
64
- }
35
+ const desired = handler.getDesired(entry, __dir);
36
+ const previous = new Set(manifest.sources?.[label] ?? []);
65
37
 
66
- const externalFailed = externalResults.filter(r => !r.ok);
67
- if (externalFailed.length > 0) {
68
- console.warn(`\nWARN: ${externalFailed.length} external source(s) failed team skills installed successfully.`);
69
- }
38
+ // Reconcile: uninstall items no longer in desired set.
39
+ // desired=null means the set couldn't be determined — skip removals, still install.
40
+ const stale = desired ? [...previous].filter(i => !desired.has(i)) : [];
41
+ if (stale.length) allResults.push(...handler.uninstall(stale, entry));
70
42
 
71
- const marketplaceFailed = marketplaceResults.filter(r => !r.ok);
72
- if (marketplaceFailed.length > 0) {
73
- console.warn(`\nWARN: ${marketplaceFailed.length} marketplace plugin(s) failed — team skills installed successfully.`);
74
- }
43
+ // Install / update
44
+ allResults.push(...handler.install(entry, __dir));
75
45
 
76
- // Configure update-check hooks
77
- setupAutoUpdate();
78
-
79
- // Reads a JSON file and returns its parsed value if it is a non-null, non-array object.
80
- // Returns `fallback` on any error (missing file, parse failure, wrong type).
81
- function readJsonObject(filePath, fallback, label) {
82
- if (!existsSync(filePath)) return fallback;
83
- try {
84
- const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
85
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
86
- console.warn(`\nWARN: Ignoring invalid ${label}: expected JSON object`);
87
- } catch (err) {
88
- console.warn(`\nWARN: Ignoring invalid ${label}: ${err.message}`);
46
+ // Update manifest (only when desired is known)
47
+ if (desired !== null) {
48
+ manifest.sources[label] = [...desired];
89
49
  }
90
- return fallback;
91
50
  }
92
51
 
93
- // Extracts the registered source for a marketplace from known_marketplaces.json.
94
- // Handles: plain string, object with string .source, and GitHub registry format
95
- // { source: { source: "github", repo: "org/repo" } }.
96
- function getRegisteredMarketplaceSource(known, marketplaceName) {
97
- const entry = known[marketplaceName];
98
- if (typeof entry === 'string') return entry;
99
- if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
100
- if (typeof entry.source === 'string') return entry.source;
101
- // GitHub registry format: { source: { source: "github", repo: "org/repo" }, ... }
102
- if (entry.source?.source === 'github' && typeof entry.source?.repo === 'string') {
103
- return entry.source.repo;
52
+ // Post-loop cleanup: handle manifest labels no longer present in sources.json.
53
+ const activeLabels = new Set(sources.map(e => e.label));
54
+ for (const [orphanLabel, items] of Object.entries(manifest.sources ?? {})) {
55
+ if (activeLabels.has(orphanLabel)) continue;
56
+
57
+ if (orphanLabel.startsWith('__v1_')) {
58
+ // Synthetic migration labels: items are now tracked under real source labels.
59
+ // Just remove the synthetic entry do NOT uninstall (items still managed).
60
+ delete manifest.sources[orphanLabel];
61
+ continue;
62
+ }
63
+
64
+ // Real orphaned label: source was removed from sources.json.
65
+ // Uninstall its previously managed items, then remove from manifest.
66
+ if (Array.isArray(items) && items.length > 0) {
67
+ const hasAtSign = items.some(i => typeof i === 'string' && i.includes('@'));
68
+ const handlerType = hasAtSign ? 'marketplace' : 'skills-repo';
69
+ if (Object.hasOwn(HANDLERS, handlerType)) {
70
+ allResults.push(...HANDLERS[handlerType].uninstall(items, { label: orphanLabel }));
104
71
  }
105
72
  }
106
- return null;
107
- }
108
-
109
- // Normalizes a marketplace source to a canonical host/path form for comparison.
110
- // Accepts SSH URLs, HTTPS URLs, and GitHub shorthand (org/repo).
111
- // Returns null if the source cannot be parsed.
112
- function normalizeMarketplaceSource(rawSource) {
113
- // GitHub shorthand: org/repo (no slashes in org, no protocol)
114
- if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(rawSource)) {
115
- return rawSource.toLowerCase();
116
- }
117
- const sshMatch = /^git@([^:]+):(.+?)(?:\.git)?$/.exec(rawSource);
118
- if (sshMatch) {
119
- return `${sshMatch[1].toLowerCase()}/${sshMatch[2].replace(/^\/+/, '').replace(/\.git$/, '')}`;
120
- }
121
- try {
122
- const url = new URL(rawSource);
123
- return `${url.hostname.toLowerCase()}/${url.pathname.replace(/^\/+/, '').replace(/\.git$/, '')}`;
124
- } catch {
125
- return null;
126
- }
73
+ delete manifest.sources[orphanLabel];
127
74
  }
128
75
 
129
- // Installs or updates all plugins for a single marketplace entry.
130
- // Returns an array of { plugin, action, ok } results.
131
- function installFromMarketplace(entry, known, installed) {
132
- // Input validation + allowlisting (SCD-1, OWASP-A04)
133
- // Leading `-` is rejected to prevent argv injection: `claude plugin install -s` parses as flag.
134
- const SAFE_NAME = /^(?!-)[A-Za-z0-9_][A-Za-z0-9_-]*$/;
135
- const SAFE_SOURCE = /^(git@[\w.-]+:[\w.\-/]+\.git|https:\/\/[\w.-]+\/[\w.\-/]+\.git)$/;
136
- const SAFE_GITHUB = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
137
-
138
- const { name: marketplaceName, source, plugins } = entry ?? {};
139
-
140
- if (typeof marketplaceName !== 'string' || typeof source !== 'string' || !Array.isArray(plugins)) {
141
- console.error('\nInvalid marketplace entry: missing required fields (name, source, plugins[])');
142
- return [];
143
- }
144
- if (!SAFE_NAME.test(marketplaceName) || (!SAFE_SOURCE.test(source) && !SAFE_GITHUB.test(source))) {
145
- console.error(`\nInvalid marketplace name or source: ${marketplaceName}`);
146
- return [];
147
- }
148
- // Reject the entire entry on any invalid plugin name (fail-closed, not silent filter).
149
- const invalidPlugins = plugins.filter(p => typeof p !== 'string' || !SAFE_NAME.test(p));
150
- if (invalidPlugins.length > 0) {
151
- console.error(`\nInvalid plugin names for ${marketplaceName}: ${invalidPlugins.map(p => JSON.stringify(p)).join(', ')}`);
152
- return [];
153
- }
154
-
155
- console.log(`\nInstalling marketplace plugins (${marketplaceName})...\n`);
156
-
157
- // Register marketplace if not already known — verify source matches to prevent supply-chain mismatch (OWASP-A08)
158
- const registeredSource = getRegisteredMarketplaceSource(known, marketplaceName);
159
- const normalizedSource = normalizeMarketplaceSource(source);
160
- const normalizedRegistered = registeredSource ? normalizeMarketplaceSource(registeredSource) : null;
161
-
162
- if (registeredSource && (!normalizedRegistered || normalizedRegistered !== normalizedSource)) {
163
- console.error(`\nMarketplace source mismatch for ${marketplaceName}: registered as ${registeredSource}, but marketplace.json specifies ${source}`);
164
- return plugins.map(plugin => ({ plugin, action: 'skipped (source mismatch)', ok: false }));
165
- }
166
-
167
- if (!registeredSource) {
168
- try {
169
- execFileSync('claude', ['plugin', 'marketplace', 'add', source, '--scope', 'user'], {
170
- stdio: 'inherit',
171
- });
172
- } catch (error) {
173
- const exitInfo = error.status != null ? `exit ${error.status}` : error.code ?? error.message;
174
- console.error(`\nFailed to register marketplace: ${marketplaceName} (${exitInfo})`);
175
- return plugins.map(plugin => ({ plugin, action: 'skipped (no marketplace)', ok: false }));
176
- }
177
- }
178
-
179
- // Always update marketplace to pull latest plugin versions before install/update
180
- try {
181
- execFileSync('claude', ['plugin', 'marketplace', 'update', marketplaceName], { stdio: 'inherit' });
182
- } catch (error) {
183
- const exitInfo = error.status != null ? `exit ${error.status}` : error.code ?? error.message;
184
- console.warn(`\nWARN: Failed to update marketplace ${marketplaceName} (${exitInfo}) — using cached version`);
185
- }
186
-
187
- return plugins.map(plugin => {
188
- const key = `${plugin}@${marketplaceName}`;
189
- const isInstalled = Object.hasOwn(installed, key); // Object.hasOwn avoids prototype chain traversal (SCD-7)
190
- const action = isInstalled ? 'update' : 'install';
191
- const args = isInstalled
192
- ? ['plugin', 'update', key]
193
- : ['plugin', 'install', key, '--scope', 'user'];
194
- try {
195
- execFileSync('claude', args, { stdio: 'inherit' });
196
- return { plugin, action, ok: true };
197
- } catch (error) {
198
- const exitInfo = error.status != null ? `exit ${error.status}` : error.code ?? error.message;
199
- console.error(`\nFailed: ${key} ${action} (${exitInfo})`);
200
- return { plugin, action, ok: false };
201
- }
202
- });
76
+ // Skip manifest write if v1 migration was aborted (backup failed) — preserves original file.
77
+ if (!manifest._writeSkipped) {
78
+ writeManifest(MANIFEST_PATH, manifest);
79
+ } else {
80
+ console.warn('\nWARN: Manifest not updated v1 backup failed. Run again to retry migration.');
203
81
  }
82
+ printSummary(allResults);
204
83
 
205
- function installMarketplacePlugins() {
206
- const configPath = join(__dir, 'marketplace.json');
207
- if (!existsSync(configPath)) return [];
84
+ // ── Auto-update hooks ─────────────────────────────────────────────────────────
208
85
 
209
- // Parse marketplace.json — all failures are non-fatal (SCD-7)
210
- const config = readJsonObject(configPath, null, 'marketplace.json');
211
- const marketplaces = config?.marketplaces;
212
- if (!Array.isArray(marketplaces) || marketplaces.length === 0) {
213
- console.warn('\nWARN: marketplace.json has no valid "marketplaces" array, skipping');
214
- return [];
215
- }
216
-
217
- // Load shared state once: known registry (for source verification) and installed plugins
218
- const knownPath = join(homedir(), '.claude', 'plugins', 'known_marketplaces.json');
219
- const knownExists = existsSync(knownPath);
220
- const known = readJsonObject(knownPath, knownExists ? null : {}, 'known_marketplaces.json');
221
- if (knownExists && known === null) {
222
- console.error('\nInvalid known_marketplaces.json: refusing to register any marketplace automatically');
223
- return marketplaces.flatMap(e =>
224
- (e?.plugins ?? []).map(plugin => ({ plugin, action: 'skipped (invalid marketplace registry)', ok: false }))
225
- );
226
- }
227
-
228
- const installedPath = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
229
- const installedData = readJsonObject(installedPath, {}, 'installed_plugins.json');
230
- // installed_plugins.json v2 nests under "plugins", v1 is flat
231
- const rawInstalled = installedData.plugins ?? installedData;
232
- const installed = (typeof rawInstalled === 'object' && rawInstalled !== null && !Array.isArray(rawInstalled))
233
- ? rawInstalled
234
- : {};
235
-
236
- return marketplaces.flatMap(entry => installFromMarketplace(entry, known, installed));
237
- }
86
+ setupAutoUpdate();
238
87
 
239
88
  function setupAutoUpdate() {
240
89
  const { version: installedVersion } = JSON.parse(
241
90
  readFileSync(join(__dir, 'package.json'), 'utf8')
242
91
  );
243
92
 
244
- const claudeDir = join(homedir(), '.claude');
245
- const hooksDir = join(claudeDir, 'hooks');
246
- const cacheDir = join(homedir(), '.cache');
247
- const installScript = join(hooksDir, 'claude-skills-install-update.sh');
248
- const notifyScript = join(hooksDir, 'auto-update-claude-skills.sh');
249
- const installStamp = join(cacheDir, 'claude-skills-install-check.json');
250
- const notifyStamp = join(cacheDir, 'claude-skills-version-check.json');
251
- const settingsPath = join(claudeDir, 'settings.json');
93
+ const claudeDir = join(homedir(), '.claude');
94
+ const hooksDir = join(claudeDir, 'hooks');
95
+ const cacheDir = join(homedir(), '.cache');
96
+ const installScript = join(hooksDir, 'claude-skills-install-update.sh');
97
+ const notifyScript = join(hooksDir, 'auto-update-claude-skills.sh');
98
+ const installStamp = join(cacheDir, 'claude-skills-install-check.json');
99
+ const notifyStamp = join(cacheDir, 'claude-skills-version-check.json');
100
+ const settingsPath = join(claudeDir, 'settings.json');
252
101
 
253
102
  mkdirSync(hooksDir, { recursive: true });
254
103
  mkdirSync(cacheDir, { recursive: true });
255
104
 
256
- // SessionStart: auto-install if update available (24h throttle, synchronous)
257
105
  writeFileSync(installScript, buildInstallScript(installStamp, installedVersion), { mode: 0o755 });
106
+ writeFileSync(notifyScript, buildNotifyScript(notifyStamp, installedVersion), { mode: 0o755 });
258
107
 
259
- // UserPromptSubmit: notify only if update available (2h throttle)
260
- writeFileSync(notifyScript, buildNotifyScript(notifyStamp, installedVersion), { mode: 0o755 });
261
-
262
- // Merge hooks into ~/.claude/settings.json (idempotent)
263
108
  let settings = {};
264
109
  if (existsSync(settingsPath)) {
265
110
  try {
@@ -273,25 +118,18 @@ function setupAutoUpdate() {
273
118
 
274
119
  settings.hooks ??= {};
275
120
 
276
- // SessionStart — auto-install update (24h throttle)
277
121
  settings.hooks.SessionStart ??= [];
278
122
  settings.hooks.SessionStart = settings.hooks.SessionStart.filter(
279
123
  e => !e.hooks?.some(h => typeof h.command === 'string' && h.command === installScript)
280
124
  );
281
- settings.hooks.SessionStart.push({
282
- hooks: [{ type: 'command', command: installScript }]
283
- });
125
+ settings.hooks.SessionStart.push({ hooks: [{ type: 'command', command: installScript }] });
284
126
 
285
- // UserPromptSubmit — notify only (2h throttle)
286
127
  settings.hooks.UserPromptSubmit ??= [];
287
128
  settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(
288
129
  e => !e.hooks?.some(h => typeof h.command === 'string' && h.command === notifyScript)
289
130
  );
290
- settings.hooks.UserPromptSubmit.push({
291
- hooks: [{ type: 'command', command: notifyScript }]
292
- });
131
+ settings.hooks.UserPromptSubmit.push({ hooks: [{ type: 'command', command: notifyScript }] });
293
132
 
294
- // Atomic write
295
133
  const tmpPath = join(tmpdir(), `claude-settings-${process.pid}.json`);
296
134
  writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n');
297
135
  renameSync(tmpPath, settingsPath);
@@ -328,11 +166,10 @@ python3 -c "import json,sys; json.dump({'ts': int(sys.argv[1])}, open(sys.argv[2
328
166
  LATEST=$(npm view "$PACKAGE" version 2>/dev/null || echo "")
329
167
  [ -z "$LATEST" ] && exit 0
330
168
  [ "$LATEST" = "$INSTALLED" ] && exit 0
331
- [[ "$LATEST" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]] || exit 0
169
+ [[ "$LATEST" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]] || exit 0
332
170
 
333
- # Install update synchronously
334
171
  INSTALL_DIR="$(mktemp -d)"
335
- if npm install --prefix "$INSTALL_DIR" "$PACKAGE@$LATEST" --silent 2>/dev/null \
172
+ if npm install --prefix "$INSTALL_DIR" "$PACKAGE@$LATEST" --silent 2>/dev/null \\
336
173
  && node "$INSTALL_DIR/node_modules/$PACKAGE/cli.js" 2>/dev/null; then
337
174
  rm -rf "$INSTALL_DIR"
338
175
  python3 -c "import json,sys; print(json.dumps({'systemMessage': 'claude-skills updated: ' + sys.argv[1] + ' \u2192 ' + sys.argv[2]}))" "$INSTALLED" "$LATEST"
@@ -371,7 +208,7 @@ python3 -c "import json,sys; json.dump({'ts': int(sys.argv[1])}, open(sys.argv[2
371
208
  LATEST=$(npm view "$PACKAGE" version 2>/dev/null || echo "")
372
209
  [ -z "$LATEST" ] && exit 0
373
210
  [ "$LATEST" = "$INSTALLED" ] && exit 0
374
- [[ "$LATEST" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]] || exit 0
211
+ [[ "$LATEST" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]] || exit 0
375
212
 
376
213
  python3 -c "import json,sys; print(json.dumps({'systemMessage': 'claude-skills update available: ' + sys.argv[1] + ' \u2192 ' + sys.argv[2] + chr(10) + 'Run: npx @trendai-crem/claude-skills@latest'}))" "$INSTALLED" "$LATEST"
377
214
  `;
package/lib/config.js ADDED
@@ -0,0 +1,49 @@
1
+ import { readJsonObject } from './utils.js';
2
+
3
+ /**
4
+ * Loads and normalizes sources.json.
5
+ *
6
+ * Each entry is normalized so:
7
+ * - entry.label is always set (= entry.label ?? entry.name ?? entry.type)
8
+ * - Entries missing type or a resolvable label are warned and skipped.
9
+ *
10
+ * @param {string} filePath - Absolute path to sources.json.
11
+ * @returns {Array} Normalized source entries (empty array on any failure).
12
+ */
13
+ export function loadSources(filePath) {
14
+ const config = readJsonObject(filePath, null, 'sources.json');
15
+ if (!config) return [];
16
+
17
+ const raw = config.sources;
18
+ if (!Array.isArray(raw) || raw.length === 0) {
19
+ console.warn('\nWARN: sources.json has no valid "sources" array, skipping all sources');
20
+ return [];
21
+ }
22
+
23
+ const seen = new Set();
24
+ const normalized = [];
25
+
26
+ for (const entry of raw) {
27
+ if (typeof entry?.type !== 'string') {
28
+ console.warn(`\nWARN: Skipping source entry with missing or non-string "type": ${JSON.stringify(entry)}`);
29
+ continue;
30
+ }
31
+
32
+ // Resolve canonical label — used as manifest key
33
+ const label = entry.label ?? entry.name;
34
+ if (typeof label !== 'string' || label.trim() === '') {
35
+ console.warn(`\nWARN: Skipping source entry of type "${entry.type}" with no resolvable label`);
36
+ continue;
37
+ }
38
+
39
+ if (seen.has(label)) {
40
+ console.warn(`\nWARN: Duplicate source label "${label}" — skipping second entry`);
41
+ continue;
42
+ }
43
+ seen.add(label);
44
+
45
+ normalized.push({ ...entry, label });
46
+ }
47
+
48
+ return normalized;
49
+ }
@@ -0,0 +1,9 @@
1
+ import { skillsDirHandler } from './skills-dir.js';
2
+ import { skillsRepoHandler } from './skills-repo.js';
3
+ import { marketplaceHandler } from './marketplace.js';
4
+
5
+ // Use Object.create(null) to prevent prototype chain traversal on adversarial type values.
6
+ export const HANDLERS = Object.create(null);
7
+ HANDLERS['skills-dir'] = skillsDirHandler;
8
+ HANDLERS['skills-repo'] = skillsRepoHandler;
9
+ HANDLERS['marketplace'] = marketplaceHandler;
@@ -0,0 +1,160 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+ import { readJsonObject, SAFE_NAME, SAFE_SOURCE, SAFE_GITHUB } from '../utils.js';
6
+
7
+ // Normalizes a marketplace source to a canonical host/path form for comparison.
8
+ // Accepts SSH URLs, HTTPS URLs, and GitHub shorthand (org/repo).
9
+ function normalizeMarketplaceSource(rawSource) {
10
+ if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(rawSource)) return rawSource.toLowerCase();
11
+ const sshMatch = /^git@([^:]+):(.+?)(?:\.git)?$/.exec(rawSource);
12
+ if (sshMatch) {
13
+ return `${sshMatch[1].toLowerCase()}/${sshMatch[2].replace(/^\/+/, '').replace(/\.git$/, '')}`;
14
+ }
15
+ try {
16
+ const url = new URL(rawSource);
17
+ return `${url.hostname.toLowerCase()}/${url.pathname.replace(/^\/+/, '').replace(/\.git$/, '')}`;
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ // Extracts the registered source for a marketplace from known_marketplaces.json.
24
+ function getRegisteredMarketplaceSource(known, marketplaceName) {
25
+ const entry = known[marketplaceName];
26
+ if (typeof entry === 'string') return entry;
27
+ if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
28
+ if (typeof entry.source === 'string') return entry.source;
29
+ if (entry.source?.source === 'github' && typeof entry.source?.repo === 'string') {
30
+ return entry.source.repo;
31
+ }
32
+ }
33
+ return null;
34
+ }
35
+
36
+ /**
37
+ * Handler for marketplace plugin sources (type: "marketplace").
38
+ * Manages Claude Code plugins from a declared marketplace.
39
+ */
40
+ export const marketplaceHandler = {
41
+ /**
42
+ * Returns the set of plugin@marketplace keys this entry should manage.
43
+ * Validates all fields against allowlist regexes (SCD-1, OWASP-A04).
44
+ * @param {object} entry - Source entry with `name`, `source`, `plugins` fields.
45
+ * @returns {Set<string>|null}
46
+ */
47
+ getDesired(entry) {
48
+ const { name: marketplaceName, source, plugins } = entry ?? {};
49
+ if (typeof marketplaceName !== 'string' || typeof source !== 'string' || !Array.isArray(plugins)) return null;
50
+ if (!SAFE_NAME.test(marketplaceName) || (!SAFE_SOURCE.test(source) && !SAFE_GITHUB.test(source))) return null;
51
+ const invalid = plugins.filter(p => typeof p !== 'string' || !SAFE_NAME.test(p));
52
+ if (invalid.length > 0) return null;
53
+ return new Set(plugins.map(p => `${p}@${marketplaceName}`));
54
+ },
55
+
56
+ /**
57
+ * Installs or updates plugins for this marketplace entry.
58
+ * All security controls from the original implementation are preserved.
59
+ * @returns {Array<{label, action, ok}>}
60
+ */
61
+ install(entry) {
62
+ const { name: marketplaceName, source, plugins } = entry ?? {};
63
+
64
+ // Full validation (SCD-1, OWASP-A04)
65
+ if (typeof marketplaceName !== 'string' || typeof source !== 'string' || !Array.isArray(plugins)) {
66
+ console.error('\nInvalid marketplace entry: missing required fields (name, source, plugins[])');
67
+ return [];
68
+ }
69
+ if (!SAFE_NAME.test(marketplaceName) || (!SAFE_SOURCE.test(source) && !SAFE_GITHUB.test(source))) {
70
+ console.error(`\nInvalid marketplace name or source: ${marketplaceName}`);
71
+ return [];
72
+ }
73
+ const invalidPlugins = plugins.filter(p => typeof p !== 'string' || !SAFE_NAME.test(p));
74
+ if (invalidPlugins.length > 0) {
75
+ console.error(`\nInvalid plugin names for ${marketplaceName}: ${invalidPlugins.map(p => JSON.stringify(p)).join(', ')}`);
76
+ return [];
77
+ }
78
+
79
+ console.log(`\nInstalling marketplace plugins (${marketplaceName})...\n`);
80
+
81
+ // Load known registry — fail-closed if corrupted (OWASP-A08, SCD-7)
82
+ const knownPath = join(homedir(), '.claude', 'plugins', 'known_marketplaces.json');
83
+ const knownExists = existsSync(knownPath);
84
+ const known = readJsonObject(knownPath, knownExists ? null : {}, 'known_marketplaces.json');
85
+ if (knownExists && known === null) {
86
+ console.error(`\nInvalid known_marketplaces.json: refusing to register marketplace ${marketplaceName}`);
87
+ return plugins.map(p => ({ label: `${p}@${marketplaceName}`, action: 'skipped (invalid registry)', ok: false }));
88
+ }
89
+
90
+ // Source mismatch check — prevent supply-chain redirection (OWASP-A08)
91
+ const registeredSource = getRegisteredMarketplaceSource(known, marketplaceName);
92
+ const normalizedSource = normalizeMarketplaceSource(source);
93
+ const normalizedRegistered = registeredSource ? normalizeMarketplaceSource(registeredSource) : null;
94
+ if (registeredSource && (!normalizedRegistered || normalizedRegistered !== normalizedSource)) {
95
+ console.error(`\nMarketplace source mismatch for ${marketplaceName}: registered as ${registeredSource}, but sources.json specifies ${source}`);
96
+ return plugins.map(p => ({ label: `${p}@${marketplaceName}`, action: 'skipped (source mismatch)', ok: false }));
97
+ }
98
+
99
+ // Register if not already known
100
+ if (!registeredSource) {
101
+ try {
102
+ execFileSync('claude', ['plugin', 'marketplace', 'add', source, '--scope', 'user'], { stdio: 'inherit' });
103
+ } catch (error) {
104
+ const exitInfo = error.status != null ? `exit ${error.status}` : error.code ?? error.message;
105
+ console.error(`\nFailed to register marketplace: ${marketplaceName} (${exitInfo})`);
106
+ return plugins.map(p => ({ label: `${p}@${marketplaceName}`, action: 'skipped (no marketplace)', ok: false }));
107
+ }
108
+ }
109
+
110
+ // Always update marketplace to pull latest plugin versions
111
+ try {
112
+ execFileSync('claude', ['plugin', 'marketplace', 'update', marketplaceName], { stdio: 'inherit' });
113
+ } catch (error) {
114
+ const exitInfo = error.status != null ? `exit ${error.status}` : error.code ?? error.message;
115
+ console.warn(`\nWARN: Failed to update marketplace ${marketplaceName} (${exitInfo}) — using cached version`);
116
+ }
117
+
118
+ // Load installed registry for install vs update decision (SCD-7)
119
+ const installedPath = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
120
+ const installedData = readJsonObject(installedPath, {}, 'installed_plugins.json');
121
+ const rawInstalled = installedData.plugins ?? installedData;
122
+ const installed = (typeof rawInstalled === 'object' && rawInstalled !== null && !Array.isArray(rawInstalled))
123
+ ? rawInstalled : {};
124
+
125
+ return plugins.map(plugin => {
126
+ const key = `${plugin}@${marketplaceName}`;
127
+ const isInstalled = Object.hasOwn(installed, key); // prototype-safe (SCD-7)
128
+ const action = isInstalled ? 'update' : 'install';
129
+ const args = isInstalled
130
+ ? ['plugin', 'update', key]
131
+ : ['plugin', 'install', key, '--scope', 'user'];
132
+ try {
133
+ execFileSync('claude', args, { stdio: 'inherit' });
134
+ return { label: key, action, ok: true };
135
+ } catch (error) {
136
+ const exitInfo = error.status != null ? `exit ${error.status}` : error.code ?? error.message;
137
+ console.error(`\nFailed: ${key} ${action} (${exitInfo})`);
138
+ return { label: key, action, ok: false };
139
+ }
140
+ });
141
+ },
142
+
143
+ /**
144
+ * Uninstalls stale plugin@marketplace keys.
145
+ * @param {string[]} staleItems - Keys like "plugin@marketplace".
146
+ * @returns {Array<{label, action, ok}>}
147
+ */
148
+ uninstall(staleItems) {
149
+ return staleItems.map(key => {
150
+ try {
151
+ execFileSync('claude', ['plugin', 'uninstall', key], { stdio: 'inherit' });
152
+ return { label: key, action: 'uninstall', ok: true };
153
+ } catch (error) {
154
+ const exitInfo = error.status != null ? `exit ${error.status}` : error.code ?? error.message;
155
+ console.error(`\nFailed to uninstall ${key} (${exitInfo})`);
156
+ return { label: key, action: 'uninstall', ok: false };
157
+ }
158
+ });
159
+ },
160
+ };
@@ -0,0 +1,55 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { readdirSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { uninstallSkills } from '../utils.js';
5
+
6
+ /**
7
+ * Handler for local skills directory sources (type: "skills-dir").
8
+ * Installs all skill subdirectories from a local path.
9
+ */
10
+ export const skillsDirHandler = {
11
+ /**
12
+ * Returns the set of skill names in the directory.
13
+ * @param {object} entry - Source entry with `path` field.
14
+ * @param {string} baseDir - Repository base directory.
15
+ * @returns {Set<string>|null}
16
+ */
17
+ getDesired(entry, baseDir) {
18
+ try {
19
+ const absPath = join(baseDir, entry.path);
20
+ const names = readdirSync(absPath, { withFileTypes: true })
21
+ .filter(e => e.isDirectory())
22
+ .map(e => e.name)
23
+ .sort();
24
+ return new Set(names);
25
+ } catch (err) {
26
+ console.warn(`\nWARN: Cannot read skills directory "${entry.path}": ${err.message}`);
27
+ return null;
28
+ }
29
+ },
30
+
31
+ /**
32
+ * Installs all skills from the directory via `npx skills add`.
33
+ * @returns {Array<{label, action, ok}>}
34
+ */
35
+ install(entry, baseDir) {
36
+ const absPath = join(baseDir, entry.path);
37
+ try {
38
+ execFileSync('npx', ['skills', 'add', absPath, '--all', '-g', '-y'], { stdio: 'inherit' });
39
+ return [{ label: entry.label, action: 'install-group', ok: true }];
40
+ } catch (error) {
41
+ const exitInfo = error.status != null ? `exit ${error.status}` : error.code ?? error.message;
42
+ console.error(`\nFailed: ${entry.label} (${exitInfo})`);
43
+ return [{ label: entry.label, action: 'install-group', ok: false }];
44
+ }
45
+ },
46
+
47
+ /**
48
+ * Removes stale skills. Delegates to shared uninstallSkills utility.
49
+ * @param {string[]} staleItems - Skill names to remove.
50
+ * @returns {Array<{label, action, ok}>}
51
+ */
52
+ uninstall(staleItems) {
53
+ return uninstallSkills(staleItems);
54
+ },
55
+ };
@@ -0,0 +1,84 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { SAFE_GITHUB, SAFE_SOURCE, uninstallSkills } from '../utils.js';
3
+
4
+ // Allowed flags for skills-repo sources (allowlist to prevent flag injection).
5
+ const ALLOWED_REPO_FLAGS = new Set(['--all', '--copy', '--full-depth']);
6
+
7
+ /**
8
+ * Handler for remote skill repository sources (type: "skills-repo").
9
+ * Installs all skills from a GitHub/git repository.
10
+ */
11
+ export const skillsRepoHandler = {
12
+ /**
13
+ * Lists available skills in the repo via --list flag (no install).
14
+ * Returns null if the query fails — reconcile is skipped but install still runs.
15
+ * Returns an empty Set if the repo has no skills (reconcile removes all stale).
16
+ * @param {object} entry - Source entry with `repo` and `flags` fields.
17
+ * @returns {Set<string>|null}
18
+ */
19
+ getDesired(entry) {
20
+ if (!_validateEntry(entry)) return null;
21
+ try {
22
+ const output = execFileSync(
23
+ 'npx', ['skills', 'add', entry.repo, ...(entry.flags ?? []), '-l'],
24
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
25
+ );
26
+ const names = output.split('\n')
27
+ .filter(l => /^│ [a-z]/.test(l))
28
+ .map(l => l.replace(/^│\s+/, '').trim());
29
+ if (names.length === 0) {
30
+ console.warn(`\nWARN: Could not parse skill list from ${entry.repo} — reconcile skipped`);
31
+ return null;
32
+ }
33
+ return new Set(names);
34
+ } catch (error) {
35
+ console.warn(`\nWARN: Cannot list skills from ${entry.repo}: ${error.message}`);
36
+ return null; // network failure — skip reconcile, still install
37
+ }
38
+ },
39
+
40
+ /**
41
+ * Installs all skills from the repo via `npx skills add`.
42
+ * @returns {Array<{label, action, ok}>}
43
+ */
44
+ install(entry) {
45
+ if (!_validateEntry(entry)) {
46
+ return [{ label: entry?.label ?? 'skills-repo', action: 'install-group', ok: false }];
47
+ }
48
+ try {
49
+ execFileSync(
50
+ 'npx', ['skills', 'add', entry.repo, ...(entry.flags ?? []), '-g', '-y'],
51
+ { stdio: 'inherit' }
52
+ );
53
+ return [{ label: entry.label, action: 'install-group', ok: true }];
54
+ } catch (error) {
55
+ const exitInfo = error.status != null ? `exit ${error.status}` : error.code ?? error.message;
56
+ console.error(`\nFailed: ${entry.label} (${exitInfo})`);
57
+ return [{ label: entry.label, action: 'install-group', ok: false }];
58
+ }
59
+ },
60
+
61
+ /**
62
+ * Removes stale skills via `npx skills remove`. Delegates to shared utility.
63
+ * @param {string[]} staleItems - Skill names to remove.
64
+ * @returns {Array<{label, action, ok}>}
65
+ */
66
+ uninstall(staleItems) {
67
+ return uninstallSkills(staleItems);
68
+ },
69
+ };
70
+
71
+ // Validates repo and flags fields before passing to execFileSync (SCD-1, OWASP-A04).
72
+ function _validateEntry(entry) {
73
+ if (typeof entry?.repo !== 'string' ||
74
+ (!SAFE_GITHUB.test(entry.repo) && !SAFE_SOURCE.test(entry.repo))) {
75
+ console.error(`\nInvalid skills-repo entry: invalid repo "${entry?.repo}"`);
76
+ return false;
77
+ }
78
+ const flags = entry.flags ?? [];
79
+ if (!Array.isArray(flags) || flags.some(f => typeof f !== 'string' || !ALLOWED_REPO_FLAGS.has(f))) {
80
+ console.error(`\nInvalid skills-repo entry: unsupported flags ${JSON.stringify(flags)}`);
81
+ return false;
82
+ }
83
+ return true;
84
+ }
@@ -0,0 +1,118 @@
1
+ import { existsSync, readFileSync, writeFileSync, copyFileSync, readdirSync, renameSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { tmpdir } from 'os';
4
+ import { readJsonObject } from './utils.js';
5
+
6
+ /**
7
+ * Loads the manifest from disk, automatically migrating v1 format to v2.
8
+ * On migration, backs up the original as manifest.json.v1.bak.
9
+ *
10
+ * v1 format: { plugins: [...], skills: [...] }
11
+ * v2 format: { version: 2, sources: { "label": [...items] } }
12
+ *
13
+ * @param {string} manifestPath - Absolute path to the manifest file.
14
+ * @param {string} skillsDirPath - Absolute path to the skills/ directory (for v1 migration attribution).
15
+ * @returns {{ version: number, sources: object, _writeSkipped?: boolean }}
16
+ * If `_writeSkipped` is true, the caller MUST NOT call writeManifest() — the
17
+ * original manifest file must be preserved for the next run to retry migration.
18
+ */
19
+ export function loadManifest(manifestPath, skillsDirPath) {
20
+ const raw = readJsonObject(manifestPath, null, 'claude-skills-manifest.json');
21
+
22
+ // No manifest yet — start fresh
23
+ if (!raw) return { version: 2, sources: {} };
24
+
25
+ // Already v2
26
+ if (raw.version === 2 && raw.sources && typeof raw.sources === 'object') {
27
+ return raw;
28
+ }
29
+
30
+ // Migrate v1 → v2
31
+ console.log('\nMigrating claude-skills manifest from v1 to v2...');
32
+
33
+ // Backup before overwriting
34
+ const backupPath = `${manifestPath}.v1.bak`;
35
+ try {
36
+ copyFileSync(manifestPath, backupPath);
37
+ console.log(` Backed up v1 manifest to ${backupPath}`);
38
+ } catch (err) {
39
+ console.error(` ERROR: Cannot back up v1 manifest (${err.message}). Skipping migration to preserve data.`);
40
+ // Signal caller to skip writeManifest so the original v1 file is preserved.
41
+ // Next run retries migration once the backup succeeds.
42
+ return { version: 2, sources: {}, _writeSkipped: true };
43
+ }
44
+
45
+ return migrateV1toV2(raw, skillsDirPath);
46
+ }
47
+
48
+ /**
49
+ * Migrates a v1 manifest to v2 format.
50
+ *
51
+ * Algorithm:
52
+ * - skills that exist in skills/ dir → sources["team-skills"]
53
+ * - remaining skills → sources["__v1_migrated"] (reconcile-skipped on first run)
54
+ * - plugins with @marketplace suffix → sources[marketplace-name]
55
+ * - plugins with unknown marketplace → sources["__v1_orphaned"]
56
+ */
57
+ function migrateV1toV2(v1, skillsDirPath) {
58
+ const sources = {};
59
+
60
+ // Migrate skills
61
+ if (Array.isArray(v1.skills) && v1.skills.length > 0) {
62
+ let teamSkillNames = new Set();
63
+ try {
64
+ teamSkillNames = new Set(
65
+ readdirSync(skillsDirPath, { withFileTypes: true })
66
+ .filter(e => e.isDirectory())
67
+ .map(e => e.name)
68
+ );
69
+ } catch { /* skills dir unreadable — all go to __v1_migrated */ }
70
+
71
+ const teamSkills = v1.skills.filter(s => teamSkillNames.has(s));
72
+ const migratedSkills = v1.skills.filter(s => !teamSkillNames.has(s));
73
+
74
+ if (teamSkills.length > 0) sources['team-skills'] = teamSkills;
75
+ if (migratedSkills.length > 0) {
76
+ sources['__v1_migrated'] = migratedSkills;
77
+ console.log(` ${migratedSkills.length} skills moved to __v1_migrated (will resolve on next full run)`);
78
+ }
79
+ }
80
+
81
+ // Migrate plugins — distribute by @marketplace-name suffix
82
+ if (Array.isArray(v1.plugins) && v1.plugins.length > 0) {
83
+ const byMarketplace = {};
84
+ const orphaned = [];
85
+
86
+ for (const key of v1.plugins) {
87
+ const atIdx = key.lastIndexOf('@');
88
+ if (atIdx !== -1) {
89
+ const mName = key.slice(atIdx + 1);
90
+ (byMarketplace[mName] ??= []).push(key);
91
+ } else {
92
+ orphaned.push(key);
93
+ }
94
+ }
95
+
96
+ for (const [mName, keys] of Object.entries(byMarketplace)) {
97
+ sources[mName] = keys;
98
+ }
99
+ if (orphaned.length > 0) {
100
+ sources['__v1_orphaned'] = orphaned;
101
+ console.log(` ${orphaned.length} plugins moved to __v1_orphaned (unknown marketplace)`);
102
+ }
103
+ }
104
+
105
+ return { version: 2, sources };
106
+ }
107
+
108
+ /**
109
+ * Writes the manifest atomically (tmp file + rename).
110
+ *
111
+ * @param {string} manifestPath - Absolute path to the manifest file.
112
+ * @param {{ version: number, sources: object }} manifest
113
+ */
114
+ export function writeManifest(manifestPath, manifest) {
115
+ const tmp = join(tmpdir(), `claude-skills-manifest-${process.pid}.json`);
116
+ writeFileSync(tmp, JSON.stringify(manifest, null, 2) + '\n');
117
+ renameSync(tmp, manifestPath);
118
+ }
package/lib/utils.js ADDED
@@ -0,0 +1,60 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { existsSync, readFileSync } from 'fs';
3
+
4
+ // Shared allowlist constants — all handlers MUST validate entry fields against these
5
+ // before passing to execFileSync (SCD-1, OWASP-A04).
6
+ export const SAFE_NAME = /^(?!-)[A-Za-z0-9_][A-Za-z0-9_-]*$/;
7
+ export const SAFE_SOURCE = /^(git@[\w.-]+:[\w.\-/]+\.git|https:\/\/[\w.-]+\/[\w.\-/]+\.git)$/;
8
+ export const SAFE_GITHUB = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
9
+
10
+ /**
11
+ * Reads a JSON file and returns its parsed value if it is a non-null, non-array object.
12
+ * Returns `fallback` on any error (missing file, parse failure, wrong type).
13
+ *
14
+ * @param {string} filePath - Absolute path to the JSON file.
15
+ * @param {*} fallback - Value returned when the file is missing or unreadable.
16
+ * @param {string} label - Human-readable label used in warning messages.
17
+ * @returns {object|*} The parsed object, or fallback.
18
+ */
19
+ export function readJsonObject(filePath, fallback, label) {
20
+ if (!existsSync(filePath)) return fallback;
21
+ try {
22
+ const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
23
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
24
+ console.warn(`\nWARN: Ignoring invalid ${label}: expected JSON object`);
25
+ } catch (err) {
26
+ console.warn(`\nWARN: Ignoring invalid ${label}: ${err.message}`);
27
+ }
28
+ return fallback;
29
+ }
30
+
31
+ /**
32
+ * Removes skills by name via `npx skills remove`. Used by skills-dir and skills-repo handlers.
33
+ *
34
+ * @param {string[]} staleItems - Skill names to remove.
35
+ * @returns {Array<{label, action, ok}>}
36
+ */
37
+ export function uninstallSkills(staleItems) {
38
+ if (staleItems.length === 0) return [];
39
+ try {
40
+ execFileSync('npx', ['skills', 'remove', ...staleItems, '-g', '-y'], { stdio: 'inherit' });
41
+ return staleItems.map(s => ({ label: s, action: 'uninstall', ok: true }));
42
+ } catch (error) {
43
+ const exitInfo = error.status != null ? `exit ${error.status}` : error.code ?? error.message;
44
+ console.error(`\nFailed to remove stale skills (${exitInfo})`);
45
+ return staleItems.map(s => ({ label: s, action: 'uninstall', ok: false }));
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Prints the consolidated install/uninstall results summary.
51
+ *
52
+ * @param {Array<{label: string, action: string, ok: boolean}>} results
53
+ */
54
+ export function printSummary(results) {
55
+ console.log('\nResults:');
56
+ results.forEach(({ label, action, ok }) => {
57
+ const suffix = action !== 'install-group' ? ` (${action})` : '';
58
+ console.log(` ${ok ? '✓' : '✗'} ${label}${suffix}`);
59
+ });
60
+ }
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": "1.0.0",
4
4
  "description": "Claude Code skills installer for the trendai-crem team",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -12,7 +12,9 @@
12
12
  },
13
13
  "files": [
14
14
  "cli.js",
15
+ "sources.json",
15
16
  "marketplace.json",
17
+ "lib/",
16
18
  "skills/"
17
19
  ],
18
20
  "type": "module",
package/sources.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "version": 1,
3
+ "sources": [
4
+ {
5
+ "type": "skills-dir",
6
+ "path": "./skills",
7
+ "label": "team-skills"
8
+ },
9
+ {
10
+ "type": "skills-repo",
11
+ "repo": "obra/superpowers",
12
+ "flags": [
13
+ "--all"
14
+ ],
15
+ "label": "superpowers"
16
+ },
17
+ {
18
+ "type": "marketplace",
19
+ "name": "ai-skill-marketplace",
20
+ "source": "git@github.com:trend-ai-taskforce/ai-skill-marketplace.git",
21
+ "plugins": [
22
+ "wiki-tools",
23
+ "atlassian-tools",
24
+ "google-style-guides",
25
+ "l2-automation",
26
+ "service-doc-generator",
27
+ "claude-on-teams"
28
+ ]
29
+ },
30
+ {
31
+ "type": "marketplace",
32
+ "name": "claude-plugins-official",
33
+ "source": "anthropics/claude-plugins-official",
34
+ "plugins": [
35
+ "ralph-loop"
36
+ ]
37
+ }
38
+ ]
39
+ }