@livedesk/client 0.1.249 → 0.1.251

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.
@@ -7,7 +7,8 @@ import crypto from 'crypto';
7
7
  import { existsSync, promises as fs, statfsSync } from 'fs';
8
8
  import { spawn } from 'child_process';
9
9
  import { createRequire } from 'node:module';
10
- import { fileURLToPath } from 'node:url';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { resolveAgentShellCommand } from '../src/runtime/agent-shell.js';
11
12
 
12
13
  const require = createRequire(import.meta.url);
13
14
  const CLIENT_UPDATE_BOOTSTRAP_PATH = fileURLToPath(
@@ -1142,9 +1143,10 @@ async function resolveAgentPath(options, value, permissionMode, rejectSensitive
1142
1143
  return resolved;
1143
1144
  }
1144
1145
 
1145
- function runNodeAgentProcess(executable, args, timeoutMs = 15000, cwd = undefined) {
1146
- return new Promise((resolve, reject) => {
1147
- const child = spawn(executable, args, { cwd, windowsHide: true, shell: false, stdio: ['ignore', 'pipe', 'pipe'] });
1146
+ function runNodeAgentProcess(executable, args, timeoutMs = 15000, cwd = undefined, maxTimeoutMs = 30000) {
1147
+ return new Promise((resolve, reject) => {
1148
+ const startedAt = Date.now();
1149
+ const child = spawn(executable, args, { cwd, windowsHide: true, shell: false, stdio: ['ignore', 'pipe', 'pipe'] });
1148
1150
  let output = '';
1149
1151
  let timedOut = false;
1150
1152
  const append = chunk => { output = `${output}${String(chunk || '')}`.replace(/[\0\r]/g, ' ').slice(0, MAX_AGENT_OUTPUT_CHARS); };
@@ -1154,11 +1156,55 @@ function runNodeAgentProcess(executable, args, timeoutMs = 15000, cwd = undefine
1154
1156
  timedOut = true;
1155
1157
  child.kill('SIGTERM');
1156
1158
  setTimeout(() => child.kill('SIGKILL'), 1000).unref?.();
1157
- }, Math.max(1000, Math.min(30000, Number(timeoutMs) || 15000)));
1158
- child.once('error', error => { clearTimeout(timer); reject(error); });
1159
- child.once('close', code => { clearTimeout(timer); resolve({ output: redactAgentOutput(`${output}${timedOut ? '\n[timeout]' : ''}`.trim()), exitCode: timedOut ? -1 : code ?? -1, timedOut }); });
1160
- });
1161
- }
1159
+ }, Math.max(1000, Math.min(maxTimeoutMs, Number(timeoutMs) || 15000)));
1160
+ child.once('error', error => { clearTimeout(timer); reject(error); });
1161
+ child.once('close', code => { clearTimeout(timer); resolve({ output: redactAgentOutput(`${output}${timedOut ? '\n[timeout]' : ''}`.trim()), exitCode: timedOut ? -1 : code ?? -1, timedOut, durationMs: Date.now() - startedAt }); });
1162
+ });
1163
+ }
1164
+
1165
+ async function searchNodeAgentFiles(options, args, permissionMode) {
1166
+ const root = await resolveAgentPath(options, args.path, permissionMode);
1167
+ const query = String(args.query || '').trim().toLowerCase();
1168
+ if (!query || query.length > 160 || /[\0\r\n]/.test(query)) throw new Error('search query is invalid');
1169
+ const maxResults = Math.max(1, Math.min(200, Number(args.maxResults) || 100));
1170
+ const maxDepth = Math.max(0, Math.min(8, Number(args.maxDepth) || 4));
1171
+ const maxScannedEntries = Math.max(maxResults, Math.min(5000, Number(args.maxScannedEntries) || 2000));
1172
+ const queue = [{ directory: root, depth: 0 }];
1173
+ const matches = [];
1174
+ let scannedEntries = 0;
1175
+ while (queue.length > 0 && matches.length < maxResults && scannedEntries < maxScannedEntries) {
1176
+ const current = queue.shift();
1177
+ let entries;
1178
+ try {
1179
+ entries = await fs.readdir(current.directory, { withFileTypes: true });
1180
+ } catch {
1181
+ continue;
1182
+ }
1183
+ for (const entry of entries) {
1184
+ if (matches.length >= maxResults || scannedEntries >= maxScannedEntries) break;
1185
+ scannedEntries += 1;
1186
+ const entryPath = path.join(current.directory, entry.name);
1187
+ if (isSensitiveAgentPath(entryPath)) continue;
1188
+ const isDirectory = entry.isDirectory();
1189
+ if (entry.name.toLowerCase().includes(query)) {
1190
+ matches.push({ name: entry.name, path: entryPath, type: isDirectory ? 'directory' : 'file' });
1191
+ }
1192
+ if (isDirectory && current.depth < maxDepth && !entry.isSymbolicLink()) {
1193
+ queue.push({ directory: entryPath, depth: current.depth + 1 });
1194
+ }
1195
+ }
1196
+ }
1197
+ return {
1198
+ summary: `Found ${matches.length} matching path${matches.length === 1 ? '' : 's'}.`,
1199
+ data: {
1200
+ path: root,
1201
+ query,
1202
+ matches,
1203
+ scannedEntries,
1204
+ truncated: matches.length >= maxResults || scannedEntries >= maxScannedEntries
1205
+ }
1206
+ };
1207
+ }
1162
1208
 
1163
1209
  function redactAgentOutput(value) {
1164
1210
  let output = String(value || '').replace(/[\0\r]/g, ' ');
@@ -1324,7 +1370,7 @@ async function executeNodeAgentOperation(options, operation, payload = {}) {
1324
1370
  else await fs.unlink(filePath);
1325
1371
  return { summary: `Deleted ${path.basename(filePath)}.`, data: { path: filePath, recursive: args.recursive === true } };
1326
1372
  }
1327
- if (operation === 'file.list') {
1373
+ if (operation === 'file.list') {
1328
1374
  const directory = await resolveAgentPath(options, args.path, permissionMode);
1329
1375
  const entries = await fs.readdir(directory, { withFileTypes: true });
1330
1376
  const maxEntries = Math.max(1, Math.min(500, Number(args.maxEntries) || 200));
@@ -1334,8 +1380,15 @@ async function executeNodeAgentOperation(options, operation, payload = {}) {
1334
1380
  if (isSensitiveAgentPath(entryPath)) continue;
1335
1381
  data.push({ name: entry.name, path: entryPath, type: entry.isDirectory() ? 'directory' : 'file', sizeBytes: entry.isDirectory() ? 0 : (await fs.stat(entryPath)).size });
1336
1382
  }
1337
- return { summary: `Listed ${data.length} entries.`, data: { path: directory, entries: data, truncated: entries.length > maxEntries } };
1338
- }
1383
+ return { summary: `Listed ${data.length} entries.`, data: { path: directory, entries: data, truncated: entries.length > maxEntries } };
1384
+ }
1385
+ if (operation === 'file.search') return searchNodeAgentFiles(options, args, permissionMode);
1386
+ if (operation === 'directory.create') {
1387
+ let directory = await resolveAgentPath(options, args.path, permissionMode);
1388
+ await fs.mkdir(directory, { recursive: true });
1389
+ directory = await resolveAgentPath(options, args.path, permissionMode);
1390
+ return { summary: `Created directory ${path.basename(directory)}.`, data: { path: directory, created: true } };
1391
+ }
1339
1392
  if (operation === 'application.launch') {
1340
1393
  const executable = String(args.executable || '').trim();
1341
1394
  if (!executable || executable.length > 400 || /[\0\r\n]/.test(executable)) throw new Error('executable is invalid');
@@ -1389,12 +1442,13 @@ async function executeNodeAgentOperation(options, operation, payload = {}) {
1389
1442
  if (!ok) throw new Error(`Service ${action} failed for ${service}.`);
1390
1443
  return { summary: `Service ${action} completed for ${service}.`, data: { service, action, results } };
1391
1444
  }
1392
- if (operation === 'command.run') {
1393
- const command = String(args.command || '');
1394
- const executable = process.platform === 'win32' ? 'cmd.exe' : '/bin/sh';
1395
- const commandArgs = process.platform === 'win32' ? ['/d', '/s', '/c', command] : ['-lc', command];
1396
- const result = await runNodeAgentProcess(executable, commandArgs, args.timeoutMs, args.workingDirectory ? await resolveAgentPath(options, args.workingDirectory, permissionMode, false) : undefined);
1397
- return { summary: result.timedOut ? 'Command timed out.' : `Command exited with code ${result.exitCode}.`, data: { ...result, output: redactAgentOutput(result.output) } };
1445
+ if (operation === 'command.run') {
1446
+ const command = String(args.command || '');
1447
+ if (!command || command.length > 16000 || /\0/.test(command)) throw new Error('command is invalid');
1448
+ const shellCommand = resolveAgentShellCommand({ shell: args.shell, command });
1449
+ const workingDirectory = args.workingDirectory ? await resolveAgentPath(options, args.workingDirectory, permissionMode, false) : undefined;
1450
+ const result = await runNodeAgentProcess(shellCommand.executable, shellCommand.args, args.timeoutMs, workingDirectory, 300000);
1451
+ return { summary: result.timedOut ? `${shellCommand.shell} command timed out.` : `${shellCommand.shell} command exited with code ${result.exitCode}.`, data: { ...result, output: redactAgentOutput(result.output), shell: shellCommand.shell, workingDirectory: workingDirectory || process.cwd() } };
1398
1452
  }
1399
1453
  if (operation === 'script.run') {
1400
1454
  const scriptPath = await resolveAgentPath(options, args.path, permissionMode);
@@ -1481,7 +1535,7 @@ function normalizeNodeAgentTaskResult(result) {
1481
1535
  return { ...result, ok, status: ok ? 'completed' : 'failed', error };
1482
1536
  }
1483
1537
 
1484
- const NODE_AGENT_OPERATIONS = new Set(['process.control', 'service.control', 'application.launch', 'application.close', 'file.read', 'file.write', 'file.delete', 'file.list', 'command.run', 'script.run', 'software.install', 'network.status', 'system.power', 'system.configure', 'logs.collect']);
1538
+ const NODE_AGENT_OPERATIONS = new Set(['process.control', 'service.control', 'application.launch', 'application.close', 'file.read', 'file.write', 'file.delete', 'file.list', 'file.search', 'directory.create', 'command.run', 'script.run', 'software.install', 'network.status', 'system.power', 'system.configure', 'logs.collect']);
1485
1539
 
1486
1540
  function remotePolicyAllows(options, command) {
1487
1541
  const policy = options.effectivePolicy;
@@ -1723,7 +1777,7 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
1723
1777
  summary: result.summary,
1724
1778
  error: result.error || undefined,
1725
1779
  data: result.data,
1726
- sideEffects: ['file.read', 'file.list', 'network.status', 'logs.collect'].includes(command) ? 'none' : 'audited',
1780
+ sideEffects: ['file.read', 'file.list', 'file.search', 'network.status', 'logs.collect'].includes(command) ? 'none' : 'audited',
1727
1781
  completedAt: new Date().toISOString()
1728
1782
  }
1729
1783
  });
@@ -19,8 +19,8 @@ import {
19
19
  signalAgentTree
20
20
  } from '../src/runtime/agent-process-lifecycle.js';
21
21
  import { writeWindowsOwnedProcessManifest } from '../src/runtime/windows-owned-process-manifest.js';
22
- import { createHubWakeListener } from '../src/runtime/hub-wake-listener.js';
23
- import { resolveSpawnablePackagedPath } from '../src/runtime/packaged-executable-path.js';
22
+ import { createHubWakeListener } from '../src/runtime/hub-wake-listener.js';
23
+ import { resolveSpawnablePackagedPath } from '../src/runtime/packaged-executable-path.js';
24
24
  import {
25
25
  inspectLinuxVideoAcceleration,
26
26
  installLinuxVideoAcceleration
@@ -588,7 +588,7 @@ export function resolveHubClientPort(env = process.env) {
588
588
  return normalizePort(env.REMOTE_HUB_PORT || env.LIVEDESK_HUB_REMOTE_PORT) || DEFAULT_HUB_CLIENT_PORT;
589
589
  }
590
590
 
591
- export function preflightHubClientPort(port = resolveHubClientPort()) {
591
+ export function preflightHubClientPort(port = resolveHubClientPort()) {
592
592
  const normalizedPort = normalizePort(port);
593
593
  if (!normalizedPort) {
594
594
  return Promise.resolve({ ok: false, port: Number(port) || 0, code: 'invalid-hub-client-port' });
@@ -618,46 +618,46 @@ export function preflightHubClientPort(port = resolveHubClientPort()) {
618
618
  finish({ ok: true, code: 'ok' });
619
619
  });
620
620
  });
621
- });
622
- }
623
-
624
- export async function recoverHubClientPortConflict(preflight, recoverPortOwner) {
625
- if (preflight?.ok
626
- || preflight?.code !== 'hub-client-port-in-use'
627
- || typeof recoverPortOwner !== 'function') {
628
- return preflight;
629
- }
630
-
631
- try {
632
- await recoverPortOwner(Number(preflight.port || resolveHubClientPort()));
633
- } catch (error) {
634
- return {
635
- ...preflight,
636
- recoveryAttempted: true,
637
- recoveryError: error instanceof Error ? error.message : String(error)
638
- };
639
- }
640
-
641
- const verified = await preflightHubClientPort(preflight.port);
642
- return {
643
- ...verified,
644
- recoveryAttempted: true,
645
- recovered: verified.ok === true
646
- };
647
- }
648
-
649
- function hubClientPortPreflightError(preflight) {
650
- const port = Number(preflight?.port || resolveHubClientPort());
651
- const recoveryError = String(preflight?.recoveryError || '').trim();
652
- return {
653
- ok: false,
654
- code: String(preflight?.code || 'hub-client-port-unavailable'),
655
- port,
656
- error: recoveryError
657
- ? `${recoveryError} The current Hub was not changed.`
658
- : preflight?.code === 'hub-client-port-in-use'
659
- ? `LiveDesk cannot switch this computer to Hub because TCP ${port} is already in use. Change or stop the owning service, then try Switch to Hub again. The current Hub was not changed.`
660
- : `LiveDesk cannot switch this computer to Hub because TCP ${port} is unavailable. Check the local port configuration, then try again. The current Hub was not changed.`
621
+ });
622
+ }
623
+
624
+ export async function recoverHubClientPortConflict(preflight, recoverPortOwner) {
625
+ if (preflight?.ok
626
+ || preflight?.code !== 'hub-client-port-in-use'
627
+ || typeof recoverPortOwner !== 'function') {
628
+ return preflight;
629
+ }
630
+
631
+ try {
632
+ await recoverPortOwner(Number(preflight.port || resolveHubClientPort()));
633
+ } catch (error) {
634
+ return {
635
+ ...preflight,
636
+ recoveryAttempted: true,
637
+ recoveryError: error instanceof Error ? error.message : String(error)
638
+ };
639
+ }
640
+
641
+ const verified = await preflightHubClientPort(preflight.port);
642
+ return {
643
+ ...verified,
644
+ recoveryAttempted: true,
645
+ recovered: verified.ok === true
646
+ };
647
+ }
648
+
649
+ function hubClientPortPreflightError(preflight) {
650
+ const port = Number(preflight?.port || resolveHubClientPort());
651
+ const recoveryError = String(preflight?.recoveryError || '').trim();
652
+ return {
653
+ ok: false,
654
+ code: String(preflight?.code || 'hub-client-port-unavailable'),
655
+ port,
656
+ error: recoveryError
657
+ ? `${recoveryError} The current Hub was not changed.`
658
+ : preflight?.code === 'hub-client-port-in-use'
659
+ ? `LiveDesk cannot switch this computer to Hub because TCP ${port} is already in use. Change or stop the owning service, then try Switch to Hub again. The current Hub was not changed.`
660
+ : `LiveDesk cannot switch this computer to Hub because TCP ${port} is unavailable. Check the local port configuration, then try again. The current Hub was not changed.`
661
661
  };
662
662
  }
663
663
 
@@ -3110,16 +3110,16 @@ async function startConnectionChoiceServer(supabase, options = {}) {
3110
3110
  res.end(JSON.stringify({ ok: false, error: 'client-can-only-transition-to-hub' }));
3111
3111
  return;
3112
3112
  }
3113
- const portPreflight = await preflightHubClientPort();
3114
- const readyPortPreflight = await recoverHubClientPortConflict(
3115
- portPreflight,
3116
- options.recoverHubClientPort
3117
- );
3118
- if (!readyPortPreflight.ok) {
3119
- res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
3120
- res.end(JSON.stringify(hubClientPortPreflightError(readyPortPreflight)));
3121
- return;
3122
- }
3113
+ const portPreflight = await preflightHubClientPort();
3114
+ const readyPortPreflight = await recoverHubClientPortConflict(
3115
+ portPreflight,
3116
+ options.recoverHubClientPort
3117
+ );
3118
+ if (!readyPortPreflight.ok) {
3119
+ res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
3120
+ res.end(JSON.stringify(hubClientPortPreflightError(readyPortPreflight)));
3121
+ return;
3122
+ }
3123
3123
  const session = await refreshSessionIfNeeded(supabase);
3124
3124
  const expectedRoleVersion = Number(dashboardState.roleVersion || 0);
3125
3125
  const { data, error } = await supabase.rpc('set_livedesk_device_role', {
@@ -3435,14 +3435,14 @@ async function chooseClientConnection(supabase, options = {}) {
3435
3435
  if (!activeSupabase) {
3436
3436
  return { ok: false, error: 'supabase-session-required' };
3437
3437
  }
3438
- const portPreflight = await preflightHubClientPort();
3439
- const readyPortPreflight = await recoverHubClientPortConflict(
3440
- portPreflight,
3441
- options.recoverHubClientPort
3442
- );
3443
- if (!readyPortPreflight.ok) {
3444
- return hubClientPortPreflightError(readyPortPreflight);
3445
- }
3438
+ const portPreflight = await preflightHubClientPort();
3439
+ const readyPortPreflight = await recoverHubClientPortConflict(
3440
+ portPreflight,
3441
+ options.recoverHubClientPort
3442
+ );
3443
+ if (!readyPortPreflight.ok) {
3444
+ return hubClientPortPreflightError(readyPortPreflight);
3445
+ }
3446
3446
  const session = await refreshSessionIfNeeded(activeSupabase);
3447
3447
  if (!session?.access_token) {
3448
3448
  return { ok: false, error: 'supabase-session-required' };
@@ -4465,14 +4465,14 @@ function getFastRuntime() {
4465
4465
  const arch = os.arch();
4466
4466
  const resolvePackagedRuntime = (packageName, rid, executableName) => {
4467
4467
  try {
4468
- const packagePath = require.resolve(`${packageName}/package.json`);
4469
- const fastRoot = join(dirname(packagePath), 'fast');
4470
- return {
4471
- rid,
4472
- packageName,
4473
- executable: resolveSpawnablePackagedPath(join(fastRoot, executableName)),
4474
- dll: resolveSpawnablePackagedPath(join(fastRoot, 'livedesk-client-fast.dll'))
4475
- };
4468
+ const packagePath = require.resolve(`${packageName}/package.json`);
4469
+ const fastRoot = join(dirname(packagePath), 'fast');
4470
+ return {
4471
+ rid,
4472
+ packageName,
4473
+ executable: resolveSpawnablePackagedPath(join(fastRoot, executableName)),
4474
+ dll: resolveSpawnablePackagedPath(join(fastRoot, 'livedesk-client-fast.dll'))
4475
+ };
4476
4476
  } catch {
4477
4477
  return {
4478
4478
  rid,
@@ -4607,15 +4607,15 @@ function clearFastPreflightCache() {
4607
4607
  }
4608
4608
  }
4609
4609
 
4610
- function resolveBundledFfmpegPaths() {
4611
- const paths = [];
4612
- const addPath = (candidate) => {
4613
- // Electron can resolve ffmpeg-static through app.asar, but RemoteFast
4614
- // is a native process and must receive the physical unpacked path.
4615
- const ffmpegPath = resolveSpawnablePackagedPath(String(candidate || '').trim());
4616
- if (ffmpegPath && existsSync(ffmpegPath) && !paths.includes(ffmpegPath)) {
4617
- paths.push(ffmpegPath);
4618
- }
4610
+ function resolveBundledFfmpegPaths() {
4611
+ const paths = [];
4612
+ const addPath = (candidate) => {
4613
+ // Electron can resolve ffmpeg-static through app.asar, but RemoteFast
4614
+ // is a native process and must receive the physical unpacked path.
4615
+ const ffmpegPath = resolveSpawnablePackagedPath(String(candidate || '').trim());
4616
+ if (ffmpegPath && existsSync(ffmpegPath) && !paths.includes(ffmpegPath)) {
4617
+ paths.push(ffmpegPath);
4618
+ }
4619
4619
  };
4620
4620
 
4621
4621
  const addFfmpegInstaller = () => {
@@ -5121,10 +5121,10 @@ export async function runClientRuntime(argv = process.argv.slice(2), runtimeOpti
5121
5121
  savedSession: null,
5122
5122
  loadSavedSession: false,
5123
5123
  savedPin: null,
5124
- allowRelayFallback: transportAllowsRelay(parsed.transport),
5125
- relayEndpoint: parsed.relay,
5126
- recoverHubClientPort: runtimeOptions.recoverHubClientPort,
5127
- openBrowser: parsed.openBrowserOnStart,
5124
+ allowRelayFallback: transportAllowsRelay(parsed.transport),
5125
+ relayEndpoint: parsed.relay,
5126
+ recoverHubClientPort: runtimeOptions.recoverHubClientPort,
5127
+ openBrowser: parsed.openBrowserOnStart,
5128
5128
  onStarted: page => {
5129
5129
  connectionPage = page;
5130
5130
  resolveStarted(page);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.249",
3
+ "version": "0.1.251",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,10 +42,10 @@
42
42
  "ws": "^8.18.3"
43
43
  },
44
44
  "optionalDependencies": {
45
- "@livedesk/fast-linux-x64": "0.1.445",
46
- "@livedesk/fast-osx-arm64": "0.1.445",
47
- "@livedesk/fast-osx-x64": "0.1.445",
48
- "@livedesk/fast-win-x64": "0.1.445"
45
+ "@livedesk/fast-linux-x64": "0.1.447",
46
+ "@livedesk/fast-osx-arm64": "0.1.447",
47
+ "@livedesk/fast-osx-x64": "0.1.447",
48
+ "@livedesk/fast-win-x64": "0.1.447"
49
49
  },
50
50
  "publishConfig": {
51
51
  "access": "public"
@@ -0,0 +1,66 @@
1
+ import { existsSync } from 'node:fs';
2
+
3
+ export const AGENT_SHELL_NAMES = Object.freeze([
4
+ 'auto',
5
+ 'powershell',
6
+ 'pwsh',
7
+ 'cmd',
8
+ 'sh',
9
+ 'bash',
10
+ 'zsh'
11
+ ]);
12
+
13
+ function unixShellPath(shell, platform, fileExists) {
14
+ if (shell === 'sh') return '/bin/sh';
15
+ if (shell === 'bash') return '/bin/bash';
16
+ if (shell === 'zsh') return '/bin/zsh';
17
+ if (shell === 'pwsh') return 'pwsh';
18
+ if (shell === 'powershell' || shell === 'cmd') {
19
+ throw new Error(`${shell} is not available on ${platform}.`);
20
+ }
21
+ if (platform === 'darwin' && fileExists('/bin/zsh')) return '/bin/zsh';
22
+ if (fileExists('/bin/bash')) return '/bin/bash';
23
+ return '/bin/sh';
24
+ }
25
+
26
+ export function resolveAgentShellCommand({
27
+ platform = process.platform,
28
+ shell = 'auto',
29
+ command = '',
30
+ fileExists = existsSync
31
+ } = {}) {
32
+ const normalizedShell = String(shell || 'auto').trim().toLowerCase();
33
+ if (!AGENT_SHELL_NAMES.includes(normalizedShell)) {
34
+ throw new Error(`Unsupported shell: ${normalizedShell || 'empty'}.`);
35
+ }
36
+ if (platform === 'win32') {
37
+ if (['sh', 'bash', 'zsh'].includes(normalizedShell)) {
38
+ throw new Error(`${normalizedShell} is not available on Windows.`);
39
+ }
40
+ if (normalizedShell === 'cmd') {
41
+ return {
42
+ shell: 'cmd',
43
+ executable: process.env.ComSpec || 'cmd.exe',
44
+ args: ['/d', '/s', '/c', String(command)]
45
+ };
46
+ }
47
+ const powershellCore = normalizedShell === 'pwsh';
48
+ return {
49
+ shell: powershellCore ? 'pwsh' : 'powershell',
50
+ executable: powershellCore ? 'pwsh.exe' : 'powershell.exe',
51
+ args: ['-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', String(command)]
52
+ };
53
+ }
54
+
55
+ const executable = unixShellPath(normalizedShell, platform, fileExists);
56
+ if (executable.startsWith('/') && !fileExists(executable)) {
57
+ throw new Error(`${normalizedShell} is not installed at ${executable}.`);
58
+ }
59
+ return {
60
+ shell: normalizedShell === 'auto'
61
+ ? executable.endsWith('/zsh') ? 'zsh' : executable.endsWith('/bash') ? 'bash' : 'sh'
62
+ : normalizedShell,
63
+ executable,
64
+ args: ['-lc', String(command)]
65
+ };
66
+ }
@@ -1,23 +1,23 @@
1
- import { existsSync } from 'node:fs';
2
-
3
- const ASAR_PATH_SEGMENT = /([\\/])app\.asar([\\/])/i;
4
-
5
- /**
6
- * Electron's Node loader can read JavaScript through the virtual app.asar
7
- * path, but the operating system cannot spawn an executable from that virtual
8
- * archive. electron-builder places executable payloads in the matching
9
- * app.asar.unpacked tree, so only spawn targets cross that boundary.
10
- */
11
- export function resolveSpawnablePackagedPath(value, pathExists = existsSync) {
12
- const candidate = String(value || '');
13
- if (!candidate || !ASAR_PATH_SEGMENT.test(candidate)) {
14
- return candidate;
15
- }
16
-
17
- const unpacked = candidate.replace(ASAR_PATH_SEGMENT, '$1app.asar.unpacked$2');
18
- try {
19
- return pathExists(unpacked) ? unpacked : candidate;
20
- } catch {
21
- return candidate;
22
- }
23
- }
1
+ import { existsSync } from 'node:fs';
2
+
3
+ const ASAR_PATH_SEGMENT = /([\\/])app\.asar([\\/])/i;
4
+
5
+ /**
6
+ * Electron's Node loader can read JavaScript through the virtual app.asar
7
+ * path, but the operating system cannot spawn an executable from that virtual
8
+ * archive. electron-builder places executable payloads in the matching
9
+ * app.asar.unpacked tree, so only spawn targets cross that boundary.
10
+ */
11
+ export function resolveSpawnablePackagedPath(value, pathExists = existsSync) {
12
+ const candidate = String(value || '');
13
+ if (!candidate || !ASAR_PATH_SEGMENT.test(candidate)) {
14
+ return candidate;
15
+ }
16
+
17
+ const unpacked = candidate.replace(ASAR_PATH_SEGMENT, '$1app.asar.unpacked$2');
18
+ try {
19
+ return pathExists(unpacked) ? unpacked : candidate;
20
+ } catch {
21
+ return candidate;
22
+ }
23
+ }