@lenne.tech/cli 1.45.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.
@@ -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 ((0, claude_cli_1.checkCommandExists)(req.checkCommand)) {
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 ((0, claude_cli_1.checkCommandExists)(req.checkCommand)) {
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 spawnSync (no shell interpretation)
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, child_process_1.spawnSync)(cmd, args, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
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
  }