@lenne.tech/cli 1.46.0 → 1.48.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/bin/postinstall.js +28 -5
- package/build/commands/dev/doctor.js +45 -3
- package/build/commands/fullstack/update.js +31 -0
- package/build/commands/git/force-pull.js +11 -2
- package/build/commands/git/rebase.js +11 -1
- package/build/commands/git/reset.js +36 -4
- package/build/commands/git/update.js +42 -6
- package/build/extensions/api-mode.js +32 -8
- package/build/extensions/git.js +23 -25
- package/build/extensions/server.js +24 -17
- package/build/lib/check-template-sync.js +127 -0
- package/build/lib/claude-cli.js +14 -25
- package/build/lib/codex-cli.js +24 -31
- package/build/lib/dev-api-launch.js +58 -6
- package/build/lib/dev-process.js +66 -30
- package/build/lib/dev-state.js +13 -2
- package/build/lib/dev-test-session.js +85 -20
- package/build/lib/git-env.js +46 -0
- package/build/lib/heal-check-wrapper.js +128 -11
- package/build/lib/heal-oxlintrc.js +232 -0
- package/build/lib/platform.js +107 -0
- package/build/lib/plugin-utils.js +9 -8
- package/build/lib/strip-vendor-schema-augmentation.js +13 -2
- package/build/lib/vendor-claude-md.js +9 -6
- package/build/lib/workspace-integration.js +12 -2
- package/build/templates/check/check.mjs +270 -268
- package/build/templates/check/lib/ansi.mjs +22 -0
- package/build/templates/check/lib/audit-report.mjs +412 -0
- package/build/templates/check/lib/workspace-packages.mjs +150 -0
- package/build/templates/check/lt-monorepo-pin.json +13 -0
- package/docs/commands.md +1 -11
- package/package.json +13 -14
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findExecutable = findExecutable;
|
|
4
|
+
exports.isWindows = isWindows;
|
|
5
|
+
exports.spawnCmdSync = spawnCmdSync;
|
|
6
|
+
exports.windowsAppData = windowsAppData;
|
|
7
|
+
const cross_spawn_1 = require("cross-spawn");
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const path_1 = require("path");
|
|
10
|
+
const DEFAULT_PATHEXT = '.COM;.EXE;.BAT;.CMD';
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a binary the way a shell would, without spawning `which`/`where.exe`.
|
|
13
|
+
*
|
|
14
|
+
* `candidates` win over PATH, in order. On Windows each PATH directory is
|
|
15
|
+
* probed with every PATHEXT extension; an extensionless file is never matched
|
|
16
|
+
* there, because Node cannot spawn it (it is typically a Git Bash script).
|
|
17
|
+
*
|
|
18
|
+
* @returns Absolute path of the first match, or null
|
|
19
|
+
*/
|
|
20
|
+
function findExecutable(name, options = {}) {
|
|
21
|
+
const { candidates = [], env = process.env, isExecutableFile = isExecutableFileOnDisk } = options;
|
|
22
|
+
const windows = isWindows(options.platform);
|
|
23
|
+
const pathApi = windows ? path_1.win32 : path_1.posix;
|
|
24
|
+
for (const candidate of candidates) {
|
|
25
|
+
if (isExecutableFile(candidate)) {
|
|
26
|
+
return candidate;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const dirs = (readEnv(env, 'PATH', windows) || '')
|
|
30
|
+
.split(pathApi.delimiter)
|
|
31
|
+
.map((dir) => dir.trim().replace(/^"(.*)"$/, '$1'))
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
const names = windows ? windowsNameVariants(name, readEnv(env, 'PATHEXT', windows)) : [name];
|
|
34
|
+
for (const dir of dirs) {
|
|
35
|
+
for (const variant of names) {
|
|
36
|
+
const full = pathApi.join(dir, variant);
|
|
37
|
+
if (isExecutableFile(full)) {
|
|
38
|
+
return full;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* True for native Windows (PowerShell, cmd.exe and Git Bash alike — Node
|
|
46
|
+
* reports `win32` in all three).
|
|
47
|
+
*/
|
|
48
|
+
function isWindows(platform = process.platform) {
|
|
49
|
+
return platform === 'win32';
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* `spawnSync` that also runs `.cmd`/`.bat` shims on Windows.
|
|
53
|
+
*
|
|
54
|
+
* Delegates to cross-spawn, which is a plain `child_process.spawnSync` on
|
|
55
|
+
* macOS/Linux. On Windows it resolves the shim and runs it through cmd.exe with
|
|
56
|
+
* escaped arguments.
|
|
57
|
+
*
|
|
58
|
+
* Limit on Windows: cross-spawn escapes cmd.exe metacharacters (`& | % ^ …`) twice
|
|
59
|
+
* only for shims under `node_modules\.bin`. A global npm shim such as
|
|
60
|
+
* `%APPDATA%\npm\claude.cmd` re-parses its `%*`, so there a metacharacter can
|
|
61
|
+
* still take effect. Pass plain arguments (names, slugs, paths), never
|
|
62
|
+
* untrusted free text.
|
|
63
|
+
*/
|
|
64
|
+
function spawnCmdSync(command, args = [], options = {}) {
|
|
65
|
+
return (0, cross_spawn_1.sync)(command, [...args], Object.assign({ encoding: 'utf-8' }, options));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* `%APPDATA%` (roaming profile), home of npm's global `.cmd` shims on Windows.
|
|
69
|
+
*/
|
|
70
|
+
function windowsAppData(env, home) {
|
|
71
|
+
return readEnv(env, 'APPDATA', true) || path_1.win32.join(home, 'AppData', 'Roaming');
|
|
72
|
+
}
|
|
73
|
+
function isExecutableFileOnDisk(path) {
|
|
74
|
+
try {
|
|
75
|
+
if (!(0, fs_1.statSync)(path).isFile()) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
// Windows has no execute bit; X_OK degrades to an existence check there
|
|
79
|
+
(0, fs_1.accessSync)(path, fs_1.constants.X_OK);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch (_a) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Windows environment keys are case-insensitive (`Path` is the usual spelling).
|
|
88
|
+
* `process.env` hides that, a plain object passed in a test does not.
|
|
89
|
+
*/
|
|
90
|
+
function readEnv(env, key, windows) {
|
|
91
|
+
if (!windows) {
|
|
92
|
+
return env[key];
|
|
93
|
+
}
|
|
94
|
+
const match = Object.keys(env).find((k) => k.toUpperCase() === key);
|
|
95
|
+
return match ? env[match] : undefined;
|
|
96
|
+
}
|
|
97
|
+
function windowsNameVariants(name, pathExt) {
|
|
98
|
+
const extensions = (pathExt || DEFAULT_PATHEXT)
|
|
99
|
+
.split(';')
|
|
100
|
+
.map((ext) => ext.trim().toLowerCase())
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
const ownExtension = path_1.win32.extname(name).toLowerCase();
|
|
103
|
+
if (ownExtension && extensions.includes(ownExtension)) {
|
|
104
|
+
return [name];
|
|
105
|
+
}
|
|
106
|
+
return extensions.map((ext) => `${name}${ext}`);
|
|
107
|
+
}
|
|
@@ -21,12 +21,12 @@ exports.setupPermissions = setupPermissions;
|
|
|
21
21
|
* Plugin utilities for Claude Code plugin management
|
|
22
22
|
* Handles reading plugin contents, permissions, and post-installation setup
|
|
23
23
|
*/
|
|
24
|
-
const child_process_1 = require("child_process");
|
|
25
24
|
const fs_1 = require("fs");
|
|
26
25
|
const os_1 = require("os");
|
|
27
26
|
const path_1 = require("path");
|
|
28
27
|
const claude_cli_1 = require("./claude-cli");
|
|
29
28
|
const json_utils_1 = require("./json-utils");
|
|
29
|
+
const platform_1 = require("./platform");
|
|
30
30
|
const shell_config_1 = require("./shell-config");
|
|
31
31
|
/**
|
|
32
32
|
* Empty plugin contents constant (used for failed installations)
|
|
@@ -62,8 +62,8 @@ exports.PLUGIN_POST_INSTALL = {
|
|
|
62
62
|
],
|
|
63
63
|
requirements: [
|
|
64
64
|
{
|
|
65
|
-
checkCommand: 'which typescript-language-server',
|
|
66
65
|
description: 'TypeScript language server',
|
|
66
|
+
executable: 'typescript-language-server',
|
|
67
67
|
installCommand: 'npm install -g typescript-language-server typescript',
|
|
68
68
|
},
|
|
69
69
|
],
|
|
@@ -245,10 +245,11 @@ function processPostInstall(pluginName, toolbox) {
|
|
|
245
245
|
// Check and install requirements
|
|
246
246
|
if (postInstall.requirements) {
|
|
247
247
|
for (const req of postInstall.requirements) {
|
|
248
|
-
// If checkCommand is provided, verify if requirement is already met
|
|
249
|
-
if (req.checkCommand) {
|
|
248
|
+
// If executable or checkCommand is provided, verify if requirement is already met
|
|
249
|
+
if (req.executable || req.checkCommand) {
|
|
250
|
+
const isMet = () => req.executable ? (0, platform_1.findExecutable)(req.executable) !== null : (0, claude_cli_1.checkCommandExists)(req.checkCommand);
|
|
250
251
|
const checkSpinner = spin(`Checking ${req.description}`);
|
|
251
|
-
if ((
|
|
252
|
+
if (isMet()) {
|
|
252
253
|
checkSpinner.succeed(`${req.description} already installed`);
|
|
253
254
|
continue;
|
|
254
255
|
}
|
|
@@ -257,7 +258,7 @@ function processPostInstall(pluginName, toolbox) {
|
|
|
257
258
|
try {
|
|
258
259
|
safeExecCommand(req.installCommand);
|
|
259
260
|
// Verify installation
|
|
260
|
-
if ((
|
|
261
|
+
if (isMet()) {
|
|
261
262
|
checkSpinner.succeed(`${req.description} installed`);
|
|
262
263
|
result.requirementsInstalled.push(req.description);
|
|
263
264
|
}
|
|
@@ -377,7 +378,7 @@ function readPluginContents(marketplaceName, pluginName) {
|
|
|
377
378
|
return result;
|
|
378
379
|
}
|
|
379
380
|
/**
|
|
380
|
-
* Execute a shell command safely using
|
|
381
|
+
* Execute a shell command safely using spawnCmdSync (no shell interpretation, `.cmd` shims such as npm work on Windows)
|
|
381
382
|
* Splits the command string into executable and arguments to prevent command injection
|
|
382
383
|
* @param command - Command string to execute (e.g., 'npm install -g typescript')
|
|
383
384
|
* @throws Error if command fails (non-zero exit code or signal)
|
|
@@ -385,7 +386,7 @@ function readPluginContents(marketplaceName, pluginName) {
|
|
|
385
386
|
function safeExecCommand(command) {
|
|
386
387
|
var _a;
|
|
387
388
|
const [cmd, ...args] = command.trim().split(/\s+/);
|
|
388
|
-
const result = (0,
|
|
389
|
+
const result = (0, platform_1.spawnCmdSync)(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
389
390
|
if (result.status !== 0 || result.error) {
|
|
390
391
|
throw new Error(result.stderr || ((_a = result.error) === null || _a === void 0 ? void 0 : _a.message) || `Command failed: ${command}`);
|
|
391
392
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.stripAugmentationBlocks = stripAugmentationBlocks;
|
|
4
4
|
exports.stripVendorSchemaAugmentation = stripVendorSchemaAugmentation;
|
|
5
|
+
const glob_1 = require("glob");
|
|
5
6
|
const fs_utils_1 = require("./fs-utils");
|
|
6
7
|
/**
|
|
7
8
|
* Drop `declare module '<nuxt|@nuxt>/schema' { … }` blocks that only augment
|
|
@@ -118,7 +119,6 @@ function stripAugmentationBlocks(source, onSkip) {
|
|
|
118
119
|
* is still there and only the transform knows it.
|
|
119
120
|
*/
|
|
120
121
|
function stripVendorSchemaAugmentation(options) {
|
|
121
|
-
var _a;
|
|
122
122
|
const { coreDir, filesystem } = options;
|
|
123
123
|
if (!filesystem.isDirectory(coreDir))
|
|
124
124
|
return { touched: [], warnings: [] };
|
|
@@ -128,7 +128,18 @@ function stripVendorSchemaAugmentation(options) {
|
|
|
128
128
|
return { touched: [], warnings: [] };
|
|
129
129
|
const touched = [];
|
|
130
130
|
const warnings = [];
|
|
131
|
-
|
|
131
|
+
// `glob`, not `filesystem.find`: gluegun's find is fs-jetpack, which builds its
|
|
132
|
+
// matcher by concatenating the resolved absolute base path in front of the glob
|
|
133
|
+
// (`fs-jetpack/lib/utils/matcher.js` → `convertPatternToAbsolutePath`). On Windows
|
|
134
|
+
// that base is backslash-separated and minimatch reads a backslash inside a
|
|
135
|
+
// PATTERN as an escape, so `D:\…\core/**/*.ts` collapses to `D:coresrc/**/*.ts`
|
|
136
|
+
// and matches nothing — minimatch normalises separators on the file side only.
|
|
137
|
+
// The strip then reported success while the TS2310 augmentation stayed in every
|
|
138
|
+
// Windows-converted project. `glob` keeps pattern and base directory apart and
|
|
139
|
+
// normalises separators itself. Same fix as `api-mode.ts`'s `globFiles`. glob does
|
|
140
|
+
// not follow symlinked directories, unlike jetpack — which is the direction the
|
|
141
|
+
// guard above already wants: nothing inside a linked checkout gets rewritten.
|
|
142
|
+
for (const file of (0, glob_1.globSync)('**/*.ts', { absolute: true, cwd: coreDir, dot: true, nodir: true })) {
|
|
132
143
|
const content = filesystem.read(file);
|
|
133
144
|
if (!content || !content.includes('PublicRuntimeConfig'))
|
|
134
145
|
continue;
|
|
@@ -33,6 +33,7 @@ exports.healVendorClaudeMd = healVendorClaudeMd;
|
|
|
33
33
|
exports.insertVendorBlockIfMissing = insertVendorBlockIfMissing;
|
|
34
34
|
exports.removeVendorBlock = removeVendorBlock;
|
|
35
35
|
exports.upsertVendorBlock = upsertVendorBlock;
|
|
36
|
+
const path_1 = require("path");
|
|
36
37
|
exports.BACKEND_VENDOR_MARKER = '<!-- lt-vendor-marker -->';
|
|
37
38
|
exports.FRONTEND_VENDOR_MARKER = '<!-- lt-vendor-marker-frontend -->';
|
|
38
39
|
exports.ROOT_VENDOR_MARKER = '<!-- lt-vendor-marker-root -->';
|
|
@@ -182,15 +183,20 @@ function healVendorClaudeMd(fs, state) {
|
|
|
182
183
|
changed.push(path);
|
|
183
184
|
}
|
|
184
185
|
};
|
|
186
|
+
// `join`, not string concatenation with `/`: the returned paths are reported
|
|
187
|
+
// to the user and compared by callers, so they have to be the same spelling
|
|
188
|
+
// the rest of the CLI produces. Concatenating `/` onto a Windows directory
|
|
189
|
+
// yields `C:\ws\projects\api/CLAUDE.md` — it opens fine, but it matches
|
|
190
|
+
// nothing and reads like a bug in the output.
|
|
185
191
|
if (state.apiDir) {
|
|
186
|
-
apply(
|
|
192
|
+
apply((0, path_1.join)(state.apiDir, 'CLAUDE.md'), exports.BACKEND_VENDOR_MARKER, state.backendVendor ? buildBackendVendorBlock() : null);
|
|
187
193
|
}
|
|
188
194
|
if (state.appDir) {
|
|
189
|
-
apply(
|
|
195
|
+
apply((0, path_1.join)(state.appDir, 'CLAUDE.md'), exports.FRONTEND_VENDOR_MARKER, state.frontendVendor ? buildFrontendVendorBlock() : null);
|
|
190
196
|
}
|
|
191
197
|
if (state.workspaceRoot) {
|
|
192
198
|
const anyVendor = state.backendVendor || state.frontendVendor;
|
|
193
|
-
apply(
|
|
199
|
+
apply((0, path_1.join)(state.workspaceRoot, 'CLAUDE.md'), exports.ROOT_VENDOR_MARKER, anyVendor ? buildRootVendorBlock({ backend: state.backendVendor, frontend: state.frontendVendor }) : null);
|
|
194
200
|
}
|
|
195
201
|
return changed;
|
|
196
202
|
}
|
|
@@ -242,6 +248,3 @@ function blockRegex(marker) {
|
|
|
242
248
|
function escapeRegExp(value) {
|
|
243
249
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
244
250
|
}
|
|
245
|
-
function joinPath(dir, file) {
|
|
246
|
-
return dir.endsWith('/') ? `${dir}${file}` : `${dir}/${file}`;
|
|
247
|
-
}
|
|
@@ -81,6 +81,7 @@ exports.runStandaloneWorkspaceGate = runStandaloneWorkspaceGate;
|
|
|
81
81
|
exports.shouldProceedAsStandalone = shouldProceedAsStandalone;
|
|
82
82
|
exports.writeApiConfig = writeApiConfig;
|
|
83
83
|
const yaml = __importStar(require("js-yaml"));
|
|
84
|
+
const path_1 = require("path");
|
|
84
85
|
const ensure_root_dockerignore_1 = require("./ensure-root-dockerignore");
|
|
85
86
|
const hoist_workspace_pnpm_config_1 = require("./hoist-workspace-pnpm-config");
|
|
86
87
|
const remove_nested_lockfiles_1 = require("./remove-nested-lockfiles");
|
|
@@ -104,11 +105,20 @@ function detectSubProjectContext(startDir, filesystem) {
|
|
|
104
105
|
const appDir = filesystem.path(root, 'projects', 'app');
|
|
105
106
|
// Resolve absolute prefixes for a clean startsWith compare. We use
|
|
106
107
|
// the gluegun-resolved paths because all callers go through it.
|
|
108
|
+
//
|
|
109
|
+
// The prefix MUST carry the OS separator, not a literal `/`:
|
|
110
|
+
// `filesystem.path` resolves through Node's `path`, so on Windows both
|
|
111
|
+
// sides come back with backslashes and an `${apiDir}/` prefix can never
|
|
112
|
+
// match. Every cwd below `projects\api\` then read as "not a
|
|
113
|
+
// sub-project", and the standalone gate in `runStandaloneWorkspaceGate`
|
|
114
|
+
// waved the user through instead of pointing them at the workspace root
|
|
115
|
+
// — the exact case (cloning a sibling tree INSIDE an existing
|
|
116
|
+
// sub-project) the check exists to prevent.
|
|
107
117
|
const startAbs = filesystem.path(startDir);
|
|
108
|
-
if (startAbs === apiDir || startAbs.startsWith(
|
|
118
|
+
if (startAbs === apiDir || startAbs.startsWith(apiDir + path_1.sep)) {
|
|
109
119
|
return { kind: 'api', subProjectDir: apiDir, workspaceRoot: root };
|
|
110
120
|
}
|
|
111
|
-
if (startAbs === appDir || startAbs.startsWith(
|
|
121
|
+
if (startAbs === appDir || startAbs.startsWith(appDir + path_1.sep)) {
|
|
112
122
|
return { kind: 'app', subProjectDir: appDir, workspaceRoot: root };
|
|
113
123
|
}
|
|
114
124
|
return null;
|