@lenne.tech/cli 1.50.6 → 1.51.1
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/plugins.js +29 -16
- package/build/commands/cli/create.js +11 -3
- package/build/commands/cli/rename.js +10 -2
- package/build/commands/dev/up.js +9 -0
- package/build/commands/directus/docker-setup.js +2 -2
- package/build/commands/directus/remove.js +1 -1
- package/build/commands/directus/typegen.js +31 -4
- package/build/commands/frontend/convert-mode.js +16 -0
- package/build/commands/frontend/nuxt.js +1 -1
- package/build/commands/fullstack/add-api.js +1 -2
- package/build/commands/fullstack/add-app.js +1 -1
- package/build/commands/fullstack/convert-mode.js +20 -0
- package/build/commands/fullstack/init.js +9 -6
- package/build/commands/git/clean.js +3 -2
- package/build/commands/git/create.js +7 -2
- package/build/commands/git/force-pull.js +4 -2
- package/build/commands/git/get.js +19 -6
- package/build/commands/git/rebase.js +13 -3
- package/build/commands/git/rename.js +5 -4
- package/build/commands/git/reset.js +18 -10
- package/build/commands/git/squash.js +4 -2
- package/build/commands/git/update.js +2 -1
- package/build/commands/npm/reinit.js +3 -3
- package/build/commands/server/add-property.js +10 -0
- package/build/commands/server/convert-mode.js +16 -0
- package/build/commands/server/permissions.js +10 -10
- package/build/commands/starter/chrome-extension.js +8 -2
- package/build/commands/typescript/create.js +3 -2
- package/build/extensions/api-mode.js +1 -1
- package/build/extensions/frontend-helper.js +25 -13
- package/build/extensions/git.js +37 -18
- package/build/extensions/server.js +14 -5
- package/build/extensions/template.js +8 -5
- package/build/extensions/typescript.js +4 -3
- package/build/lib/caddy.js +72 -2
- package/build/lib/claude-cli.js +17 -2
- package/build/lib/exec-args.js +117 -0
- package/build/lib/plugin-permissions.js +201 -0
- package/build/lib/plugin-utils.js +96 -36
- package/build/lib/safe-paths.js +57 -0
- package/build/lib/unknown-options.js +53 -0
- package/build/lib/workspace-integration.js +4 -2
- package/docs/commands.md +12 -0
- package/package.json +6 -6
|
@@ -22,11 +22,11 @@ exports.setupPermissions = setupPermissions;
|
|
|
22
22
|
* Handles reading plugin contents, permissions, and post-installation setup
|
|
23
23
|
*/
|
|
24
24
|
const fs_1 = require("fs");
|
|
25
|
-
const os_1 = require("os");
|
|
26
25
|
const path_1 = require("path");
|
|
27
26
|
const claude_cli_1 = require("./claude-cli");
|
|
28
27
|
const json_utils_1 = require("./json-utils");
|
|
29
28
|
const platform_1 = require("./platform");
|
|
29
|
+
const plugin_permissions_1 = require("./plugin-permissions");
|
|
30
30
|
const shell_config_1 = require("./shell-config");
|
|
31
31
|
/**
|
|
32
32
|
* Empty plugin contents constant (used for failed installations)
|
|
@@ -34,6 +34,7 @@ const shell_config_1 = require("./shell-config");
|
|
|
34
34
|
exports.EMPTY_PLUGIN_CONTENTS = {
|
|
35
35
|
agents: [],
|
|
36
36
|
commands: [],
|
|
37
|
+
formerPermissions: [],
|
|
37
38
|
hooks: 0,
|
|
38
39
|
mcpServers: [],
|
|
39
40
|
permissions: [],
|
|
@@ -312,10 +313,11 @@ function processPostInstall(pluginName, toolbox) {
|
|
|
312
313
|
* @returns Plugin contents with skills, commands, agents, hooks, etc.
|
|
313
314
|
*/
|
|
314
315
|
function readPluginContents(marketplaceName, pluginName) {
|
|
315
|
-
const pluginDir = (0, path_1.join)((0,
|
|
316
|
+
const pluginDir = (0, path_1.join)((0, claude_cli_1.getClaudeConfigDir)(), 'plugins', 'marketplaces', marketplaceName, 'plugins', pluginName);
|
|
316
317
|
const result = {
|
|
317
318
|
agents: [],
|
|
318
319
|
commands: [],
|
|
320
|
+
formerPermissions: [],
|
|
319
321
|
hooks: 0,
|
|
320
322
|
mcpServers: [],
|
|
321
323
|
permissions: [],
|
|
@@ -375,6 +377,9 @@ function readPluginContents(marketplaceName, pluginName) {
|
|
|
375
377
|
if (pluginPerms === null || pluginPerms === void 0 ? void 0 : pluginPerms.permissions) {
|
|
376
378
|
result.permissions = pluginPerms.permissions.map((p) => p.pattern);
|
|
377
379
|
}
|
|
380
|
+
if (Array.isArray(pluginPerms === null || pluginPerms === void 0 ? void 0 : pluginPerms.formerPatterns)) {
|
|
381
|
+
result.formerPermissions = pluginPerms.formerPatterns.filter((p) => typeof p === 'string');
|
|
382
|
+
}
|
|
378
383
|
return result;
|
|
379
384
|
}
|
|
380
385
|
/**
|
|
@@ -392,57 +397,112 @@ function safeExecCommand(command) {
|
|
|
392
397
|
}
|
|
393
398
|
}
|
|
394
399
|
/**
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
400
|
+
* Sync a plugin's permission rules into Claude Code's user settings.json.
|
|
401
|
+
*
|
|
402
|
+
* Adds the rules the plugin ships and removes the rules lt wrote for the plugin that it no
|
|
403
|
+
* longer ships. Rules the user wrote stay untouched. How the two are told apart:
|
|
404
|
+
* plugin-permissions.ts. Rules with `${CLAUDE_PLUGIN_ROOT}` are never written.
|
|
405
|
+
* @param plugin - Marketplace and name of the plugin
|
|
406
|
+
* @param rules - `permissions` and `formerPermissions` from the plugin's permissions.json, may be empty
|
|
399
407
|
* @param error - Error print function from toolbox
|
|
400
|
-
* @
|
|
408
|
+
* @param options - Overrides for the Claude config directory and the ledger (tests)
|
|
409
|
+
* @returns Added, existing and removed rules
|
|
401
410
|
*/
|
|
402
|
-
function setupPermissions(
|
|
403
|
-
|
|
411
|
+
function setupPermissions(plugin, rules, error, options = {}) {
|
|
412
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
413
|
+
const claudeConfigDir = (_a = options.claudeConfigDir) !== null && _a !== void 0 ? _a : (0, claude_cli_1.getClaudeConfigDir)();
|
|
414
|
+
const ledgerPath = (_b = options.ledgerPath) !== null && _b !== void 0 ? _b : (0, plugin_permissions_1.getPermissionLedgerPath)();
|
|
415
|
+
const settingsPath = (0, path_1.join)(claudeConfigDir, 'settings.json');
|
|
416
|
+
const pluginKey = (0, plugin_permissions_1.pluginPermissionKey)(plugin.marketplaceName, plugin.pluginName);
|
|
417
|
+
const pluginRoot = (0, plugin_permissions_1.pluginCacheRootPattern)(claudeConfigDir, plugin.marketplaceName, plugin.pluginName);
|
|
418
|
+
const resolve = (rule) => (0, plugin_permissions_1.resolvePluginRootVariable)(rule, pluginRoot);
|
|
419
|
+
const skipped = [...new Set(rules.permissions.filter(plugin_permissions_1.hasPluginRootVariable))];
|
|
420
|
+
const written = [...new Set(rules.permissions.filter((rule) => !(0, plugin_permissions_1.hasPluginRootVariable)(rule)))];
|
|
421
|
+
const failed = {
|
|
422
|
+
added: [],
|
|
423
|
+
existing: [],
|
|
424
|
+
removed: [],
|
|
425
|
+
settingsPath,
|
|
426
|
+
skipped,
|
|
427
|
+
success: false,
|
|
428
|
+
written,
|
|
429
|
+
};
|
|
404
430
|
try {
|
|
405
|
-
// Read existing settings
|
|
406
431
|
let settings = {};
|
|
407
432
|
if ((0, fs_1.existsSync)(settingsPath)) {
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
433
|
+
const parsed = (0, json_utils_1.safeJsonParse)((0, fs_1.readFileSync)(settingsPath, 'utf-8'));
|
|
434
|
+
if (!isPlainObject(parsed)) {
|
|
435
|
+
// Writing now would replace every other setting in the file with an empty object.
|
|
436
|
+
error(`Could not configure permissions: ${settingsPath} is not a valid JSON object, left unchanged`);
|
|
437
|
+
return failed;
|
|
412
438
|
}
|
|
439
|
+
settings = parsed;
|
|
413
440
|
}
|
|
414
|
-
|
|
415
|
-
if (!
|
|
416
|
-
|
|
441
|
+
const perms = (_c = settings.permissions) !== null && _c !== void 0 ? _c : {};
|
|
442
|
+
if (!isPlainObject(perms)) {
|
|
443
|
+
error(`Could not configure permissions: "permissions" in ${settingsPath} is not an object, left unchanged`);
|
|
444
|
+
return failed;
|
|
417
445
|
}
|
|
418
|
-
const
|
|
419
|
-
if (!
|
|
420
|
-
|
|
446
|
+
const allow = (_d = perms.allow) !== null && _d !== void 0 ? _d : [];
|
|
447
|
+
if (!Array.isArray(allow) || !allow.every((rule) => typeof rule === 'string')) {
|
|
448
|
+
error(`Could not configure permissions: "permissions.allow" in ${settingsPath} is not a list of rules, left unchanged`);
|
|
449
|
+
return failed;
|
|
421
450
|
}
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
451
|
+
const ledgerEntries = (0, plugin_permissions_1.readLedgerEntries)(ledgerPath, settingsPath);
|
|
452
|
+
const entry = (_e = ledgerEntries[pluginKey]) !== null && _e !== void 0 ? _e : null;
|
|
453
|
+
const plan = (0, plugin_permissions_1.planPermissionSync)({
|
|
454
|
+
allow,
|
|
455
|
+
formerApplied: (_f = entry === null || entry === void 0 ? void 0 : entry.formerApplied) !== null && _f !== void 0 ? _f : [],
|
|
456
|
+
// A former rule with the variable may sit in settings.json in either spelling.
|
|
457
|
+
formerRules: [...new Set([...rules.formerPermissions, ...rules.formerPermissions.map(resolve)])],
|
|
458
|
+
listed: written,
|
|
459
|
+
owned: (_g = entry === null || entry === void 0 ? void 0 : entry.owned) !== null && _g !== void 0 ? _g : null,
|
|
460
|
+
ownedByOthers: Object.entries(ledgerEntries)
|
|
461
|
+
.filter(([key]) => key !== pluginKey)
|
|
462
|
+
.flatMap(([, other]) => other.owned),
|
|
463
|
+
withdrawnRules: [...skipped, ...skipped.map(resolve)],
|
|
433
464
|
});
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
465
|
+
if (plan.added.length > 0 || plan.removed.length > 0) {
|
|
466
|
+
perms.allow = plan.allow;
|
|
467
|
+
settings.permissions = perms;
|
|
468
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(settingsPath), { recursive: true });
|
|
437
469
|
(0, fs_1.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2));
|
|
438
470
|
}
|
|
439
|
-
|
|
471
|
+
// Only after settings.json: a ledger entry must never claim a write that did not happen.
|
|
472
|
+
// If this write fails, the rules added in this run stay unrecorded, and a later run
|
|
473
|
+
// will not remove them when the plugin drops them. The message says so.
|
|
474
|
+
if (JSON.stringify(entry) !== JSON.stringify(plan.ledger)) {
|
|
475
|
+
try {
|
|
476
|
+
(0, plugin_permissions_1.recordLedgerEntry)(ledgerPath, settingsPath, pluginKey, plan.ledger);
|
|
477
|
+
}
|
|
478
|
+
catch (err) {
|
|
479
|
+
error(`Could not record lt's permission rules in ${ledgerPath}: ${err.message}. ` +
|
|
480
|
+
'Rules added in this run will not be removed automatically when the plugin drops them.');
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
added: plan.added,
|
|
485
|
+
existing: plan.existing,
|
|
486
|
+
removed: plan.removed,
|
|
487
|
+
settingsPath,
|
|
488
|
+
skipped,
|
|
489
|
+
success: true,
|
|
490
|
+
written,
|
|
491
|
+
};
|
|
440
492
|
}
|
|
441
493
|
catch (err) {
|
|
442
494
|
error(`Could not configure permissions: ${err.message}`);
|
|
443
|
-
return
|
|
495
|
+
return failed;
|
|
444
496
|
}
|
|
445
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* Whether a parsed JSON value is an object (not an array, not null)
|
|
500
|
+
* @param value - Parsed JSON value
|
|
501
|
+
* @returns true for a plain object
|
|
502
|
+
*/
|
|
503
|
+
function isPlainObject(value) {
|
|
504
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
505
|
+
}
|
|
446
506
|
/**
|
|
447
507
|
* Safely read and parse a JSON file
|
|
448
508
|
* @param filePath - Path to the JSON file
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fileOpener = fileOpener;
|
|
4
|
+
exports.isPathInside = isPathInside;
|
|
5
|
+
exports.isSafePathSegment = isSafePathSegment;
|
|
6
|
+
exports.openFileWithDefaultApp = openFileWithDefaultApp;
|
|
7
|
+
/**
|
|
8
|
+
* Path checks for values that arrive from CLI flags or `lt.config` files. Those values can come
|
|
9
|
+
* from a repository the user cloned (a config file in a parent directory counts too) or from an
|
|
10
|
+
* AI agent running `lt` under a global permission rule, so they must not reach outside the
|
|
11
|
+
* project the command was asked to work on, and must not be handed to a shell.
|
|
12
|
+
*/
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
const path_1 = require("path");
|
|
15
|
+
/**
|
|
16
|
+
* The program and arguments that open a file with its default application, without a shell.
|
|
17
|
+
* Windows uses `explorer.exe`: `start` is a `cmd.exe` builtin, and `cmd.exe` re-parses its
|
|
18
|
+
* command line, where `&` in a file name (legal on Windows) would start a second command.
|
|
19
|
+
* @param platform - `process.platform`
|
|
20
|
+
* @param filePath - File to open
|
|
21
|
+
* @returns Program and argument list
|
|
22
|
+
*/
|
|
23
|
+
function fileOpener(platform, filePath) {
|
|
24
|
+
if (platform === 'darwin')
|
|
25
|
+
return { args: [filePath], command: 'open' };
|
|
26
|
+
if (platform === 'win32')
|
|
27
|
+
return { args: [filePath], command: 'explorer.exe' };
|
|
28
|
+
return { args: [filePath], command: 'xdg-open' };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Whether `target` lies inside `base` (or is `base` itself), after resolving `..` segments.
|
|
32
|
+
* @param base - Directory the target must stay in
|
|
33
|
+
* @param target - Path to check
|
|
34
|
+
* @returns true if the target does not escape the base
|
|
35
|
+
*/
|
|
36
|
+
function isPathInside(base, target) {
|
|
37
|
+
const rel = (0, path_1.relative)((0, path_1.resolve)(base), (0, path_1.resolve)(target));
|
|
38
|
+
return rel === '' || (!rel.startsWith('..') && !(0, path_1.isAbsolute)(rel));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Whether a name can be used as a single directory segment (a module or object name): no
|
|
42
|
+
* separators, not `.` or `..`, no NUL. `join(dir, name)` with anything else leaves `dir`.
|
|
43
|
+
* @param name - Name from a flag or a prompt
|
|
44
|
+
* @returns true if the name stays one segment below its parent
|
|
45
|
+
*/
|
|
46
|
+
function isSafePathSegment(name) {
|
|
47
|
+
return Boolean(name) && name !== '.' && name !== '..' && !/[/\\\0]/.test(name);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Open a file with its default application. Fire and forget, like before; `explorer.exe` exits
|
|
51
|
+
* with 1 even on success, so errors are ignored.
|
|
52
|
+
* @param filePath - File to open
|
|
53
|
+
*/
|
|
54
|
+
function openFileWithDefaultApp(filePath) {
|
|
55
|
+
const { args, command } = fileOpener(process.platform, filePath);
|
|
56
|
+
(0, child_process_1.execFile)(command, args, () => undefined);
|
|
57
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Flags a command did not declare.
|
|
4
|
+
*
|
|
5
|
+
* gluegun parses argv with yargs-parser and no schema, so a misspelt flag is silently ignored.
|
|
6
|
+
* For `lt fullstack convert-mode` that converted more than asked: the plugin docs said
|
|
7
|
+
* `--skip-app`, the CLI only knows `--skip-frontend`, and with `--noConfirm` the frontend was
|
|
8
|
+
* converted although the caller had asked to skip it. A command that changes a project should
|
|
9
|
+
* refuse a flag it does not know rather than guess.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.findUnknownOptions = findUnknownOptions;
|
|
13
|
+
exports.formatUnknownOptions = formatUnknownOptions;
|
|
14
|
+
/** Flags every command accepts: gluegun's menu, `bin/lt`, the help interceptor, confirmations. */
|
|
15
|
+
const GLOBAL_OPTIONS = ['compiled-build', 'fromGluegunMenu', 'h', 'help', 'help-json', 'noConfirm'];
|
|
16
|
+
/**
|
|
17
|
+
* Options in `options` that are neither declared nor global. yargs-parser reports a kebab-case
|
|
18
|
+
* flag twice (`skip-app` and `skipApp`); it is reported once, in the spelling that was typed.
|
|
19
|
+
* @param options - `parameters.options` of the command
|
|
20
|
+
* @param declaredFlags - The command's flags, with or without leading dashes (e.g. from its `help`)
|
|
21
|
+
* @returns Unknown option names, without dashes
|
|
22
|
+
*/
|
|
23
|
+
function findUnknownOptions(options, declaredFlags) {
|
|
24
|
+
const accepted = new Set([...declaredFlags.map((flag) => flag.replace(/^-+/, '')), ...GLOBAL_OPTIONS].map(toCamel));
|
|
25
|
+
const unknown = new Map();
|
|
26
|
+
for (const key of Object.keys(options)) {
|
|
27
|
+
const camel = toCamel(key);
|
|
28
|
+
if (!accepted.has(camel) && !unknown.has(camel)) {
|
|
29
|
+
unknown.set(camel, key);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return [...unknown.values()];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Error text for unknown options, with the right name where a known misspelling has one.
|
|
36
|
+
* @param unknown - Result of {@link findUnknownOptions}
|
|
37
|
+
* @param declaredFlags - The command's flags
|
|
38
|
+
* @param renamed - Known wrong name → right flag, without dashes (e.g. `skip-app` → `skip-frontend`)
|
|
39
|
+
* @returns Message naming each unknown flag and listing the known ones
|
|
40
|
+
*/
|
|
41
|
+
function formatUnknownOptions(unknown, declaredFlags, renamed = {}) {
|
|
42
|
+
const listed = unknown.map((name) => (renamed[name] ? `--${name} (use --${renamed[name]})` : `--${name}`));
|
|
43
|
+
const known = declaredFlags.map((flag) => `--${flag.replace(/^-+/, '')}`).join(', ');
|
|
44
|
+
return `Unknown option${unknown.length > 1 ? 's' : ''}: ${listed.join(', ')}. Nothing was changed.\nKnown options: ${known}`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* `skip-app` → `skipApp`, as yargs-parser expands it
|
|
48
|
+
* @param name - Option name
|
|
49
|
+
* @returns camelCase form
|
|
50
|
+
*/
|
|
51
|
+
function toCamel(name) {
|
|
52
|
+
return name.replace(/-([a-z0-9])/g, (_, char) => char.toUpperCase());
|
|
53
|
+
}
|
|
@@ -83,6 +83,7 @@ exports.writeApiConfig = writeApiConfig;
|
|
|
83
83
|
const yaml = __importStar(require("js-yaml"));
|
|
84
84
|
const path_1 = require("path");
|
|
85
85
|
const ensure_root_dockerignore_1 = require("./ensure-root-dockerignore");
|
|
86
|
+
const exec_args_1 = require("./exec-args");
|
|
86
87
|
const hoist_workspace_pnpm_config_1 = require("./hoist-workspace-pnpm-config");
|
|
87
88
|
const remove_nested_lockfiles_1 = require("./remove-nested-lockfiles");
|
|
88
89
|
/**
|
|
@@ -321,7 +322,7 @@ function reportWorkspaceConflicts(conflicts, warn) {
|
|
|
321
322
|
*/
|
|
322
323
|
function runExperimentalNestBaseRename(options) {
|
|
323
324
|
return __awaiter(this, void 0, void 0, function* () {
|
|
324
|
-
const { apiDir, patching, projectDir,
|
|
325
|
+
const { apiDir, patching, projectDir, run = exec_args_1.spawnArgs } = options;
|
|
325
326
|
// setupServerForFullstack already patched package.json to set
|
|
326
327
|
// `name = projectDir`. The rename planner reads that name as the
|
|
327
328
|
// "old" slug, which would short-circuit the rest of the rewrites
|
|
@@ -333,7 +334,8 @@ function runExperimentalNestBaseRename(options) {
|
|
|
333
334
|
return config;
|
|
334
335
|
});
|
|
335
336
|
try {
|
|
336
|
-
|
|
337
|
+
// No shell: projectDir can come from --name or the directory name (see exec-args.ts).
|
|
338
|
+
yield run('bun', ['run', 'rename', projectDir], { cwd: apiDir });
|
|
337
339
|
return { attempted: true };
|
|
338
340
|
}
|
|
339
341
|
catch (err) {
|
package/docs/commands.md
CHANGED
|
@@ -426,6 +426,8 @@ lt dev init --skip-install # do NOT auto-run `lt dev install` first
|
|
|
426
426
|
|
|
427
427
|
Start API + App behind Caddy. Allocates internal ports (4000+), spawns processes detached, persists PIDs to `<root>/.lt-dev/state.json`.
|
|
428
428
|
|
|
429
|
+
Caddy listens on the loopback interfaces only (`default_bind 127.0.0.1 [::1]` in the global options block of `~/.lenneTech/Caddyfile`), so a running stack is not reachable from the local network. `lt dev up` adds the block to a Caddyfile written by an older CLI and reloads Caddy. A `default_bind` you set yourself is left as it is.
|
|
430
|
+
|
|
429
431
|
**Usage:**
|
|
430
432
|
```bash
|
|
431
433
|
lt dev up
|
|
@@ -1283,6 +1285,8 @@ lt fullstack convert-mode --to <vendor|npm> [options]
|
|
|
1283
1285
|
| `--dry-run` | Show the resolved plan without making any changes |
|
|
1284
1286
|
| `--noConfirm` | Skip confirmation prompt |
|
|
1285
1287
|
|
|
1288
|
+
Any other flag is refused with exit code 1 before anything is converted, and a known wrong name gets the right one in the message (`--skip-app` → `--skip-frontend`, `--api-version` → `--framework-version`, …). Before, an unknown flag was ignored, so a misspelt `--skip-*` together with `--noConfirm` converted the side it was meant to skip. `lt server convert-mode` and `lt frontend convert-mode` refuse unknown flags the same way.
|
|
1289
|
+
|
|
1286
1290
|
**Subproject Detection:**
|
|
1287
1291
|
|
|
1288
1292
|
The command searches for subprojects in this order:
|
|
@@ -1994,6 +1998,14 @@ To list or uninstall installed plugins, use Claude Code's own `/plugin` menu.
|
|
|
1994
1998
|
**Default Behavior:**
|
|
1995
1999
|
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.
|
|
1996
2000
|
|
|
2001
|
+
**Permission rules:**
|
|
2002
|
+
A plugin's `permissions.json` is synced into Claude Code's user `settings.json` (`$CLAUDE_CONFIG_DIR/settings.json`, default `~/.claude/settings.json`). These rules apply in every project, so the sync goes both ways:
|
|
2003
|
+
|
|
2004
|
+
- Rules the plugin ships are added, except rules that use `${CLAUDE_PLUGIN_ROOT}`. Claude Code does not expand that variable in permission rules, so such a rule never matches, and replacing it with the plugin's cache directory plus a wildcard for the version folder would allow any file on disk (a wildcard also matches `../`). lt therefore writes neither form and removes both if present; Claude asks before running those plugin scripts.
|
|
2005
|
+
- Rules lt wrote for a plugin that the plugin no longer ships are removed, and listed as `removed:` in the output. lt records the rules it wrote in `~/.lenneTech/claude-plugin-permissions.json`. Rules you added yourself are never removed.
|
|
2006
|
+
- Rules older lt versions installed before that record existed are named by the plugin itself, in an optional `formerPatterns` list of strings in its `permissions.json` (e.g. `"formerPatterns": ["Bash(curl:*)", "Bash(rm:*)"]`). lt removes each of them once, unless the plugin still ships it. Add one back by hand if you want it; lt leaves it alone after that.
|
|
2007
|
+
- If `settings.json` is not valid JSON, lt leaves it unchanged and reports an error.
|
|
2008
|
+
|
|
1997
2009
|
**Examples:**
|
|
1998
2010
|
```bash
|
|
1999
2011
|
# Install all recommended plugins (lenne.Tech + recommended external)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.51.1",
|
|
4
4
|
"description": "lenne.Tech CLI: lt",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lenne.Tech",
|
|
@@ -58,13 +58,13 @@
|
|
|
58
58
|
"bin"
|
|
59
59
|
],
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@aws-sdk/client-s3": "3.
|
|
61
|
+
"@aws-sdk/client-s3": "3.1152.0",
|
|
62
62
|
"@lenne.tech/cli-plugin-helper": "0.0.14",
|
|
63
63
|
"axios": "1.20.0",
|
|
64
64
|
"bcrypt": "6.0.0",
|
|
65
65
|
"cross-spawn": "7.0.6",
|
|
66
66
|
"defuddle": "0.19.5",
|
|
67
|
-
"ejs": "7.0.
|
|
67
|
+
"ejs": "7.0.2",
|
|
68
68
|
"glob": "13.0.6",
|
|
69
69
|
"gluegun": "5.2.2",
|
|
70
70
|
"js-sha256": "1.0.0",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"jsonc-parser": "3.3.1",
|
|
74
74
|
"lodash": "4.18.1",
|
|
75
75
|
"open": "11.0.4",
|
|
76
|
-
"playwright-core": "1.64.
|
|
76
|
+
"playwright-core": "1.64.1",
|
|
77
77
|
"ts-morph": "28.0.0",
|
|
78
78
|
"ts-node": "10.9.2",
|
|
79
79
|
"turndown": "7.2.4",
|
|
@@ -97,13 +97,13 @@
|
|
|
97
97
|
"@types/lodash": "4.17.26",
|
|
98
98
|
"@types/node": "26.6.5",
|
|
99
99
|
"@types/turndown": "5.0.6",
|
|
100
|
-
"eslint": "10.
|
|
100
|
+
"eslint": "10.13.0",
|
|
101
101
|
"husky": "9.1.7",
|
|
102
102
|
"jest": "30.5.2",
|
|
103
103
|
"minimatch": "10.2.6",
|
|
104
104
|
"prettier": "3.9.10",
|
|
105
105
|
"rimraf": "6.1.3",
|
|
106
|
-
"ts-jest": "29.4.
|
|
106
|
+
"ts-jest": "29.4.15"
|
|
107
107
|
},
|
|
108
108
|
"//overrides": {
|
|
109
109
|
"semver@*": "Force latest semver 7.x across all sub-deps; gluegun@5.2.2 pins semver@7.7.0 which is stale - remove once gluegun updates its dep.",
|