@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.
Files changed (3) hide show
  1. package/cli.js +64 -46
  2. package/marketplace.json +20 -11
  3. 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 URL for a marketplace from known_marketplaces.json.
94
- // Handles both string entries and object entries with a `source` field.
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
- return typeof entry.source === 'string' ? entry.source : null;
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 URL to a canonical host/path form for comparison.
105
- // Treats SSH and HTTPS URLs pointing to the same repo as equivalent.
106
- // Returns null if the URL cannot be parsed.
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
- function installMarketplacePlugins() {
121
- const configPath = join(__dir, 'marketplace.json');
122
- if (!existsSync(configPath)) return [];
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
- typeof marketplace?.name !== 'string' ||
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(marketplace.name) || !SAFE_SOURCE.test(marketplace.source)) {
142
- console.error('\nInvalid marketplace name or source URL in marketplace.json');
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 config on any invalid plugin name (fail-closed, not silent filter).
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 in marketplace.json: ${invalidPlugins.map(p => JSON.stringify(p)).join(', ')}`);
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 validPlugins.map(plugin => ({ plugin, action: 'skipped (source mismatch)', ok: false }));
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 validPlugins.map(plugin => ({ plugin, action: 'skipped (no marketplace)', ok: false }));
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
- // Load installed plugin registry (SCD-7)
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
- "marketplace": {
3
- "name": "ai-skill-marketplace",
4
- "source": "git@github.com:trend-ai-taskforce/ai-skill-marketplace.git"
5
- },
6
- "plugins": [
7
- "wiki-tools",
8
- "atlassian-tools",
9
- "google-style-guides",
10
- "l2-automation",
11
- "service-doc-generator",
12
- "claude-on-teams"
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trendai-crem/claude-skills",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Claude Code skills installer for the trendai-crem team",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {