@lenne.tech/cli 1.46.0 → 1.47.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/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/heal-check-wrapper.js +128 -11
- package/build/lib/heal-oxlintrc.js +215 -0
- package/build/lib/platform.js +107 -0
- package/build/lib/plugin-utils.js +9 -8
- 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,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findDangerousFixFlagUsage = findDangerousFixFlagUsage;
|
|
4
|
+
exports.healDangerousOxlintFixFlags = healDangerousOxlintFixFlags;
|
|
5
|
+
exports.healOxlintrcFilename = healOxlintrcFilename;
|
|
6
|
+
const child_process_1 = require("child_process");
|
|
7
|
+
const fs_1 = require("fs");
|
|
8
|
+
const path_1 = require("path");
|
|
9
|
+
const LEGACY = 'oxlint.json';
|
|
10
|
+
const TARGET = '.oxlintrc.json';
|
|
11
|
+
/** A `-c`/`--config` flag pointing at the legacy file, e.g. `-c oxlint.json`, `--config=./oxlint.json`. */
|
|
12
|
+
const CONFIG_FLAG = /(-c|--config)(\s+|=)(\.\/)?oxlint\.json(?![\w.-])/g;
|
|
13
|
+
/**
|
|
14
|
+
* oxlint flags that apply behaviour-changing fixes: `--fix-suggestions` (e.g. deletes
|
|
15
|
+
* console calls for no-console) and `--fix-dangerously`. Matches a USE of the flag —
|
|
16
|
+
* preceded by whitespace or a quote — not a mention in backticks such as the comment
|
|
17
|
+
* "Never `--fix-suggestions`" the current templates carry.
|
|
18
|
+
*/
|
|
19
|
+
const DANGEROUS_FIX_FLAG_USE = /(?:^|[\s'"])--fix-(?:suggestions|dangerously)(?![\w-])/m;
|
|
20
|
+
/** The removable form: the flag as a further argument after whitespace. `--fix` itself stays. */
|
|
21
|
+
const DANGEROUS_FIX_FLAG_ARG = /[ \t]+--fix-(?:suggestions|dangerously)(?![\w-])/g;
|
|
22
|
+
const LINT_STAGED_FILES = [
|
|
23
|
+
'.lintstagedrc',
|
|
24
|
+
'.lintstagedrc.json',
|
|
25
|
+
'.lintstagedrc.yaml',
|
|
26
|
+
'.lintstagedrc.yml',
|
|
27
|
+
'.lintstagedrc.js',
|
|
28
|
+
'.lintstagedrc.cjs',
|
|
29
|
+
'.lintstagedrc.mjs',
|
|
30
|
+
'lint-staged.config.js',
|
|
31
|
+
'lint-staged.config.cjs',
|
|
32
|
+
'lint-staged.config.mjs',
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* Files in which a dangerous fix flag is still USED. The root `scripts/check.mjs` counts
|
|
36
|
+
* for the gate but is not stripped here — `healCheckWrapper` owns that file.
|
|
37
|
+
* Paths are relative to `workspaceRoot` (or `appDir` for a standalone app).
|
|
38
|
+
*/
|
|
39
|
+
function findDangerousFixFlagUsage(appDir, workspaceRoot) {
|
|
40
|
+
const base = workspaceRoot !== null && workspaceRoot !== void 0 ? workspaceRoot : appDir;
|
|
41
|
+
const files = [...(workspaceRoot ? [(0, path_1.join)(workspaceRoot, 'scripts', 'check.mjs')] : []), ...fixFlagFiles(appDir, workspaceRoot)];
|
|
42
|
+
return [...new Set(files)].filter((file) => usesDangerousFixFlag(file)).map((file) => (0, path_1.relative)(base, file) || file);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Remove `--fix-suggestions` and `--fix-dangerously` from the project's lint commands
|
|
46
|
+
* — root and app `package.json` (scripts and inline lint-staged), lint-staged configs
|
|
47
|
+
* and the app's own `scripts/check.mjs` — so the oxlint config can be loaded safely.
|
|
48
|
+
*
|
|
49
|
+
* Textual, so formatting stays exactly as it was. Only tracked files without
|
|
50
|
+
* uncommitted changes are edited (git can restore them); an untracked, dirty or
|
|
51
|
+
* symlinked file is reported in `skipped` and keeps blocking the rename.
|
|
52
|
+
*/
|
|
53
|
+
function healDangerousOxlintFixFlags(appDir, workspaceRoot) {
|
|
54
|
+
const base = workspaceRoot !== null && workspaceRoot !== void 0 ? workspaceRoot : appDir;
|
|
55
|
+
const result = { changed: [], skipped: [] };
|
|
56
|
+
for (const file of fixFlagFiles(appDir, workspaceRoot)) {
|
|
57
|
+
if (!usesDangerousFixFlag(file)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const rel = (0, path_1.relative)(base, file) || file;
|
|
61
|
+
const dir = (0, path_1.join)(file, '..');
|
|
62
|
+
const name = (0, path_1.relative)(dir, file);
|
|
63
|
+
if (isSymlink(file)) {
|
|
64
|
+
result.skipped.push(`${rel} (symlink)`);
|
|
65
|
+
}
|
|
66
|
+
else if (!isTracked(dir, name)) {
|
|
67
|
+
result.skipped.push(`${rel} (not tracked by git — remove the flag by hand)`);
|
|
68
|
+
}
|
|
69
|
+
else if (hasUncommittedChanges(dir, name)) {
|
|
70
|
+
result.skipped.push(`${rel} (uncommitted changes — commit or discard them, then re-run)`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const content = (0, fs_1.readFileSync)(file, 'utf8');
|
|
74
|
+
const stripped = content.replace(DANGEROUS_FIX_FLAG_ARG, '');
|
|
75
|
+
if (stripped !== content) {
|
|
76
|
+
(0, fs_1.writeFileSync)(file, stripped);
|
|
77
|
+
result.changed.push(rel);
|
|
78
|
+
}
|
|
79
|
+
if (DANGEROUS_FIX_FLAG_USE.test(stripped)) {
|
|
80
|
+
result.skipped.push(`${rel} (a flag use that is not a plain argument remains — remove it by hand)`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Heal the oxlint config filename of an app project: `oxlint.json` → `.oxlintrc.json`.
|
|
88
|
+
*
|
|
89
|
+
* oxlint only auto-discovers `.oxlintrc.json`. nuxt-base-template and
|
|
90
|
+
* nuxt-extensions shipped the config as `oxlint.json`, so every rule in it was
|
|
91
|
+
* silently ignored — projects linted with oxlint's defaults only. The templates
|
|
92
|
+
* renamed the file; existing projects keep the old name until this runs.
|
|
93
|
+
*
|
|
94
|
+
* Loading the config is not neutral. It turns on rules such as `no-console`, and
|
|
95
|
+
* an auto-fix run with `--fix-suggestions` applies that rule's suggestion, which
|
|
96
|
+
* deletes console calls. So the rename is GATED: it only happens once no check
|
|
97
|
+
* wrapper, script or lint-staged entry of the project still passes
|
|
98
|
+
* `--fix-suggestions`. Run it after `healCheckWrapper`, which removes the flag
|
|
99
|
+
* from the root wrapper in the same update.
|
|
100
|
+
*
|
|
101
|
+
* Same recoverability rules as the other heals: never through a symlink, never
|
|
102
|
+
* over uncommitted changes to the file, `git mv` for a tracked file so history
|
|
103
|
+
* follows the rename.
|
|
104
|
+
*
|
|
105
|
+
* @param appDir The app project (e.g. `<workspace>/projects/app`).
|
|
106
|
+
* @param workspaceRoot The monorepo root, whose `scripts/check.mjs` and
|
|
107
|
+
* `package.json` also gate the rename. Omit for a standalone app.
|
|
108
|
+
*/
|
|
109
|
+
function healOxlintrcFilename(appDir, workspaceRoot) {
|
|
110
|
+
const legacy = (0, path_1.join)(appDir, LEGACY);
|
|
111
|
+
const target = (0, path_1.join)(appDir, TARGET);
|
|
112
|
+
if (!(0, fs_1.existsSync)(legacy) && !isSymlink(legacy)) {
|
|
113
|
+
return { action: 'none', changed: [] };
|
|
114
|
+
}
|
|
115
|
+
if ((0, fs_1.existsSync)(target) || isSymlink(target)) {
|
|
116
|
+
return {
|
|
117
|
+
action: 'both-present',
|
|
118
|
+
changed: [],
|
|
119
|
+
detail: `both ${LEGACY} and ${TARGET} exist — oxlint only loads ${TARGET}; merge the rules from ${LEGACY} by hand, then delete it`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (isSymlink(legacy)) {
|
|
123
|
+
return { action: 'skipped', changed: [], detail: `${LEGACY} is a symlink — rename it by hand` };
|
|
124
|
+
}
|
|
125
|
+
const withFlag = findDangerousFixFlagUsage(appDir, workspaceRoot);
|
|
126
|
+
if (withFlag.length > 0) {
|
|
127
|
+
return {
|
|
128
|
+
action: 'skipped',
|
|
129
|
+
changed: [],
|
|
130
|
+
detail: `--fix-suggestions/--fix-dangerously is still used in ${withFlag.join(', ')}; loading the config would let it delete console calls — remove the flag, then re-run`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const tracked = isTracked(appDir, LEGACY);
|
|
134
|
+
if (tracked && hasUncommittedChanges(appDir, LEGACY)) {
|
|
135
|
+
return {
|
|
136
|
+
action: 'skipped',
|
|
137
|
+
changed: [],
|
|
138
|
+
detail: `${LEGACY} has uncommitted changes — commit or discard them, then re-run`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (tracked) {
|
|
142
|
+
(0, child_process_1.execFileSync)('git', ['-C', appDir, 'mv', '--', LEGACY, TARGET], { stdio: 'ignore' });
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// Nothing is overwritten (the target does not exist), so an untracked file
|
|
146
|
+
// keeps its content under the new name — no backup needed.
|
|
147
|
+
(0, fs_1.renameSync)(legacy, target);
|
|
148
|
+
}
|
|
149
|
+
const changed = [`${LEGACY} → ${TARGET}`];
|
|
150
|
+
const pkgPath = (0, path_1.join)(appDir, 'package.json');
|
|
151
|
+
if ((0, fs_1.existsSync)(pkgPath) && !isSymlink(pkgPath)) {
|
|
152
|
+
const pkg = (0, fs_1.readFileSync)(pkgPath, 'utf8');
|
|
153
|
+
// Textual, not JSON round-trip: only the flag value changes, the file keeps its formatting.
|
|
154
|
+
const updated = pkg.replace(CONFIG_FLAG, (_match, flag, sep, dot = '') => `${flag}${sep}${dot}${TARGET}`);
|
|
155
|
+
if (updated !== pkg) {
|
|
156
|
+
(0, fs_1.writeFileSync)(pkgPath, updated);
|
|
157
|
+
changed.push('package.json');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { action: 'renamed', changed };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Files that may pass a dangerous fix flag and are ours to strip: both package.json
|
|
164
|
+
* files (scripts and inline lint-staged), lint-staged configs, and the app's own
|
|
165
|
+
* check wrapper.
|
|
166
|
+
*/
|
|
167
|
+
function fixFlagFiles(appDir, workspaceRoot) {
|
|
168
|
+
const roots = [...new Set([workspaceRoot, appDir].filter((dir) => Boolean(dir)))];
|
|
169
|
+
return [
|
|
170
|
+
...roots.flatMap((dir) => ['package.json', ...LINT_STAGED_FILES].map((name) => (0, path_1.join)(dir, name))),
|
|
171
|
+
(0, path_1.join)(appDir, 'scripts', 'check.mjs'),
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
/** True when the TRACKED `relPath` has uncommitted modifications. */
|
|
175
|
+
function hasUncommittedChanges(dir, relPath) {
|
|
176
|
+
try {
|
|
177
|
+
const out = (0, child_process_1.execFileSync)('git', ['-C', dir, 'status', '--porcelain', '--', relPath], {
|
|
178
|
+
encoding: 'utf8',
|
|
179
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
180
|
+
});
|
|
181
|
+
return out.trim().length > 0;
|
|
182
|
+
}
|
|
183
|
+
catch (_a) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** True when `path` is a symlink (checked without following it). */
|
|
188
|
+
function isSymlink(path) {
|
|
189
|
+
try {
|
|
190
|
+
return (0, fs_1.lstatSync)(path).isSymbolicLink();
|
|
191
|
+
}
|
|
192
|
+
catch (_a) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** True when git tracks `relPath`; false outside a repository as well. */
|
|
197
|
+
function isTracked(dir, relPath) {
|
|
198
|
+
try {
|
|
199
|
+
(0, child_process_1.execFileSync)('git', ['-C', dir, 'ls-files', '--error-unmatch', '--', relPath], { stdio: 'ignore' });
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
catch (_a) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function usesDangerousFixFlag(file) {
|
|
207
|
+
try {
|
|
208
|
+
// Follows a symlink on purpose: a linked config still feeds the lint run, so it
|
|
209
|
+
// must block the rename, even though the strip refuses to write through it.
|
|
210
|
+
return DANGEROUS_FIX_FLAG_USE.test((0, fs_1.readFileSync)(file, 'utf8'));
|
|
211
|
+
}
|
|
212
|
+
catch (_a) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -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
|
}
|