@trendai-crem/claude-skills 0.9.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.
- package/cli.js +163 -51
- package/marketplace.json +19 -11
- 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,21 +110,63 @@ function readJsonObject(filePath, fallback, label) {
|
|
|
90
110
|
return fallback;
|
|
91
111
|
}
|
|
92
112
|
|
|
93
|
-
//
|
|
94
|
-
//
|
|
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
|
+
|
|
146
|
+
// Extracts the registered source for a marketplace from known_marketplaces.json.
|
|
147
|
+
// Handles: plain string, object with string .source, and GitHub registry format
|
|
148
|
+
// { source: { source: "github", repo: "org/repo" } }.
|
|
95
149
|
function getRegisteredMarketplaceSource(known, marketplaceName) {
|
|
96
150
|
const entry = known[marketplaceName];
|
|
97
151
|
if (typeof entry === 'string') return entry;
|
|
98
152
|
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
99
|
-
|
|
153
|
+
if (typeof entry.source === 'string') return entry.source;
|
|
154
|
+
// GitHub registry format: { source: { source: "github", repo: "org/repo" }, ... }
|
|
155
|
+
if (entry.source?.source === 'github' && typeof entry.source?.repo === 'string') {
|
|
156
|
+
return entry.source.repo;
|
|
157
|
+
}
|
|
100
158
|
}
|
|
101
159
|
return null;
|
|
102
160
|
}
|
|
103
161
|
|
|
104
|
-
// Normalizes a marketplace source
|
|
105
|
-
//
|
|
106
|
-
// Returns null if the
|
|
162
|
+
// Normalizes a marketplace source to a canonical host/path form for comparison.
|
|
163
|
+
// Accepts SSH URLs, HTTPS URLs, and GitHub shorthand (org/repo).
|
|
164
|
+
// Returns null if the source cannot be parsed.
|
|
107
165
|
function normalizeMarketplaceSource(rawSource) {
|
|
166
|
+
// GitHub shorthand: org/repo (no slashes in org, no protocol)
|
|
167
|
+
if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(rawSource)) {
|
|
168
|
+
return rawSource.toLowerCase();
|
|
169
|
+
}
|
|
108
170
|
const sshMatch = /^git@([^:]+):(.+?)(?:\.git)?$/.exec(rawSource);
|
|
109
171
|
if (sshMatch) {
|
|
110
172
|
return `${sshMatch[1].toLowerCase()}/${sshMatch[2].replace(/^\/+/, '').replace(/\.git$/, '')}`;
|
|
@@ -117,58 +179,42 @@ function normalizeMarketplaceSource(rawSource) {
|
|
|
117
179
|
}
|
|
118
180
|
}
|
|
119
181
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// Parse and validate marketplace.json — all failures are non-fatal (SCD-7)
|
|
125
|
-
const config = readJsonObject(configPath, null, 'marketplace.json');
|
|
126
|
-
const { marketplace, plugins } = config ?? {};
|
|
127
|
-
|
|
182
|
+
// Installs or updates all plugins for a single marketplace entry.
|
|
183
|
+
// Returns an array of { plugin, action, ok } results.
|
|
184
|
+
function installFromMarketplace(entry, known, installed) {
|
|
128
185
|
// Input validation + allowlisting (SCD-1, OWASP-A04)
|
|
129
186
|
// Leading `-` is rejected to prevent argv injection: `claude plugin install -s` parses as flag.
|
|
130
187
|
const SAFE_NAME = /^(?!-)[A-Za-z0-9_][A-Za-z0-9_-]*$/;
|
|
131
188
|
const SAFE_SOURCE = /^(git@[\w.-]+:[\w.\-/]+\.git|https:\/\/[\w.-]+\/[\w.\-/]+\.git)$/;
|
|
189
|
+
const SAFE_GITHUB = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
190
|
+
|
|
191
|
+
const { name: marketplaceName, source, plugins } = entry ?? {};
|
|
132
192
|
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
typeof marketplace?.source !== 'string' ||
|
|
136
|
-
!Array.isArray(plugins)
|
|
137
|
-
) {
|
|
138
|
-
console.error('\nInvalid marketplace.json: missing required fields (marketplace.name, marketplace.source, plugins[])');
|
|
193
|
+
if (typeof marketplaceName !== 'string' || typeof source !== 'string' || !Array.isArray(plugins)) {
|
|
194
|
+
console.error('\nInvalid marketplace entry: missing required fields (name, source, plugins[])');
|
|
139
195
|
return [];
|
|
140
196
|
}
|
|
141
|
-
if (!SAFE_NAME.test(
|
|
142
|
-
console.error(
|
|
197
|
+
if (!SAFE_NAME.test(marketplaceName) || (!SAFE_SOURCE.test(source) && !SAFE_GITHUB.test(source))) {
|
|
198
|
+
console.error(`\nInvalid marketplace name or source: ${marketplaceName}`);
|
|
143
199
|
return [];
|
|
144
200
|
}
|
|
145
|
-
// Reject the entire
|
|
201
|
+
// Reject the entire entry on any invalid plugin name (fail-closed, not silent filter).
|
|
146
202
|
const invalidPlugins = plugins.filter(p => typeof p !== 'string' || !SAFE_NAME.test(p));
|
|
147
203
|
if (invalidPlugins.length > 0) {
|
|
148
|
-
console.error(`\nInvalid plugin names
|
|
204
|
+
console.error(`\nInvalid plugin names for ${marketplaceName}: ${invalidPlugins.map(p => JSON.stringify(p)).join(', ')}`);
|
|
149
205
|
return [];
|
|
150
206
|
}
|
|
151
|
-
const validPlugins = plugins;
|
|
152
207
|
|
|
153
|
-
const { name: marketplaceName, source } = marketplace;
|
|
154
208
|
console.log(`\nInstalling marketplace plugins (${marketplaceName})...\n`);
|
|
155
209
|
|
|
156
210
|
// Register marketplace if not already known — verify source matches to prevent supply-chain mismatch (OWASP-A08)
|
|
157
|
-
const knownPath = join(homedir(), '.claude', 'plugins', 'known_marketplaces.json');
|
|
158
|
-
// Distinguish "file missing" (first-run, safe) from "file corrupted" (fail-closed).
|
|
159
|
-
const knownExists = existsSync(knownPath);
|
|
160
|
-
const known = readJsonObject(knownPath, knownExists ? null : {}, 'known_marketplaces.json');
|
|
161
|
-
if (knownExists && known === null) {
|
|
162
|
-
console.error('\nInvalid known_marketplaces.json: refusing to register marketplace automatically');
|
|
163
|
-
return validPlugins.map(plugin => ({ plugin, action: 'skipped (invalid marketplace registry)', ok: false }));
|
|
164
|
-
}
|
|
165
211
|
const registeredSource = getRegisteredMarketplaceSource(known, marketplaceName);
|
|
166
212
|
const normalizedSource = normalizeMarketplaceSource(source);
|
|
167
213
|
const normalizedRegistered = registeredSource ? normalizeMarketplaceSource(registeredSource) : null;
|
|
168
214
|
|
|
169
215
|
if (registeredSource && (!normalizedRegistered || normalizedRegistered !== normalizedSource)) {
|
|
170
216
|
console.error(`\nMarketplace source mismatch for ${marketplaceName}: registered as ${registeredSource}, but marketplace.json specifies ${source}`);
|
|
171
|
-
return
|
|
217
|
+
return plugins.map(plugin => ({ plugin, action: 'skipped (source mismatch)', ok: false }));
|
|
172
218
|
}
|
|
173
219
|
|
|
174
220
|
if (!registeredSource) {
|
|
@@ -179,7 +225,7 @@ function installMarketplacePlugins() {
|
|
|
179
225
|
} catch (error) {
|
|
180
226
|
const exitInfo = error.status != null ? `exit ${error.status}` : error.code ?? error.message;
|
|
181
227
|
console.error(`\nFailed to register marketplace: ${marketplaceName} (${exitInfo})`);
|
|
182
|
-
return
|
|
228
|
+
return plugins.map(plugin => ({ plugin, action: 'skipped (no marketplace)', ok: false }));
|
|
183
229
|
}
|
|
184
230
|
}
|
|
185
231
|
|
|
@@ -191,16 +237,7 @@ function installMarketplacePlugins() {
|
|
|
191
237
|
console.warn(`\nWARN: Failed to update marketplace ${marketplaceName} (${exitInfo}) — using cached version`);
|
|
192
238
|
}
|
|
193
239
|
|
|
194
|
-
|
|
195
|
-
const installedPath = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
196
|
-
const installedData = readJsonObject(installedPath, {}, 'installed_plugins.json');
|
|
197
|
-
// installed_plugins.json v2 nests under "plugins", v1 is flat
|
|
198
|
-
const rawInstalled = installedData.plugins ?? installedData;
|
|
199
|
-
const installed = (typeof rawInstalled === 'object' && rawInstalled !== null && !Array.isArray(rawInstalled))
|
|
200
|
-
? rawInstalled
|
|
201
|
-
: {};
|
|
202
|
-
|
|
203
|
-
return validPlugins.map(plugin => {
|
|
240
|
+
return plugins.map(plugin => {
|
|
204
241
|
const key = `${plugin}@${marketplaceName}`;
|
|
205
242
|
const isInstalled = Object.hasOwn(installed, key); // Object.hasOwn avoids prototype chain traversal (SCD-7)
|
|
206
243
|
const action = isInstalled ? 'update' : 'install';
|
|
@@ -218,6 +255,81 @@ function installMarketplacePlugins() {
|
|
|
218
255
|
});
|
|
219
256
|
}
|
|
220
257
|
|
|
258
|
+
function installMarketplacePlugins() {
|
|
259
|
+
const configPath = join(__dir, 'marketplace.json');
|
|
260
|
+
if (!existsSync(configPath)) return [];
|
|
261
|
+
|
|
262
|
+
// Parse marketplace.json — all failures are non-fatal (SCD-7)
|
|
263
|
+
const config = readJsonObject(configPath, null, 'marketplace.json');
|
|
264
|
+
const marketplaces = config?.marketplaces;
|
|
265
|
+
if (!Array.isArray(marketplaces) || marketplaces.length === 0) {
|
|
266
|
+
console.warn('\nWARN: marketplace.json has no valid "marketplaces" array, skipping');
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Load shared state once: known registry (for source verification) and installed plugins
|
|
271
|
+
const knownPath = join(homedir(), '.claude', 'plugins', 'known_marketplaces.json');
|
|
272
|
+
const knownExists = existsSync(knownPath);
|
|
273
|
+
const known = readJsonObject(knownPath, knownExists ? null : {}, 'known_marketplaces.json');
|
|
274
|
+
if (knownExists && known === null) {
|
|
275
|
+
console.error('\nInvalid known_marketplaces.json: refusing to register any marketplace automatically');
|
|
276
|
+
return marketplaces.flatMap(e =>
|
|
277
|
+
(e?.plugins ?? []).map(plugin => ({ plugin, action: 'skipped (invalid marketplace registry)', ok: false }))
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const installedPath = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
282
|
+
const installedData = readJsonObject(installedPath, {}, 'installed_plugins.json');
|
|
283
|
+
// installed_plugins.json v2 nests under "plugins", v1 is flat
|
|
284
|
+
const rawInstalled = installedData.plugins ?? installedData;
|
|
285
|
+
const installed = (typeof rawInstalled === 'object' && rawInstalled !== null && !Array.isArray(rawInstalled))
|
|
286
|
+
? rawInstalled
|
|
287
|
+
: {};
|
|
288
|
+
|
|
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
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
221
333
|
function setupAutoUpdate() {
|
|
222
334
|
const { version: installedVersion } = JSON.parse(
|
|
223
335
|
readFileSync(join(__dir, 'package.json'), 'utf8')
|
package/marketplace.json
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
"marketplaces": [
|
|
3
|
+
{
|
|
4
|
+
"name": "ai-skill-marketplace",
|
|
5
|
+
"source": "git@github.com:trend-ai-taskforce/ai-skill-marketplace.git",
|
|
6
|
+
"plugins": [
|
|
7
|
+
"wiki-tools",
|
|
8
|
+
"atlassian-tools",
|
|
9
|
+
"google-style-guides",
|
|
10
|
+
"l2-automation",
|
|
11
|
+
"service-doc-generator"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "claude-plugins-official",
|
|
16
|
+
"source": "anthropics/claude-plugins-official",
|
|
17
|
+
"plugins": [
|
|
18
|
+
"ralph-loop"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
13
21
|
]
|
|
14
22
|
}
|