@lenne.tech/cli 1.38.0 → 1.39.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/build/commands/claude/marketplaces.js +245 -0
- package/build/commands/claude/plugins.js +30 -8
- package/build/commands/dev/prune.js +6 -2
- package/build/commands/dev/up.js +3 -1
- package/build/lib/claude-cli.js +28 -1
- package/build/lib/marketplace-config.js +120 -0
- package/build/lib/marketplace.js +225 -27
- package/docs/commands.md +43 -1
- package/package.json +1 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const claude_cli_1 = require("../../lib/claude-cli");
|
|
13
|
+
const marketplace_config_1 = require("../../lib/marketplace-config");
|
|
14
|
+
/**
|
|
15
|
+
* Print the configured marketplaces.
|
|
16
|
+
*/
|
|
17
|
+
function printConfigured(toolbox) {
|
|
18
|
+
var _a, _b;
|
|
19
|
+
const { print: { info }, } = toolbox;
|
|
20
|
+
const configured = (0, marketplace_config_1.readConfiguredMarketplaces)();
|
|
21
|
+
info('');
|
|
22
|
+
info(`Configured marketplaces (${(0, marketplace_config_1.getMarketplaceConfigPath)()}):`);
|
|
23
|
+
if (configured.length === 0) {
|
|
24
|
+
info(' (none yet — add one with: lt claude marketplaces add)');
|
|
25
|
+
info('');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
for (const m of configured) {
|
|
29
|
+
const flags = [
|
|
30
|
+
m.private ? 'private' : 'public',
|
|
31
|
+
`provider=${(_a = m.provider) !== null && _a !== void 0 ? _a : 'git'}`,
|
|
32
|
+
`autoInstall=${(_b = m.autoInstall) !== null && _b !== void 0 ? _b : true}`,
|
|
33
|
+
];
|
|
34
|
+
info(` ${m.name}`);
|
|
35
|
+
info(` source: ${m.source}`);
|
|
36
|
+
info(` ${flags.join(', ')}${m.description ? ` — ${m.description}` : ''}`);
|
|
37
|
+
}
|
|
38
|
+
info('');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the marketplace name that a `claude plugin marketplace add` produced,
|
|
42
|
+
* by diffing the Claude registry before/after and falling back to a source match.
|
|
43
|
+
*/
|
|
44
|
+
function resolveAddedName(source, before) {
|
|
45
|
+
var _a, _b, _c, _d, _e, _f;
|
|
46
|
+
const known = (0, claude_cli_1.readKnownMarketplaces)();
|
|
47
|
+
const after = Object.keys(known);
|
|
48
|
+
const added = after.filter((n) => !before.includes(n));
|
|
49
|
+
if (added.length === 1) {
|
|
50
|
+
return added[0];
|
|
51
|
+
}
|
|
52
|
+
// Already present (or ambiguous): match by source (repo/url contains the input).
|
|
53
|
+
const needle = source.trim().replace(/\.git$/, '');
|
|
54
|
+
const ownerRepo = (_b = (_a = needle.match(/[:/]([^/\s]+\/[^/\s]+)$/)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : needle;
|
|
55
|
+
for (const [name, entry] of Object.entries(known)) {
|
|
56
|
+
const src = `${(_d = (_c = entry.source) === null || _c === void 0 ? void 0 : _c.repo) !== null && _d !== void 0 ? _d : ''} ${(_f = (_e = entry.source) === null || _e === void 0 ? void 0 : _e.url) !== null && _f !== void 0 ? _f : ''}`;
|
|
57
|
+
if (src.includes(ownerRepo) || src.includes(needle)) {
|
|
58
|
+
return name;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Add (or update) a configured marketplace.
|
|
65
|
+
*/
|
|
66
|
+
function runAdd(toolbox, sourceArg) {
|
|
67
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
68
|
+
const { parameters, print: { error, info, spin, success }, prompt: { ask, confirm }, } = toolbox;
|
|
69
|
+
const cli = (0, claude_cli_1.findClaudeCli)();
|
|
70
|
+
if (!cli) {
|
|
71
|
+
error('Claude CLI not found. Please install Claude Code first.');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Resolve the source (Git URL or owner/repo).
|
|
75
|
+
let source = (sourceArg || parameters.options.source || '').trim();
|
|
76
|
+
if (!source) {
|
|
77
|
+
const answer = yield ask([
|
|
78
|
+
{
|
|
79
|
+
message: 'Marketplace source (Git URL or owner/repo):',
|
|
80
|
+
name: 'source',
|
|
81
|
+
type: 'input',
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
source = (answer.source || '').trim();
|
|
85
|
+
}
|
|
86
|
+
if (!source) {
|
|
87
|
+
error('No source provided.');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Add via the Claude CLI (this clones the repo locally, honoring the user's Git
|
|
91
|
+
// access) and determine the resulting marketplace name from the registry.
|
|
92
|
+
const before = (0, claude_cli_1.listKnownMarketplaceNames)();
|
|
93
|
+
const addSpinner = spin(`Adding marketplace from ${source}`);
|
|
94
|
+
const addResult = (0, claude_cli_1.runClaudeCommand)(cli, `plugin marketplace add ${source}`);
|
|
95
|
+
if (!addResult.success && !addResult.output.includes('already')) {
|
|
96
|
+
addSpinner.fail('Failed to add marketplace');
|
|
97
|
+
error(addResult.output.trim());
|
|
98
|
+
info('');
|
|
99
|
+
info('Make sure you have access to the repository (SSH key / permissions).');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const name = resolveAddedName(source, before);
|
|
103
|
+
if (!name) {
|
|
104
|
+
addSpinner.fail('Could not determine the marketplace name');
|
|
105
|
+
info('The repository was added to Claude, but its name could not be resolved.');
|
|
106
|
+
info('Check: claude plugin marketplace list');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
addSpinner.succeed(`Marketplace "${name}" available`);
|
|
110
|
+
// Gather options (defaults geared towards a private internal marketplace).
|
|
111
|
+
const isPrivate = yield confirm('Is this a private / access-restricted repository?', true);
|
|
112
|
+
const autoInstall = yield confirm('Auto-install all its plugins on `lt claude plugins`?', true);
|
|
113
|
+
const descAnswer = yield ask([
|
|
114
|
+
{
|
|
115
|
+
message: 'Description (optional):',
|
|
116
|
+
name: 'description',
|
|
117
|
+
type: 'input',
|
|
118
|
+
},
|
|
119
|
+
]);
|
|
120
|
+
const entry = {
|
|
121
|
+
autoInstall,
|
|
122
|
+
name,
|
|
123
|
+
private: isPrivate,
|
|
124
|
+
provider: 'git',
|
|
125
|
+
source,
|
|
126
|
+
};
|
|
127
|
+
const description = (descAnswer.description || '').trim();
|
|
128
|
+
if (description) {
|
|
129
|
+
entry.description = description;
|
|
130
|
+
}
|
|
131
|
+
(0, marketplace_config_1.upsertConfiguredMarketplace)(entry);
|
|
132
|
+
info('');
|
|
133
|
+
success(`Saved marketplace "${name}" to ${(0, marketplace_config_1.getMarketplaceConfigPath)()}`);
|
|
134
|
+
info('It will be used automatically on the next `lt claude plugins` run.');
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Interactive menu when no action is given.
|
|
139
|
+
*/
|
|
140
|
+
function runMenu(toolbox) {
|
|
141
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
142
|
+
const { prompt: { ask }, } = toolbox;
|
|
143
|
+
printConfigured(toolbox);
|
|
144
|
+
const answer = yield ask([
|
|
145
|
+
{
|
|
146
|
+
choices: ['List', 'Add', 'Remove', 'Exit'],
|
|
147
|
+
message: 'Manage Claude marketplaces',
|
|
148
|
+
name: 'action',
|
|
149
|
+
type: 'select',
|
|
150
|
+
},
|
|
151
|
+
]);
|
|
152
|
+
switch (answer.action) {
|
|
153
|
+
case 'Add':
|
|
154
|
+
yield runAdd(toolbox);
|
|
155
|
+
break;
|
|
156
|
+
case 'List':
|
|
157
|
+
printConfigured(toolbox);
|
|
158
|
+
break;
|
|
159
|
+
case 'Remove':
|
|
160
|
+
yield runRemove(toolbox);
|
|
161
|
+
break;
|
|
162
|
+
default:
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Remove a configured marketplace.
|
|
169
|
+
*/
|
|
170
|
+
function runRemove(toolbox, nameArg) {
|
|
171
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
172
|
+
const { print: { error, info, success, warning }, prompt: { ask, confirm }, } = toolbox;
|
|
173
|
+
const configured = (0, marketplace_config_1.readConfiguredMarketplaces)();
|
|
174
|
+
if (configured.length === 0) {
|
|
175
|
+
warning('No configured marketplaces to remove.');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
let name = (nameArg || '').trim();
|
|
179
|
+
if (!name) {
|
|
180
|
+
const answer = yield ask([
|
|
181
|
+
{
|
|
182
|
+
choices: configured.map((m) => m.name),
|
|
183
|
+
message: 'Which marketplace do you want to remove?',
|
|
184
|
+
name: 'name',
|
|
185
|
+
type: 'select',
|
|
186
|
+
},
|
|
187
|
+
]);
|
|
188
|
+
name = (answer.name || '').trim();
|
|
189
|
+
}
|
|
190
|
+
const { removed } = (0, marketplace_config_1.removeConfiguredMarketplace)(name);
|
|
191
|
+
if (!removed) {
|
|
192
|
+
error(`Marketplace "${name}" is not in the configuration.`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
success(`Removed "${name}" from the lt configuration.`);
|
|
196
|
+
// Offer to also detach it from the Claude CLI.
|
|
197
|
+
const cli = (0, claude_cli_1.findClaudeCli)();
|
|
198
|
+
if (cli) {
|
|
199
|
+
const alsoClaude = yield confirm(`Also remove it from Claude (claude plugin marketplace remove ${name})?`, false);
|
|
200
|
+
if (alsoClaude) {
|
|
201
|
+
const result = (0, claude_cli_1.runClaudeCommand)(cli, `plugin marketplace remove ${name}`);
|
|
202
|
+
if (result.success || result.output.includes('not found')) {
|
|
203
|
+
info(`Detached "${name}" from Claude.`);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
warning(`Could not detach from Claude: ${result.output.trim()}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Manage additional Claude Code plugin marketplaces (GitLab, GitHub or any Git host).
|
|
214
|
+
* The configuration is stored locally so internal/private repositories never end
|
|
215
|
+
* up in the (public) CLI source, and it is used on every `lt claude plugins` run.
|
|
216
|
+
*/
|
|
217
|
+
const MarketplacesCommand = {
|
|
218
|
+
alias: ['mp'],
|
|
219
|
+
description: 'Manage additional Claude plugin marketplaces',
|
|
220
|
+
name: 'marketplaces',
|
|
221
|
+
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
222
|
+
var _a;
|
|
223
|
+
const { parameters } = toolbox;
|
|
224
|
+
const action = (parameters.first || '').toLowerCase();
|
|
225
|
+
const arg = (_a = parameters.array) === null || _a === void 0 ? void 0 : _a[1];
|
|
226
|
+
if (action === 'list' || action === 'ls' || parameters.options.list) {
|
|
227
|
+
printConfigured(toolbox);
|
|
228
|
+
}
|
|
229
|
+
else if (action === 'add' || parameters.options.add) {
|
|
230
|
+
yield runAdd(toolbox, action === 'add' ? arg : undefined);
|
|
231
|
+
}
|
|
232
|
+
else if (action === 'remove' || action === 'rm' || parameters.options.remove) {
|
|
233
|
+
const removeName = typeof parameters.options.remove === 'string' ? parameters.options.remove : arg;
|
|
234
|
+
yield runRemove(toolbox, removeName);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
yield runMenu(toolbox);
|
|
238
|
+
}
|
|
239
|
+
if (!parameters.options.fromGluegunMenu) {
|
|
240
|
+
process.exit(0);
|
|
241
|
+
}
|
|
242
|
+
return 'claude marketplaces';
|
|
243
|
+
}),
|
|
244
|
+
};
|
|
245
|
+
exports.default = MarketplacesCommand;
|
|
@@ -11,6 +11,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
const claude_cli_1 = require("../../lib/claude-cli");
|
|
13
13
|
const marketplace_1 = require("../../lib/marketplace");
|
|
14
|
+
const marketplace_config_1 = require("../../lib/marketplace-config");
|
|
14
15
|
const plugin_utils_1 = require("../../lib/plugin-utils");
|
|
15
16
|
const shell_config_1 = require("../../lib/shell-config");
|
|
16
17
|
/**
|
|
@@ -145,10 +146,10 @@ const PluginsCommand = {
|
|
|
145
146
|
info('Installation: https://docs.anthropic.com/en/docs/claude-code');
|
|
146
147
|
process.exit(1);
|
|
147
148
|
}
|
|
148
|
-
// Fetch available plugins from
|
|
149
|
+
// Fetch available plugins from all marketplaces (built-in + configured)
|
|
149
150
|
let availablePlugins;
|
|
150
151
|
try {
|
|
151
|
-
availablePlugins = yield (0, marketplace_1.fetchAvailablePlugins)(spin);
|
|
152
|
+
availablePlugins = yield (0, marketplace_1.fetchAvailablePlugins)(spin, cli);
|
|
152
153
|
}
|
|
153
154
|
catch (err) {
|
|
154
155
|
error(`Failed to fetch plugins: ${err.message}`);
|
|
@@ -192,8 +193,7 @@ const PluginsCommand = {
|
|
|
192
193
|
}
|
|
193
194
|
else {
|
|
194
195
|
// Install all plugins from primary marketplace (lenne-tech) plus default external plugins
|
|
195
|
-
const
|
|
196
|
-
const primaryPlugins = availablePlugins.filter((p) => p.marketplaceName === primaryMarketplace);
|
|
196
|
+
const primaryPlugins = availablePlugins.filter((p) => p.marketplaceName === marketplace_1.PRIMARY_MARKETPLACE_NAME);
|
|
197
197
|
// Add default external plugins
|
|
198
198
|
const externalPlugins = [];
|
|
199
199
|
for (const defaultPlugin of marketplace_1.DEFAULT_EXTERNAL_PLUGINS) {
|
|
@@ -202,10 +202,23 @@ const PluginsCommand = {
|
|
|
202
202
|
externalPlugins.push(plugin);
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
// Also install plugins from configured auto-install marketplaces (e.g. the
|
|
206
|
+
// internal one). They only appear in availablePlugins when the user has
|
|
207
|
+
// repo access, so this auto-installs them for authorized team members and
|
|
208
|
+
// is a no-op for everyone else.
|
|
209
|
+
const autoInstallNames = new Set((0, marketplace_1.getAllMarketplaces)()
|
|
210
|
+
.filter((m) => m.autoInstall && m.name !== marketplace_1.PRIMARY_MARKETPLACE_NAME)
|
|
211
|
+
.map((m) => m.name));
|
|
212
|
+
const internalPlugins = availablePlugins.filter((p) => autoInstallNames.has(p.marketplaceName));
|
|
213
|
+
pluginsToInstall = [...primaryPlugins, ...externalPlugins, ...internalPlugins];
|
|
214
|
+
if (externalPlugins.length > 0 || internalPlugins.length > 0) {
|
|
215
|
+
info(`Installing ${primaryPlugins.length} plugins from ${marketplace_1.PRIMARY_MARKETPLACE_NAME}`);
|
|
216
|
+
if (externalPlugins.length > 0) {
|
|
217
|
+
info(` + ${externalPlugins.length} default plugins: ${externalPlugins.map((p) => p.pluginName).join(', ')}`);
|
|
218
|
+
}
|
|
219
|
+
if (internalPlugins.length > 0) {
|
|
220
|
+
info(` + ${internalPlugins.length} internal plugins: ${internalPlugins.map((p) => p.pluginName).join(', ')}`);
|
|
221
|
+
}
|
|
209
222
|
}
|
|
210
223
|
else {
|
|
211
224
|
info(`Installing all plugins (${pluginsToInstall.length})...`);
|
|
@@ -306,6 +319,15 @@ const PluginsCommand = {
|
|
|
306
319
|
}
|
|
307
320
|
}
|
|
308
321
|
info('');
|
|
322
|
+
// Remind the user that marketplaces are configurable and used on every run.
|
|
323
|
+
const configuredCount = (0, marketplace_config_1.readConfiguredMarketplaces)().length;
|
|
324
|
+
if (configuredCount > 0) {
|
|
325
|
+
info(`Tip: ${configuredCount} extra marketplace${configuredCount > 1 ? 's' : ''} configured — manage with \`lt claude marketplaces\`.`);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
info('Tip: Add private/internal or extra plugin marketplaces (GitLab, GitHub, any Git host) with `lt claude marketplaces`.');
|
|
329
|
+
}
|
|
330
|
+
info('');
|
|
309
331
|
if (!toolbox.parameters.options.fromGluegunMenu) {
|
|
310
332
|
process.exit(totalIssues > 0 ? 1 : 0);
|
|
311
333
|
}
|
|
@@ -56,8 +56,12 @@ const PruneCommand = {
|
|
|
56
56
|
const mainLayout = (0, dev_project_1.resolveLayout)(mainRepoRoot, filesystem);
|
|
57
57
|
const slug = (0, dev_identity_1.buildIdentity)(mainRepoRoot).slug;
|
|
58
58
|
const projectDevDb = (0, dev_project_1.deriveDbName)(mainLayout.apiDir, slug);
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
// Always observe — the smoke-test sweep is GLOBAL (reserved prefix), so it
|
|
60
|
+
// must work from any project, even one without an own API (e.g. the CLI
|
|
61
|
+
// repo itself). mongosh against the local default URI needs no project
|
|
62
|
+
// driver; the api dir merely improves the driver fallback when present.
|
|
63
|
+
const observed = (0, dev_ticket_1.listDatabaseNames)(undefined, [mainLayout.apiDir, mainRepoRoot].filter(Boolean));
|
|
64
|
+
if (observed === null) {
|
|
61
65
|
warning('Could not list databases (mongosh missing or MongoDB unreachable) — only the registry is pruned.');
|
|
62
66
|
}
|
|
63
67
|
const plan = (0, dev_prune_1.collectDevPrunePlan)({
|
package/build/commands/dev/up.js
CHANGED
|
@@ -454,7 +454,9 @@ const UpCommand = {
|
|
|
454
454
|
const plan = (0, dev_prune_1.collectDevPrunePlan)({
|
|
455
455
|
loadRegistry: dev_state_1.loadRegistry,
|
|
456
456
|
mainRepoRoot,
|
|
457
|
-
|
|
457
|
+
// Always observe (not only with an own api dir): the smoke-test sweep is
|
|
458
|
+
// global, and mongosh against the local default URI needs no project driver.
|
|
459
|
+
observedDbNames: (0, dev_ticket_1.listDatabaseNames)(undefined, [mainLayout.apiDir, mainRepoRoot].filter(Boolean)),
|
|
458
460
|
projectDevDb: (0, dev_project_1.deriveDbName)(mainLayout.apiDir, baseSlug),
|
|
459
461
|
slug: baseSlug,
|
|
460
462
|
});
|
package/build/lib/claude-cli.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CLAUDE_MARKETPLACES_DIR = void 0;
|
|
3
|
+
exports.CLAUDE_KNOWN_MARKETPLACES_PATH = exports.CLAUDE_MARKETPLACES_DIR = void 0;
|
|
4
4
|
exports.checkCommandExists = checkCommandExists;
|
|
5
5
|
exports.checkMarketplaceExists = checkMarketplaceExists;
|
|
6
6
|
exports.findClaudeCli = findClaudeCli;
|
|
7
|
+
exports.listKnownMarketplaceNames = listKnownMarketplaceNames;
|
|
8
|
+
exports.readKnownMarketplaces = readKnownMarketplaces;
|
|
7
9
|
exports.runClaudeCommand = runClaudeCommand;
|
|
8
10
|
/**
|
|
9
11
|
* Claude CLI utilities
|
|
@@ -17,6 +19,10 @@ const path_1 = require("path");
|
|
|
17
19
|
* Path to Claude plugins marketplaces directory
|
|
18
20
|
*/
|
|
19
21
|
exports.CLAUDE_MARKETPLACES_DIR = (0, path_1.join)((0, os_1.homedir)(), '.claude', 'plugins', 'marketplaces');
|
|
22
|
+
/**
|
|
23
|
+
* Path to the Claude CLI's registry of known marketplaces (name → source).
|
|
24
|
+
*/
|
|
25
|
+
exports.CLAUDE_KNOWN_MARKETPLACES_PATH = (0, path_1.join)((0, os_1.homedir)(), '.claude', 'plugins', 'known_marketplaces.json');
|
|
20
26
|
/**
|
|
21
27
|
* Check if a shell command exists and succeeds
|
|
22
28
|
* @param command - Command to check (e.g., 'which typescript-language-server')
|
|
@@ -64,6 +70,27 @@ function findClaudeCli() {
|
|
|
64
70
|
}
|
|
65
71
|
return null;
|
|
66
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* List the names of all marketplaces known to the Claude CLI.
|
|
75
|
+
* @returns Array of marketplace names
|
|
76
|
+
*/
|
|
77
|
+
function listKnownMarketplaceNames() {
|
|
78
|
+
return Object.keys(readKnownMarketplaces());
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Read the Claude CLI's registry of known marketplaces.
|
|
82
|
+
* Never throws — returns an empty object when the file is missing or invalid.
|
|
83
|
+
* @returns Map of marketplace name to its registry entry
|
|
84
|
+
*/
|
|
85
|
+
function readKnownMarketplaces() {
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(exports.CLAUDE_KNOWN_MARKETPLACES_PATH, 'utf-8'));
|
|
88
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
89
|
+
}
|
|
90
|
+
catch (_a) {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
67
94
|
/**
|
|
68
95
|
* Execute a Claude CLI command
|
|
69
96
|
* @param cli - Path to Claude CLI executable
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getLtConfigDir = getLtConfigDir;
|
|
4
|
+
exports.getMarketplaceConfigPath = getMarketplaceConfigPath;
|
|
5
|
+
exports.readConfiguredMarketplaces = readConfiguredMarketplaces;
|
|
6
|
+
exports.removeConfiguredMarketplace = removeConfiguredMarketplace;
|
|
7
|
+
exports.upsertConfiguredMarketplace = upsertConfiguredMarketplace;
|
|
8
|
+
exports.writeConfiguredMarketplaces = writeConfiguredMarketplaces;
|
|
9
|
+
/**
|
|
10
|
+
* User-level configuration for additional Claude Code plugin marketplaces.
|
|
11
|
+
*
|
|
12
|
+
* Marketplace repositories must NOT be hard-coded in the (public) CLI source.
|
|
13
|
+
* Instead they live in a local, user-owned config file so that private/internal
|
|
14
|
+
* repositories stay confidential and any number of extra marketplaces can be
|
|
15
|
+
* added. The file is read on every `lt claude plugins` run and maintained via
|
|
16
|
+
* `lt claude marketplaces`.
|
|
17
|
+
*/
|
|
18
|
+
const fs_1 = require("fs");
|
|
19
|
+
const os_1 = require("os");
|
|
20
|
+
const path_1 = require("path");
|
|
21
|
+
const json_utils_1 = require("./json-utils");
|
|
22
|
+
/**
|
|
23
|
+
* Directory holding lenne.Tech CLI user configuration (shared with e.g. the
|
|
24
|
+
* `lt dev` Caddyfile). Resolved at call time so tests can redirect it.
|
|
25
|
+
* @returns Absolute path to the lenne.Tech config directory
|
|
26
|
+
*/
|
|
27
|
+
function getLtConfigDir() {
|
|
28
|
+
return process.env.LT_CONFIG_DIR || (0, path_1.join)((0, os_1.homedir)(), '.lenneTech');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Absolute path to the marketplace configuration file. Resolved at call time so
|
|
32
|
+
* tests can redirect it via LT_MARKETPLACE_CONFIG or LT_CONFIG_DIR.
|
|
33
|
+
* @returns Absolute path to claude-marketplaces.json
|
|
34
|
+
*/
|
|
35
|
+
function getMarketplaceConfigPath() {
|
|
36
|
+
return process.env.LT_MARKETPLACE_CONFIG || (0, path_1.join)(getLtConfigDir(), 'claude-marketplaces.json');
|
|
37
|
+
}
|
|
38
|
+
const CONFIG_VERSION = 1;
|
|
39
|
+
/**
|
|
40
|
+
* Read the configured marketplaces from disk.
|
|
41
|
+
* Never throws — returns an empty array when the file is missing or invalid.
|
|
42
|
+
* @returns Array of configured marketplaces (validated, deduplicated by name)
|
|
43
|
+
*/
|
|
44
|
+
function readConfiguredMarketplaces() {
|
|
45
|
+
const configPath = getMarketplaceConfigPath();
|
|
46
|
+
if (!(0, fs_1.existsSync)(configPath)) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
let raw;
|
|
50
|
+
try {
|
|
51
|
+
raw = (0, fs_1.readFileSync)(configPath, 'utf-8');
|
|
52
|
+
}
|
|
53
|
+
catch (_a) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const parsed = (0, json_utils_1.safeJsonParse)(raw);
|
|
57
|
+
if (!parsed || !Array.isArray(parsed.marketplaces)) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
// Keep only structurally valid entries and drop duplicates (first wins).
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
const result = [];
|
|
63
|
+
for (const entry of parsed.marketplaces) {
|
|
64
|
+
if (!entry || typeof entry.name !== 'string' || typeof entry.source !== 'string') {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const name = entry.name.trim();
|
|
68
|
+
const source = entry.source.trim();
|
|
69
|
+
if (!name || !source || seen.has(name)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
seen.add(name);
|
|
73
|
+
result.push(Object.assign(Object.assign({}, entry), { name, source }));
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Remove a marketplace entry by name.
|
|
79
|
+
* @param name - Name of the marketplace to remove
|
|
80
|
+
* @returns Object with the updated list and whether an entry was removed
|
|
81
|
+
*/
|
|
82
|
+
function removeConfiguredMarketplace(name) {
|
|
83
|
+
const target = name.trim();
|
|
84
|
+
const current = readConfiguredMarketplaces();
|
|
85
|
+
const next = current.filter((m) => m.name !== target);
|
|
86
|
+
const removed = next.length !== current.length;
|
|
87
|
+
if (removed) {
|
|
88
|
+
writeConfiguredMarketplaces(next);
|
|
89
|
+
}
|
|
90
|
+
return { marketplaces: next, removed };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Add or update a marketplace entry (matched by name). Returns the resulting
|
|
94
|
+
* list. Existing entries with the same name are replaced.
|
|
95
|
+
* @param entry - Marketplace to add or update
|
|
96
|
+
* @returns The updated list of configured marketplaces
|
|
97
|
+
*/
|
|
98
|
+
function upsertConfiguredMarketplace(entry) {
|
|
99
|
+
const name = entry.name.trim();
|
|
100
|
+
const source = entry.source.trim();
|
|
101
|
+
const normalized = Object.assign(Object.assign({}, entry), { name, source });
|
|
102
|
+
const current = readConfiguredMarketplaces().filter((m) => m.name !== name);
|
|
103
|
+
current.push(normalized);
|
|
104
|
+
writeConfiguredMarketplaces(current);
|
|
105
|
+
return current;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Persist the given marketplaces to disk, creating the config directory if
|
|
109
|
+
* needed.
|
|
110
|
+
* @param marketplaces - Marketplaces to write
|
|
111
|
+
*/
|
|
112
|
+
function writeConfiguredMarketplaces(marketplaces) {
|
|
113
|
+
const configPath = getMarketplaceConfigPath();
|
|
114
|
+
const dir = (0, path_1.dirname)(configPath);
|
|
115
|
+
if (!(0, fs_1.existsSync)(dir)) {
|
|
116
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
const payload = { marketplaces, version: CONFIG_VERSION };
|
|
119
|
+
(0, fs_1.writeFileSync)(configPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
|
|
120
|
+
}
|
package/build/lib/marketplace.js
CHANGED
|
@@ -9,32 +9,54 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.DEFAULT_EXTERNAL_PLUGINS = exports.
|
|
12
|
+
exports.DEFAULT_EXTERNAL_PLUGINS = exports.PRIMARY_MARKETPLACE_NAME = exports.BUILTIN_MARKETPLACES = void 0;
|
|
13
|
+
exports.configuredToMarketplaceConfig = configuredToMarketplaceConfig;
|
|
14
|
+
exports.deriveGitHubRepo = deriveGitHubRepo;
|
|
13
15
|
exports.fetchAvailablePlugins = fetchAvailablePlugins;
|
|
14
16
|
exports.fetchPluginsFromMarketplace = fetchPluginsFromMarketplace;
|
|
17
|
+
exports.getAllMarketplaces = getAllMarketplaces;
|
|
15
18
|
exports.printAvailablePlugins = printAvailablePlugins;
|
|
16
19
|
/**
|
|
17
20
|
* Plugin marketplace utilities
|
|
18
|
-
* Handles
|
|
21
|
+
* Handles discovering and managing plugins from configurable marketplaces.
|
|
22
|
+
*
|
|
23
|
+
* Built-in marketplaces (public) are discovered via the GitHub REST API.
|
|
24
|
+
* Additional marketplaces — including private/internal ones on GitLab, GitHub or
|
|
25
|
+
* any other Git host — are NOT hard-coded here (that would expose internal repo
|
|
26
|
+
* URLs in this public package). They are loaded from the user's local config
|
|
27
|
+
* (see marketplace-config.ts) and discovered by cloning them via the Claude CLI
|
|
28
|
+
* and reading the manifest from the local checkout.
|
|
19
29
|
*/
|
|
30
|
+
const child_process_1 = require("child_process");
|
|
31
|
+
const fs_1 = require("fs");
|
|
32
|
+
const path_1 = require("path");
|
|
33
|
+
const claude_cli_1 = require("./claude-cli");
|
|
20
34
|
const json_utils_1 = require("./json-utils");
|
|
35
|
+
const marketplace_config_1 = require("./marketplace-config");
|
|
21
36
|
/**
|
|
22
|
-
*
|
|
37
|
+
* Built-in, public marketplaces that always ship with the CLI.
|
|
38
|
+
* Private/internal marketplaces are configured locally, never listed here.
|
|
23
39
|
*/
|
|
24
|
-
exports.
|
|
40
|
+
exports.BUILTIN_MARKETPLACES = [
|
|
25
41
|
{
|
|
26
42
|
apiBase: 'https://api.github.com/repos/lenneTech/claude-code/contents',
|
|
27
43
|
name: 'lenne-tech',
|
|
44
|
+
provider: 'github',
|
|
28
45
|
rawBase: 'https://raw.githubusercontent.com/lenneTech/claude-code/main',
|
|
29
46
|
repo: 'lenneTech/claude-code',
|
|
30
47
|
},
|
|
31
48
|
{
|
|
32
49
|
apiBase: 'https://api.github.com/repos/anthropics/claude-plugins-official/contents',
|
|
33
50
|
name: 'claude-plugins-official',
|
|
51
|
+
provider: 'github',
|
|
34
52
|
rawBase: 'https://raw.githubusercontent.com/anthropics/claude-plugins-official/main',
|
|
35
53
|
repo: 'anthropics/claude-plugins-official',
|
|
36
54
|
},
|
|
37
55
|
];
|
|
56
|
+
/**
|
|
57
|
+
* Name of the primary marketplace whose plugins are installed by default.
|
|
58
|
+
*/
|
|
59
|
+
exports.PRIMARY_MARKETPLACE_NAME = exports.BUILTIN_MARKETPLACES[0].name;
|
|
38
60
|
/**
|
|
39
61
|
* Default plugins to install when no specific plugins are requested
|
|
40
62
|
* These are installed in addition to all plugins from the primary marketplace (lenne-tech)
|
|
@@ -43,17 +65,68 @@ exports.DEFAULT_EXTERNAL_PLUGINS = [
|
|
|
43
65
|
{ marketplaceName: 'claude-plugins-official', pluginName: 'typescript-lsp' },
|
|
44
66
|
];
|
|
45
67
|
/**
|
|
46
|
-
*
|
|
68
|
+
* Map a locally configured marketplace to a MarketplaceConfig used for discovery.
|
|
69
|
+
* Defaults to the provider-agnostic `git` strategy; a `github` provider derives
|
|
70
|
+
* API/raw bases from the source so it can use the fast REST discovery path.
|
|
71
|
+
* @param entry - Configured marketplace from the user config
|
|
72
|
+
* @returns Discovery configuration
|
|
73
|
+
*/
|
|
74
|
+
function configuredToMarketplaceConfig(entry) {
|
|
75
|
+
var _a, _b;
|
|
76
|
+
const provider = (_a = entry.provider) !== null && _a !== void 0 ? _a : 'git';
|
|
77
|
+
const base = {
|
|
78
|
+
autoInstall: (_b = entry.autoInstall) !== null && _b !== void 0 ? _b : true,
|
|
79
|
+
branch: entry.branch,
|
|
80
|
+
name: entry.name,
|
|
81
|
+
private: entry.private,
|
|
82
|
+
provider,
|
|
83
|
+
source: entry.source,
|
|
84
|
+
};
|
|
85
|
+
if (provider === 'github') {
|
|
86
|
+
const repo = deriveGitHubRepo(entry.source);
|
|
87
|
+
if (repo) {
|
|
88
|
+
const branch = entry.branch || 'main';
|
|
89
|
+
base.apiBase = `https://api.github.com/repos/${repo}/contents`;
|
|
90
|
+
base.rawBase = `https://raw.githubusercontent.com/${repo}/${branch}`;
|
|
91
|
+
base.repo = repo;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return base;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Derive an 'owner/repo' identifier from a marketplace source (github provider).
|
|
98
|
+
* Accepts 'owner/repo', an https GitHub URL or an SSH GitHub URL.
|
|
99
|
+
* @param source - Marketplace source string
|
|
100
|
+
* @returns 'owner/repo' or undefined when it is not a recognizable GitHub source
|
|
101
|
+
*/
|
|
102
|
+
function deriveGitHubRepo(source) {
|
|
103
|
+
const trimmed = source.trim().replace(/\.git$/, '');
|
|
104
|
+
// https://github.com/owner/repo or git@github.com:owner/repo (check first, so
|
|
105
|
+
// a host prefix like git@gitlab.example.com:… is not mistaken for owner/repo).
|
|
106
|
+
const match = trimmed.match(/github\.com[:/]([^/\s]+\/[^/\s]+)$/);
|
|
107
|
+
if (match) {
|
|
108
|
+
return match[1];
|
|
109
|
+
}
|
|
110
|
+
// Plain owner/repo — reject anything carrying URL/host syntax (@, :, /host/…).
|
|
111
|
+
if (/^[\w.-]+\/[\w.-]+$/.test(trimmed)) {
|
|
112
|
+
return trimmed;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Fetch available plugins from all marketplaces (built-in + configured)
|
|
47
118
|
* @param spin - Spinner factory function from toolbox
|
|
119
|
+
* @param cli - Path to the Claude CLI (required to discover `git` marketplaces)
|
|
48
120
|
* @returns Array of plugin configurations
|
|
49
121
|
* @throws Error if no plugins are found
|
|
50
122
|
*/
|
|
51
|
-
function fetchAvailablePlugins(spin) {
|
|
123
|
+
function fetchAvailablePlugins(spin, cli) {
|
|
52
124
|
return __awaiter(this, void 0, void 0, function* () {
|
|
53
125
|
const spinner = spin('Fetching available plugins from marketplaces');
|
|
126
|
+
const marketplaces = getAllMarketplaces();
|
|
54
127
|
try {
|
|
55
128
|
// Fetch plugins from all marketplaces in parallel
|
|
56
|
-
const results = yield Promise.all(
|
|
129
|
+
const results = yield Promise.all(marketplaces.map((marketplace) => fetchPluginsFromMarketplace(marketplace, cli)));
|
|
57
130
|
// Flatten results
|
|
58
131
|
const plugins = results.flat();
|
|
59
132
|
if (plugins.length === 0) {
|
|
@@ -61,10 +134,12 @@ function fetchAvailablePlugins(spin) {
|
|
|
61
134
|
throw new Error('No plugins found');
|
|
62
135
|
}
|
|
63
136
|
// Group by marketplace for display
|
|
64
|
-
const byMarketplace =
|
|
137
|
+
const byMarketplace = marketplaces
|
|
138
|
+
.map((m) => ({
|
|
65
139
|
count: plugins.filter((p) => p.marketplaceName === m.name).length,
|
|
66
140
|
name: m.name,
|
|
67
|
-
}))
|
|
141
|
+
}))
|
|
142
|
+
.filter((m) => m.count > 0);
|
|
68
143
|
const summary = byMarketplace.map((m) => `${m.name}: ${m.count}`).join(', ');
|
|
69
144
|
spinner.succeed(`Found ${plugins.length} plugins (${summary})`);
|
|
70
145
|
return plugins;
|
|
@@ -76,18 +151,71 @@ function fetchAvailablePlugins(spin) {
|
|
|
76
151
|
});
|
|
77
152
|
}
|
|
78
153
|
/**
|
|
79
|
-
* Fetch available plugins from a single marketplace
|
|
80
|
-
* First tries central marketplace.json, then falls back to directory scan
|
|
154
|
+
* Fetch available plugins from a single marketplace, dispatching on provider.
|
|
81
155
|
* @param marketplace - Marketplace configuration
|
|
156
|
+
* @param cli - Path to the Claude CLI (required for the `git` provider)
|
|
157
|
+
* @returns Array of plugin configurations
|
|
158
|
+
*/
|
|
159
|
+
function fetchPluginsFromMarketplace(marketplace, cli) {
|
|
160
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
161
|
+
if (marketplace.provider === 'git') {
|
|
162
|
+
return fetchPluginsFromGitMarketplace(marketplace, cli);
|
|
163
|
+
}
|
|
164
|
+
return fetchPluginsFromGitHubMarketplace(marketplace);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Return all marketplaces: built-in public ones plus any configured locally.
|
|
169
|
+
* @returns Combined list of marketplace configurations
|
|
170
|
+
*/
|
|
171
|
+
function getAllMarketplaces() {
|
|
172
|
+
const configured = (0, marketplace_config_1.readConfiguredMarketplaces)().map(configuredToMarketplaceConfig);
|
|
173
|
+
// Built-in names take precedence; drop configured entries that collide.
|
|
174
|
+
const builtinNames = new Set(exports.BUILTIN_MARKETPLACES.map((m) => m.name));
|
|
175
|
+
const extra = configured.filter((m) => !builtinNames.has(m.name));
|
|
176
|
+
return [...exports.BUILTIN_MARKETPLACES, ...extra];
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Print available plugins list
|
|
180
|
+
* @param plugins - Array of plugin configurations
|
|
181
|
+
* @param info - Info print function from toolbox
|
|
182
|
+
*/
|
|
183
|
+
function printAvailablePlugins(plugins, info) {
|
|
184
|
+
info('Available plugins:');
|
|
185
|
+
for (const plugin of plugins) {
|
|
186
|
+
info(` ${plugin.pluginName} - ${plugin.description}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Discover plugins from a `github`-provider marketplace via the GitHub REST API.
|
|
191
|
+
* First tries the central marketplace.json, then falls back to a directory scan.
|
|
192
|
+
* @param marketplace - Marketplace configuration (must have apiBase/rawBase)
|
|
82
193
|
* @returns Array of plugin configurations
|
|
83
194
|
*/
|
|
84
|
-
function
|
|
195
|
+
function fetchPluginsFromGitHubMarketplace(marketplace) {
|
|
85
196
|
return __awaiter(this, void 0, void 0, function* () {
|
|
86
197
|
const plugins = [];
|
|
198
|
+
if (!marketplace.apiBase || !marketplace.rawBase) {
|
|
199
|
+
return plugins;
|
|
200
|
+
}
|
|
201
|
+
const repo = marketplace.repo || marketplace.source || marketplace.name;
|
|
202
|
+
// Private marketplaces need a GitHub token. No token available → silently skip
|
|
203
|
+
// so discovery never fails for users without access.
|
|
204
|
+
const token = marketplace.private ? getGitHubToken() : undefined;
|
|
205
|
+
if (marketplace.private && !token) {
|
|
206
|
+
return plugins;
|
|
207
|
+
}
|
|
208
|
+
const authHeaders = token ? { Authorization: `Bearer ${token}` } : {};
|
|
87
209
|
try {
|
|
88
|
-
// First try to read central marketplace.json
|
|
89
|
-
|
|
90
|
-
|
|
210
|
+
// First try to read central marketplace.json. Private repos: via the API
|
|
211
|
+
// contents endpoint with the raw media type (raw.githubusercontent.com does
|
|
212
|
+
// not serve private content).
|
|
213
|
+
const marketplaceJsonUrl = marketplace.private
|
|
214
|
+
? `${marketplace.apiBase}/.claude-plugin/marketplace.json`
|
|
215
|
+
: `${marketplace.rawBase}/.claude-plugin/marketplace.json`;
|
|
216
|
+
const marketplaceJsonResponse = yield fetch(marketplaceJsonUrl, {
|
|
217
|
+
headers: marketplace.private ? Object.assign(Object.assign({}, authHeaders), { Accept: 'application/vnd.github.raw+json' }) : authHeaders,
|
|
218
|
+
});
|
|
91
219
|
if (marketplaceJsonResponse.ok) {
|
|
92
220
|
const text = yield marketplaceJsonResponse.text();
|
|
93
221
|
const marketplaceManifest = (0, json_utils_1.safeJsonParse)(text);
|
|
@@ -96,7 +224,7 @@ function fetchPluginsFromMarketplace(marketplace) {
|
|
|
96
224
|
plugins.push({
|
|
97
225
|
description: plugin.description || '',
|
|
98
226
|
marketplaceName: marketplace.name,
|
|
99
|
-
marketplaceRepo:
|
|
227
|
+
marketplaceRepo: repo,
|
|
100
228
|
pluginName: plugin.name,
|
|
101
229
|
});
|
|
102
230
|
}
|
|
@@ -104,7 +232,7 @@ function fetchPluginsFromMarketplace(marketplace) {
|
|
|
104
232
|
}
|
|
105
233
|
}
|
|
106
234
|
// Fallback: Get list of plugin directories and read individual plugin.json files
|
|
107
|
-
const dirResponse = yield fetch(`${marketplace.apiBase}/plugins
|
|
235
|
+
const dirResponse = yield fetch(`${marketplace.apiBase}/plugins`, { headers: authHeaders });
|
|
108
236
|
if (!dirResponse.ok) {
|
|
109
237
|
return plugins;
|
|
110
238
|
}
|
|
@@ -117,8 +245,12 @@ function fetchPluginsFromMarketplace(marketplace) {
|
|
|
117
245
|
// Fetch plugin.json for each plugin in parallel
|
|
118
246
|
const manifestPromises = pluginDirs.map((dir) => __awaiter(this, void 0, void 0, function* () {
|
|
119
247
|
try {
|
|
120
|
-
const manifestUrl =
|
|
121
|
-
|
|
248
|
+
const manifestUrl = marketplace.private
|
|
249
|
+
? `${marketplace.apiBase}/plugins/${dir.name}/.claude-plugin/plugin.json`
|
|
250
|
+
: `${marketplace.rawBase}/plugins/${dir.name}/.claude-plugin/plugin.json`;
|
|
251
|
+
const manifestResponse = yield fetch(manifestUrl, {
|
|
252
|
+
headers: marketplace.private ? Object.assign(Object.assign({}, authHeaders), { Accept: 'application/vnd.github.raw+json' }) : authHeaders,
|
|
253
|
+
});
|
|
122
254
|
if (manifestResponse.ok) {
|
|
123
255
|
const manifestText = yield manifestResponse.text();
|
|
124
256
|
const manifest = (0, json_utils_1.safeJsonParse)(manifestText);
|
|
@@ -126,7 +258,7 @@ function fetchPluginsFromMarketplace(marketplace) {
|
|
|
126
258
|
return {
|
|
127
259
|
description: manifest.description,
|
|
128
260
|
marketplaceName: marketplace.name,
|
|
129
|
-
marketplaceRepo:
|
|
261
|
+
marketplaceRepo: repo,
|
|
130
262
|
pluginName: manifest.name,
|
|
131
263
|
};
|
|
132
264
|
}
|
|
@@ -147,13 +279,79 @@ function fetchPluginsFromMarketplace(marketplace) {
|
|
|
147
279
|
});
|
|
148
280
|
}
|
|
149
281
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
282
|
+
* Discover plugins from a `git`-provider marketplace: ensure the marketplace is
|
|
283
|
+
* present locally (clone via the Claude CLI), then read its manifest from the
|
|
284
|
+
* local checkout. Requires no API token — access is governed by the user's Git
|
|
285
|
+
* permissions, so unauthorized users are skipped silently.
|
|
286
|
+
* @param marketplace - Marketplace configuration (must have a source)
|
|
287
|
+
* @param cli - Path to the Claude CLI
|
|
288
|
+
* @returns Array of plugin configurations (empty when inaccessible)
|
|
153
289
|
*/
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
290
|
+
function fetchPluginsFromGitMarketplace(marketplace, cli) {
|
|
291
|
+
const plugins = [];
|
|
292
|
+
const source = marketplace.source || marketplace.repo;
|
|
293
|
+
if (!cli || !source) {
|
|
294
|
+
return plugins;
|
|
295
|
+
}
|
|
296
|
+
// Ensure the marketplace exists locally. Add it if missing; a failed add
|
|
297
|
+
// (e.g. no repo access) is treated as "skip" so discovery never hard-fails.
|
|
298
|
+
if (!(0, claude_cli_1.checkMarketplaceExists)(marketplace.name)) {
|
|
299
|
+
const addResult = (0, claude_cli_1.runClaudeCommand)(cli, `plugin marketplace add ${source}`);
|
|
300
|
+
if (!addResult.success && !addResult.output.includes('already') && !(0, claude_cli_1.checkMarketplaceExists)(marketplace.name)) {
|
|
301
|
+
return plugins;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
// Refresh the local cache to pick up newly published plugins (best effort).
|
|
306
|
+
(0, claude_cli_1.runClaudeCommand)(cli, `plugin marketplace update ${marketplace.name}`);
|
|
307
|
+
}
|
|
308
|
+
// Read the central manifest from the local checkout.
|
|
309
|
+
const manifestPath = (0, path_1.join)(claude_cli_1.CLAUDE_MARKETPLACES_DIR, marketplace.name, '.claude-plugin', 'marketplace.json');
|
|
310
|
+
if (!(0, fs_1.existsSync)(manifestPath)) {
|
|
311
|
+
return plugins;
|
|
312
|
+
}
|
|
313
|
+
let manifestText;
|
|
314
|
+
try {
|
|
315
|
+
manifestText = (0, fs_1.readFileSync)(manifestPath, 'utf-8');
|
|
316
|
+
}
|
|
317
|
+
catch (_a) {
|
|
318
|
+
return plugins;
|
|
319
|
+
}
|
|
320
|
+
const manifest = (0, json_utils_1.safeJsonParse)(manifestText);
|
|
321
|
+
if (!(manifest === null || manifest === void 0 ? void 0 : manifest.plugins)) {
|
|
322
|
+
return plugins;
|
|
323
|
+
}
|
|
324
|
+
for (const plugin of manifest.plugins) {
|
|
325
|
+
if (!(plugin === null || plugin === void 0 ? void 0 : plugin.name)) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
plugins.push({
|
|
329
|
+
description: plugin.description || '',
|
|
330
|
+
marketplaceName: marketplace.name,
|
|
331
|
+
marketplaceRepo: source,
|
|
332
|
+
pluginName: plugin.name,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return plugins;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Resolve a GitHub token for authenticated access to private marketplaces.
|
|
339
|
+
* Tries GH_TOKEN / GITHUB_TOKEN, then the gh CLI. Returns undefined when none is
|
|
340
|
+
* available — callers then simply skip private marketplaces (commands never fail).
|
|
341
|
+
* @returns A GitHub token or undefined
|
|
342
|
+
*/
|
|
343
|
+
function getGitHubToken() {
|
|
344
|
+
const fromEnv = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
345
|
+
if (fromEnv) {
|
|
346
|
+
return fromEnv.trim();
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const fromGh = (0, child_process_1.execSync)('gh auth token', { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
350
|
+
.toString()
|
|
351
|
+
.trim();
|
|
352
|
+
return fromGh || undefined;
|
|
353
|
+
}
|
|
354
|
+
catch (_a) {
|
|
355
|
+
return undefined;
|
|
158
356
|
}
|
|
159
357
|
}
|
package/docs/commands.md
CHANGED
|
@@ -1812,9 +1812,10 @@ lt claude plugins [plugin-name] [options]
|
|
|
1812
1812
|
**Plugin Sources:**
|
|
1813
1813
|
- [lenne-tech marketplace](https://github.com/lenneTech/claude-code) - lenne.Tech plugins for NestJS development
|
|
1814
1814
|
- [claude-plugins-official](https://github.com/anthropics/claude-plugins-official) - Official Anthropic plugins (e.g., typescript-lsp)
|
|
1815
|
+
- Additional marketplaces you configure via [`lt claude marketplaces`](#lt-claude-marketplaces) — including private/internal repositories on GitLab, GitHub or any Git host. These are read on every run and their plugins auto-installed when accessible.
|
|
1815
1816
|
|
|
1816
1817
|
**Default Behavior:**
|
|
1817
|
-
When run without a plugin name, all lenne.Tech plugins plus recommended external plugins (like `typescript-lsp`) are installed automatically.
|
|
1818
|
+
When run without a plugin name, all lenne.Tech plugins plus recommended external plugins (like `typescript-lsp`) are installed automatically. Plugins from configured `autoInstall` marketplaces are added as well, but only for users who can access the underlying repository — everyone else silently skips them.
|
|
1818
1819
|
|
|
1819
1820
|
**Examples:**
|
|
1820
1821
|
```bash
|
|
@@ -1833,6 +1834,47 @@ lt claude plugins lt-dev --uninstall
|
|
|
1833
1834
|
|
|
1834
1835
|
---
|
|
1835
1836
|
|
|
1837
|
+
### `lt claude marketplaces`
|
|
1838
|
+
|
|
1839
|
+
Manages **additional** plugin marketplaces used by `lt claude plugins`. Marketplace repositories are **not** hard-coded in the CLI — they live in a local, user-owned config file (`~/.lenneTech/claude-marketplaces.json`), so private/internal repositories stay confidential and any number of extra marketplaces can be added. The config is read on every `lt claude plugins` run.
|
|
1840
|
+
|
|
1841
|
+
**Usage:**
|
|
1842
|
+
```bash
|
|
1843
|
+
lt claude marketplaces [action] [source|name] [options]
|
|
1844
|
+
```
|
|
1845
|
+
|
|
1846
|
+
**Alias:** `lt claude mp`
|
|
1847
|
+
|
|
1848
|
+
**Actions:**
|
|
1849
|
+
| Action | Description |
|
|
1850
|
+
|--------|-------------|
|
|
1851
|
+
| _(none)_ | Interactive menu (list / add / remove) |
|
|
1852
|
+
| `list` | Show the configured marketplaces |
|
|
1853
|
+
| `add [source]` | Add a marketplace (Git URL or `owner/repo`); prompts for the source if omitted |
|
|
1854
|
+
| `remove [name]` | Remove a configured marketplace; prompts for the name if omitted |
|
|
1855
|
+
|
|
1856
|
+
**How it works:**
|
|
1857
|
+
- Adding a marketplace runs `claude plugin marketplace add <source>`, which clones the repository via your existing Git access (SSH or HTTPS). The real marketplace name is read back from Claude's registry, so it always lines up with the plugin install.
|
|
1858
|
+
- Discovery uses the provider-agnostic `git` strategy by default: the marketplace manifest is read from the local checkout — **no API token required**.
|
|
1859
|
+
- **Access control is governed solely by your Git permissions.** Users without access to a private repository are skipped silently; the command never fails for them.
|
|
1860
|
+
|
|
1861
|
+
**Examples:**
|
|
1862
|
+
```bash
|
|
1863
|
+
# Interactive management
|
|
1864
|
+
lt claude marketplaces
|
|
1865
|
+
|
|
1866
|
+
# Add a marketplace (any Git host)
|
|
1867
|
+
lt claude marketplaces add git@gitlab.example.com:group/my-marketplace.git
|
|
1868
|
+
|
|
1869
|
+
# List configured marketplaces
|
|
1870
|
+
lt claude marketplaces list
|
|
1871
|
+
|
|
1872
|
+
# Remove one
|
|
1873
|
+
lt claude marketplaces remove my-marketplace
|
|
1874
|
+
```
|
|
1875
|
+
|
|
1876
|
+
---
|
|
1877
|
+
|
|
1836
1878
|
### `lt claude shortcuts`
|
|
1837
1879
|
|
|
1838
1880
|
Installs Claude Code shell shortcuts (aliases) for quick access to common commands.
|