@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.
@@ -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 GitHub
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 primaryMarketplace = marketplace_1.MARKETPLACES[0].name;
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
- pluginsToInstall = [...primaryPlugins, ...externalPlugins];
206
- if (externalPlugins.length > 0) {
207
- info(`Installing ${primaryPlugins.length} plugins from ${primaryMarketplace}`);
208
- info(` + ${externalPlugins.length} default plugins: ${externalPlugins.map((p) => p.pluginName).join(', ')}`);
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
  }
@@ -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 apiResult = (0, dev_process_1.spawnDetached)(apiPm.bin, apiPm.runScript('start'), {
365
- cwd: layout.apiDir,
366
- env: devEnv.api.env,
367
- logFile: (0, path_1.join)(layout.root, '.lt-dev', 'api.log'),
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
- module.exports = UpCommand;
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 });
@@ -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 = ['dist/src/main.js', 'dist/main.js']
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
+ }
@@ -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.MARKETPLACES = void 0;
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 fetching and managing plugins from GitHub-based marketplaces
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
- * Available marketplaces for plugin discovery
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.MARKETPLACES = [
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
- * Fetch available plugins from all configured marketplaces
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(exports.MARKETPLACES.map((marketplace) => fetchPluginsFromMarketplace(marketplace)));
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 = exports.MARKETPLACES.map((m) => ({
137
+ const byMarketplace = marketplaces
138
+ .map((m) => ({
65
139
  count: plugins.filter((p) => p.marketplaceName === m.name).length,
66
140
  name: m.name,
67
- })).filter((m) => m.count > 0);
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 fetchPluginsFromMarketplace(marketplace) {
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 (used by official marketplace)
89
- const marketplaceJsonUrl = `${marketplace.rawBase}/.claude-plugin/marketplace.json`;
90
- const marketplaceJsonResponse = yield fetch(marketplaceJsonUrl);
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: marketplace.repo,
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 = `${marketplace.rawBase}/plugins/${dir.name}/.claude-plugin/plugin.json`;
121
- const manifestResponse = yield fetch(manifestUrl);
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: marketplace.repo,
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
- * Print available plugins list
151
- * @param plugins - Array of plugin configurations
152
- * @param info - Info print function from toolbox
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 printAvailablePlugins(plugins, info) {
155
- info('Available plugins:');
156
- for (const plugin of plugins) {
157
- info(` ${plugin.pluginName} - ${plugin.description}`);
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` (it does not
471
- fall back to compiled `node dist` — ts-node is kept so code edits hot-reload).
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/cli",
3
- "version": "1.38.1",
3
+ "version": "1.40.0",
4
4
  "description": "lenne.Tech CLI: lt",
5
5
  "keywords": [
6
6
  "lenne.Tech",