@lenne.tech/cli 1.38.1 → 1.40.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/up.js +51 -7
- package/build/lib/claude-cli.js +28 -1
- package/build/lib/dev-api-launch.js +89 -0
- package/build/lib/dev-test-session.js +2 -3
- package/build/lib/marketplace-config.js +120 -0
- package/build/lib/marketplace.js +225 -27
- package/docs/commands.md +54 -3
- 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
|
}
|
package/build/commands/dev/up.js
CHANGED
|
@@ -9,9 +9,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.help = void 0;
|
|
12
13
|
const fs_1 = require("fs");
|
|
13
14
|
const path_1 = require("path");
|
|
14
15
|
const caddy_1 = require("../../lib/caddy");
|
|
16
|
+
const dev_api_launch_1 = require("../../lib/dev-api-launch");
|
|
15
17
|
const dev_env_1 = require("../../lib/dev-env");
|
|
16
18
|
const dev_env_bridge_1 = require("../../lib/dev-env-bridge");
|
|
17
19
|
const dev_identity_1 = require("../../lib/dev-identity");
|
|
@@ -112,6 +114,10 @@ const UpCommand = {
|
|
|
112
114
|
// suffixed so the stack is fully isolated from the base dev session and every
|
|
113
115
|
// other ticket. Without a ticket this is the plain project identity.
|
|
114
116
|
const { dbName, identity, ticket } = (0, dev_ticket_1.resolveDevIdentity)(layout, { ticket: parameters.options.ticket });
|
|
117
|
+
// `--api-compiled`: run the API compiled (`node dist`) instead of ts-node.
|
|
118
|
+
// Trades hot reload for stability — ts-node intermittently dies under browser
|
|
119
|
+
// load without a stacktrace (DEV-2525); `lt dev test` already runs compiled.
|
|
120
|
+
const apiCompiled = (0, dev_api_launch_1.isApiCompiledRequested)(parameters.options);
|
|
115
121
|
// Guard against two checkouts of the SAME project (same package.json "name"
|
|
116
122
|
// → same slug → shared URLs / ports / DB / Caddy block). If another checkout
|
|
117
123
|
// is already RUNNING under this slug, abort with a clear message — otherwise
|
|
@@ -321,7 +327,7 @@ const UpCommand = {
|
|
|
321
327
|
if (identity.subdomains.app)
|
|
322
328
|
info(` app: https://${identity.subdomains.app.hostname} → 127.0.0.1:${appPort}`);
|
|
323
329
|
if (identity.subdomains.api)
|
|
324
|
-
info(` api: https://${identity.subdomains.api.hostname} → 127.0.0.1:${apiPort}`);
|
|
330
|
+
info(` api: https://${identity.subdomains.api.hostname} → 127.0.0.1:${apiPort}${apiCompiled ? colors.dim(' (compiled, no hot reload)') : ''}`);
|
|
325
331
|
if (identity.subdomains.api)
|
|
326
332
|
info(` db: mongodb://127.0.0.1/${dbName}`);
|
|
327
333
|
info('');
|
|
@@ -353,6 +359,11 @@ const UpCommand = {
|
|
|
353
359
|
if (apiHealth === 'running') {
|
|
354
360
|
pids.api = existingSession === null || existingSession === void 0 ? void 0 : existingSession.pids.api;
|
|
355
361
|
kept.push('api');
|
|
362
|
+
// `--api-compiled` only takes effect when the API (re)starts; a healthy API is
|
|
363
|
+
// kept as-is (force-restarting it would contradict the keep logic), so tell the
|
|
364
|
+
// user how to switch a currently-running ts-node API to compiled.
|
|
365
|
+
if (apiCompiled)
|
|
366
|
+
info(colors.dim(' --api-compiled: API already running — `lt dev down` first to switch it.'));
|
|
356
367
|
}
|
|
357
368
|
else {
|
|
358
369
|
yield reclaimPort(existingSession === null || existingSession === void 0 ? void 0 : existingSession.pids.api, apiPort, apiHealth !== null && apiHealth !== void 0 ? apiHealth : 'dead');
|
|
@@ -361,11 +372,20 @@ const UpCommand = {
|
|
|
361
372
|
// regenerate a foreign lockfile + crash on un-approved build
|
|
362
373
|
// scripts when run against an npm-only project.
|
|
363
374
|
const apiPm = (0, dev_package_manager_1.pickPackageManager)(layout.apiDir);
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
375
|
+
const apiLogFile = (0, path_1.join)(layout.root, '.lt-dev', 'api.log');
|
|
376
|
+
const apiResult = apiCompiled
|
|
377
|
+
? yield (0, dev_api_launch_1.startCompiledApi)({
|
|
378
|
+
apiDir: layout.apiDir,
|
|
379
|
+
env: devEnv.api.env,
|
|
380
|
+
log: { info, warn: warning },
|
|
381
|
+
logFile: apiLogFile,
|
|
382
|
+
pm: apiPm,
|
|
383
|
+
})
|
|
384
|
+
: (0, dev_process_1.spawnDetached)(apiPm.bin, apiPm.runScript('start'), {
|
|
385
|
+
cwd: layout.apiDir,
|
|
386
|
+
env: devEnv.api.env,
|
|
387
|
+
logFile: apiLogFile,
|
|
388
|
+
});
|
|
369
389
|
if (apiResult) {
|
|
370
390
|
pids.api = apiResult.pid;
|
|
371
391
|
started.push('api');
|
|
@@ -502,4 +522,28 @@ const UpCommand = {
|
|
|
502
522
|
return `dev up: api=${pids.api}, app=${pids.app}`;
|
|
503
523
|
}),
|
|
504
524
|
};
|
|
505
|
-
|
|
525
|
+
exports.help = {
|
|
526
|
+
aliases: ['u'],
|
|
527
|
+
configuration: 'Ephemeral dev-orchestration flags — no lt.config counterpart. Override the spawn binary for both processes via the LT_PNPM_BIN env var.',
|
|
528
|
+
description: 'Start the API + App behind Caddy under stable https://<slug>.localhost URLs. Health-aware and idempotent: re-running (re)starts only the component(s) that are not truly serving.',
|
|
529
|
+
examples: ['dev up', 'dev up --api-compiled', 'dev up --ticket DEV-1234'],
|
|
530
|
+
features: [
|
|
531
|
+
'Registers a Caddy block and allocates opaque internal ports (4000+).',
|
|
532
|
+
'Spawns API + App detached; persists PIDs to <root>/.lt-dev/state.json.',
|
|
533
|
+
'Self-heals legacy hardcoded ports and restarts only the down component(s).',
|
|
534
|
+
],
|
|
535
|
+
name: 'up',
|
|
536
|
+
options: [
|
|
537
|
+
{
|
|
538
|
+
description: 'Run the API compiled (node dist/src/main.js) instead of ts-node — trades hot reload for stability under browser load (DEV-2525). Builds + migrates first; falls back to the ts-node start if the build fails. Only applies when the API (re)starts.',
|
|
539
|
+
flag: '--api-compiled',
|
|
540
|
+
type: 'boolean',
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
description: 'Suffix the slug / URLs / DB for an isolated ticket stack (also auto-detected from a .lt-dev/ticket marker).',
|
|
544
|
+
flag: '--ticket',
|
|
545
|
+
type: 'string',
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
};
|
|
549
|
+
module.exports = Object.assign(UpCommand, { help: exports.help });
|
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,89 @@
|
|
|
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
|
+
exports.findCompiledEntry = findCompiledEntry;
|
|
13
|
+
exports.isApiCompiledRequested = isApiCompiledRequested;
|
|
14
|
+
exports.startCompiledApi = startCompiledApi;
|
|
15
|
+
/**
|
|
16
|
+
* Launch strategy for the API process under `lt dev up`.
|
|
17
|
+
*
|
|
18
|
+
* By default `lt dev up` runs the API via ts-node (`<pm> run start` → nodemon →
|
|
19
|
+
* ts-node src/main.ts) for hot reload. Under a browser driving the app, that
|
|
20
|
+
* ts-node process intermittently dies WITHOUT a stacktrace (dev-SSR load plus
|
|
21
|
+
* ts-node's heavier footprint) — see DEV-2525. `lt dev test` already sidesteps
|
|
22
|
+
* this by running the API COMPILED (`node dist/src/main.js`); this module brings
|
|
23
|
+
* the same option to `lt dev up`, opt-in via `--api-compiled`. The trade-off is
|
|
24
|
+
* NO hot reload, so it stays opt-in — the caller decides stability vs. reload.
|
|
25
|
+
*/
|
|
26
|
+
const node_fs_1 = require("node:fs");
|
|
27
|
+
const node_path_1 = require("node:path");
|
|
28
|
+
const dev_process_1 = require("./dev-process");
|
|
29
|
+
/** Candidate compiled entry points, in preference order. Single-sourced so `lt dev test` agrees. */
|
|
30
|
+
const COMPILED_ENTRIES = ['dist/src/main.js', 'dist/main.js'];
|
|
31
|
+
/** Resolve the compiled API entry point in `apiDir`, or `undefined` if none was built. */
|
|
32
|
+
function findCompiledEntry(apiDir) {
|
|
33
|
+
return COMPILED_ENTRIES.map((rel) => (0, node_path_1.join)(apiDir, rel)).find((candidate) => (0, node_fs_1.existsSync)(candidate));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* True when the caller opted into the compiled API via `--api-compiled`.
|
|
37
|
+
*
|
|
38
|
+
* gluegun parses argv with yargs-parser and declares no booleans, so the flag
|
|
39
|
+
* arrives in several shapes: a value-less `--api-compiled` → boolean `true`, but
|
|
40
|
+
* `--api-compiled=true` → the STRING `'true'` and `--api-compiled=1` → the NUMBER
|
|
41
|
+
* `1`. A bare `=== true` check silently ignores the latter two and drops the very
|
|
42
|
+
* stability fix the user asked for. This is an ENABLE flag, so a mis-parse fails
|
|
43
|
+
* SAFE (default ts-node) — but the repo convention is to honour `true`/`'true'`
|
|
44
|
+
* too (see `dev-ticket.ts#keepDbFlag` for the destructive-flag counterpart).
|
|
45
|
+
*/
|
|
46
|
+
function isApiCompiledRequested(options = {}) {
|
|
47
|
+
const affirmative = (value) => value === true || ['1', 'true', 'yes'].includes(String(value).toLowerCase());
|
|
48
|
+
return affirmative(options.apiCompiled) || affirmative(options['api-compiled']);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build the API and start it compiled (`node dist/src/main.js`). Applies pending
|
|
52
|
+
* migrations first for parity with the ts-node path it replaces (`<pm> run start`
|
|
53
|
+
* = `migrate:up && start:local`). Falls back to the ts-node start when the build
|
|
54
|
+
* fails or produces no dist entry, so this never leaves the developer with a dead
|
|
55
|
+
* API. Returns the detached spawn result (`undefined` when nothing was started).
|
|
56
|
+
*/
|
|
57
|
+
function startCompiledApi(options) {
|
|
58
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
59
|
+
const { apiDir, env, log, logFile, pm } = options;
|
|
60
|
+
log.info('Building API (compiled, for stability — no hot reload) …');
|
|
61
|
+
const build = yield (0, dev_process_1.runChildInherit)(pm.bin, pm.runScript('build'), { cwd: apiDir, env });
|
|
62
|
+
const entry = findCompiledEntry(apiDir);
|
|
63
|
+
if (build === 0 && entry) {
|
|
64
|
+
if (hasScript(apiDir, 'migrate:up')) {
|
|
65
|
+
const migrate = yield (0, dev_process_1.runChildInherit)(pm.bin, pm.runScript('migrate:up'), { cwd: apiDir, env });
|
|
66
|
+
if (migrate !== 0) {
|
|
67
|
+
// Parity with `migrate:up && start:local`: a failed migration must PREVENT the server
|
|
68
|
+
// from starting rather than boot it against a half-migrated DB behind a "Started" banner.
|
|
69
|
+
log.warn(`migrate:up failed (exit ${String(migrate)}) — API NOT started (would run on an un-migrated DB).`);
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return (0, dev_process_1.spawnDetached)('node', [entry], { cwd: apiDir, env: Object.assign(Object.assign({}, env), { NODE_ENV: 'local' }), logFile });
|
|
74
|
+
}
|
|
75
|
+
log.warn(`compiled API unavailable (build exit ${String(build)}) — falling back to \`${pm.bin} start\` (ts-node).`);
|
|
76
|
+
return (0, dev_process_1.spawnDetached)(pm.bin, pm.runScript('start'), { cwd: apiDir, env, logFile });
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** True when `package.json` in `apiDir` defines a script named `name`. */
|
|
80
|
+
function hasScript(apiDir, name) {
|
|
81
|
+
var _a;
|
|
82
|
+
try {
|
|
83
|
+
const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(apiDir, 'package.json'), 'utf8'));
|
|
84
|
+
return typeof ((_a = pkg.scripts) === null || _a === void 0 ? void 0 : _a[name]) === 'string';
|
|
85
|
+
}
|
|
86
|
+
catch (_b) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -47,6 +47,7 @@ const fs_1 = require("fs");
|
|
|
47
47
|
const os_1 = require("os");
|
|
48
48
|
const path_1 = require("path");
|
|
49
49
|
const caddy_1 = require("./caddy");
|
|
50
|
+
const dev_api_launch_1 = require("./dev-api-launch");
|
|
50
51
|
const dev_env_1 = require("./dev-env");
|
|
51
52
|
const dev_env_bridge_1 = require("./dev-env-bridge");
|
|
52
53
|
const dev_identity_1 = require("./dev-identity");
|
|
@@ -245,9 +246,7 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
245
246
|
log.info(log.dim('Building API (compiled, for stable long runs) …'));
|
|
246
247
|
build = yield (0, dev_process_1.runChildInherit)(apiPm.bin, apiPm.runScript('build'), { cwd: layout.apiDir, env: process.env });
|
|
247
248
|
}
|
|
248
|
-
const entry =
|
|
249
|
-
.map((rel) => (0, path_1.join)(layout.apiDir, rel))
|
|
250
|
-
.find((p) => (0, fs_1.existsSync)(p));
|
|
249
|
+
const entry = (0, dev_api_launch_1.findCompiledEntry)(layout.apiDir);
|
|
251
250
|
// Seed a throwaway initial admin into the fresh, isolated test DB so the
|
|
252
251
|
// standard auth E2E specs run against a set-up system — locally exactly like
|
|
253
252
|
// the lt-monorepo template CI. Defaults first so an explicitly inherited
|
|
@@ -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
|
@@ -433,6 +433,13 @@ lt dev up
|
|
|
433
433
|
|
|
434
434
|
**Alias:** `lt d u`
|
|
435
435
|
|
|
436
|
+
**Flags:**
|
|
437
|
+
- `--api-compiled` — run the API **compiled** (`node dist/src/main.js`) instead of ts-node. Trades hot reload for stability: under sustained browser / dev-SSR load the ts-node API process intermittently dies without a stacktrace (DEV-2525); the compiled `node` process does not. Opt-in — omit it for the default ts-node hot-reload start. Details:
|
|
438
|
+
- Builds the API first, then applies pending migrations (`migrate:up`) for parity with the default `migrate:up && start:local`; a failed migration aborts the start rather than booting against a half-migrated DB.
|
|
439
|
+
- Auto-falls-back to the ts-node `start` if the build fails or produces no `dist` entry, so you never end up with a dead API.
|
|
440
|
+
- Only takes effect when the API actually (re)starts. A healthy running API is kept as-is — run `lt dev down` first to switch a live ts-node API to compiled.
|
|
441
|
+
- Accepted spellings: `--api-compiled` or `--api-compiled=true`.
|
|
442
|
+
|
|
436
443
|
**Environment variables injected:**
|
|
437
444
|
| Variable | Consumer | Example value |
|
|
438
445
|
|----------|----------|---------------|
|
|
@@ -467,8 +474,10 @@ Behaviour:
|
|
|
467
474
|
reclaims any orphaned listener still squatting the reused port.
|
|
468
475
|
|
|
469
476
|
This is the fix for the "`status` says api running but no data loads" case: a
|
|
470
|
-
crashed ts-node dev API is healed by simply re-running `lt dev up
|
|
471
|
-
|
|
477
|
+
crashed ts-node dev API is healed by simply re-running `lt dev up`. The **automatic**
|
|
478
|
+
heal keeps ts-node (it does not silently switch to compiled `node dist`, so code
|
|
479
|
+
edits still hot-reload); pass the explicit `--api-compiled` flag (see **Flags** above)
|
|
480
|
+
when you deliberately want to trade hot reload for compiled stability.
|
|
472
481
|
|
|
473
482
|
**Logs:** `<root>/.lt-dev/api.log`, `<root>/.lt-dev/app.log` (append-mode).
|
|
474
483
|
|
|
@@ -1812,9 +1821,10 @@ lt claude plugins [plugin-name] [options]
|
|
|
1812
1821
|
**Plugin Sources:**
|
|
1813
1822
|
- [lenne-tech marketplace](https://github.com/lenneTech/claude-code) - lenne.Tech plugins for NestJS development
|
|
1814
1823
|
- [claude-plugins-official](https://github.com/anthropics/claude-plugins-official) - Official Anthropic plugins (e.g., typescript-lsp)
|
|
1824
|
+
- 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
1825
|
|
|
1816
1826
|
**Default Behavior:**
|
|
1817
|
-
When run without a plugin name, all lenne.Tech plugins plus recommended external plugins (like `typescript-lsp`) are installed automatically.
|
|
1827
|
+
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
1828
|
|
|
1819
1829
|
**Examples:**
|
|
1820
1830
|
```bash
|
|
@@ -1833,6 +1843,47 @@ lt claude plugins lt-dev --uninstall
|
|
|
1833
1843
|
|
|
1834
1844
|
---
|
|
1835
1845
|
|
|
1846
|
+
### `lt claude marketplaces`
|
|
1847
|
+
|
|
1848
|
+
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.
|
|
1849
|
+
|
|
1850
|
+
**Usage:**
|
|
1851
|
+
```bash
|
|
1852
|
+
lt claude marketplaces [action] [source|name] [options]
|
|
1853
|
+
```
|
|
1854
|
+
|
|
1855
|
+
**Alias:** `lt claude mp`
|
|
1856
|
+
|
|
1857
|
+
**Actions:**
|
|
1858
|
+
| Action | Description |
|
|
1859
|
+
|--------|-------------|
|
|
1860
|
+
| _(none)_ | Interactive menu (list / add / remove) |
|
|
1861
|
+
| `list` | Show the configured marketplaces |
|
|
1862
|
+
| `add [source]` | Add a marketplace (Git URL or `owner/repo`); prompts for the source if omitted |
|
|
1863
|
+
| `remove [name]` | Remove a configured marketplace; prompts for the name if omitted |
|
|
1864
|
+
|
|
1865
|
+
**How it works:**
|
|
1866
|
+
- 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.
|
|
1867
|
+
- Discovery uses the provider-agnostic `git` strategy by default: the marketplace manifest is read from the local checkout — **no API token required**.
|
|
1868
|
+
- **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.
|
|
1869
|
+
|
|
1870
|
+
**Examples:**
|
|
1871
|
+
```bash
|
|
1872
|
+
# Interactive management
|
|
1873
|
+
lt claude marketplaces
|
|
1874
|
+
|
|
1875
|
+
# Add a marketplace (any Git host)
|
|
1876
|
+
lt claude marketplaces add git@gitlab.example.com:group/my-marketplace.git
|
|
1877
|
+
|
|
1878
|
+
# List configured marketplaces
|
|
1879
|
+
lt claude marketplaces list
|
|
1880
|
+
|
|
1881
|
+
# Remove one
|
|
1882
|
+
lt claude marketplaces remove my-marketplace
|
|
1883
|
+
```
|
|
1884
|
+
|
|
1885
|
+
---
|
|
1886
|
+
|
|
1836
1887
|
### `lt claude shortcuts`
|
|
1837
1888
|
|
|
1838
1889
|
Installs Claude Code shell shortcuts (aliases) for quick access to common commands.
|