@trendai-crem/claude-skills 0.9.0 → 0.10.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 +64 -46
- package/marketplace.json +20 -11
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -90,21 +90,30 @@ function readJsonObject(filePath, fallback, label) {
|
|
|
90
90
|
return fallback;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
// Extracts the registered source
|
|
94
|
-
// Handles
|
|
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" } }.
|
|
95
96
|
function getRegisteredMarketplaceSource(known, marketplaceName) {
|
|
96
97
|
const entry = known[marketplaceName];
|
|
97
98
|
if (typeof entry === 'string') return entry;
|
|
98
99
|
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
99
|
-
|
|
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;
|
|
104
|
+
}
|
|
100
105
|
}
|
|
101
106
|
return null;
|
|
102
107
|
}
|
|
103
108
|
|
|
104
|
-
// Normalizes a marketplace source
|
|
105
|
-
//
|
|
106
|
-
// Returns null if the
|
|
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.
|
|
107
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
|
+
}
|
|
108
117
|
const sshMatch = /^git@([^:]+):(.+?)(?:\.git)?$/.exec(rawSource);
|
|
109
118
|
if (sshMatch) {
|
|
110
119
|
return `${sshMatch[1].toLowerCase()}/${sshMatch[2].replace(/^\/+/, '').replace(/\.git$/, '')}`;
|
|
@@ -117,58 +126,42 @@ function normalizeMarketplaceSource(rawSource) {
|
|
|
117
126
|
}
|
|
118
127
|
}
|
|
119
128
|
|
|
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
|
-
|
|
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) {
|
|
128
132
|
// Input validation + allowlisting (SCD-1, OWASP-A04)
|
|
129
133
|
// Leading `-` is rejected to prevent argv injection: `claude plugin install -s` parses as flag.
|
|
130
134
|
const SAFE_NAME = /^(?!-)[A-Za-z0-9_][A-Za-z0-9_-]*$/;
|
|
131
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 ?? {};
|
|
132
139
|
|
|
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[])');
|
|
140
|
+
if (typeof marketplaceName !== 'string' || typeof source !== 'string' || !Array.isArray(plugins)) {
|
|
141
|
+
console.error('\nInvalid marketplace entry: missing required fields (name, source, plugins[])');
|
|
139
142
|
return [];
|
|
140
143
|
}
|
|
141
|
-
if (!SAFE_NAME.test(
|
|
142
|
-
console.error(
|
|
144
|
+
if (!SAFE_NAME.test(marketplaceName) || (!SAFE_SOURCE.test(source) && !SAFE_GITHUB.test(source))) {
|
|
145
|
+
console.error(`\nInvalid marketplace name or source: ${marketplaceName}`);
|
|
143
146
|
return [];
|
|
144
147
|
}
|
|
145
|
-
// Reject the entire
|
|
148
|
+
// Reject the entire entry on any invalid plugin name (fail-closed, not silent filter).
|
|
146
149
|
const invalidPlugins = plugins.filter(p => typeof p !== 'string' || !SAFE_NAME.test(p));
|
|
147
150
|
if (invalidPlugins.length > 0) {
|
|
148
|
-
console.error(`\nInvalid plugin names
|
|
151
|
+
console.error(`\nInvalid plugin names for ${marketplaceName}: ${invalidPlugins.map(p => JSON.stringify(p)).join(', ')}`);
|
|
149
152
|
return [];
|
|
150
153
|
}
|
|
151
|
-
const validPlugins = plugins;
|
|
152
154
|
|
|
153
|
-
const { name: marketplaceName, source } = marketplace;
|
|
154
155
|
console.log(`\nInstalling marketplace plugins (${marketplaceName})...\n`);
|
|
155
156
|
|
|
156
157
|
// 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
158
|
const registeredSource = getRegisteredMarketplaceSource(known, marketplaceName);
|
|
166
159
|
const normalizedSource = normalizeMarketplaceSource(source);
|
|
167
160
|
const normalizedRegistered = registeredSource ? normalizeMarketplaceSource(registeredSource) : null;
|
|
168
161
|
|
|
169
162
|
if (registeredSource && (!normalizedRegistered || normalizedRegistered !== normalizedSource)) {
|
|
170
163
|
console.error(`\nMarketplace source mismatch for ${marketplaceName}: registered as ${registeredSource}, but marketplace.json specifies ${source}`);
|
|
171
|
-
return
|
|
164
|
+
return plugins.map(plugin => ({ plugin, action: 'skipped (source mismatch)', ok: false }));
|
|
172
165
|
}
|
|
173
166
|
|
|
174
167
|
if (!registeredSource) {
|
|
@@ -179,7 +172,7 @@ function installMarketplacePlugins() {
|
|
|
179
172
|
} catch (error) {
|
|
180
173
|
const exitInfo = error.status != null ? `exit ${error.status}` : error.code ?? error.message;
|
|
181
174
|
console.error(`\nFailed to register marketplace: ${marketplaceName} (${exitInfo})`);
|
|
182
|
-
return
|
|
175
|
+
return plugins.map(plugin => ({ plugin, action: 'skipped (no marketplace)', ok: false }));
|
|
183
176
|
}
|
|
184
177
|
}
|
|
185
178
|
|
|
@@ -191,16 +184,7 @@ function installMarketplacePlugins() {
|
|
|
191
184
|
console.warn(`\nWARN: Failed to update marketplace ${marketplaceName} (${exitInfo}) — using cached version`);
|
|
192
185
|
}
|
|
193
186
|
|
|
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 => {
|
|
187
|
+
return plugins.map(plugin => {
|
|
204
188
|
const key = `${plugin}@${marketplaceName}`;
|
|
205
189
|
const isInstalled = Object.hasOwn(installed, key); // Object.hasOwn avoids prototype chain traversal (SCD-7)
|
|
206
190
|
const action = isInstalled ? 'update' : 'install';
|
|
@@ -218,6 +202,40 @@ function installMarketplacePlugins() {
|
|
|
218
202
|
});
|
|
219
203
|
}
|
|
220
204
|
|
|
205
|
+
function installMarketplacePlugins() {
|
|
206
|
+
const configPath = join(__dir, 'marketplace.json');
|
|
207
|
+
if (!existsSync(configPath)) return [];
|
|
208
|
+
|
|
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
|
+
}
|
|
238
|
+
|
|
221
239
|
function setupAutoUpdate() {
|
|
222
240
|
const { version: installedVersion } = JSON.parse(
|
|
223
241
|
readFileSync(join(__dir, 'package.json'), 'utf8')
|
package/marketplace.json
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
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
|
+
"claude-on-teams"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "claude-plugins-official",
|
|
17
|
+
"source": "anthropics/claude-plugins-official",
|
|
18
|
+
"plugins": [
|
|
19
|
+
"ralph-loop"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
13
22
|
]
|
|
14
23
|
}
|