@atom-js-org/cli 0.3.0-alpha.0 → 0.4.1-alpha.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atom-js-org/cli",
3
- "version": "0.3.0-alpha.0",
3
+ "version": "0.4.1-alpha.0",
4
4
  "description": "CLI for running and building AtomJS desktop applications.",
5
5
  "bin": {
6
6
  "atom": "bin/atom.cjs"
package/src/build.cjs CHANGED
@@ -39,8 +39,8 @@ async function localBuild(project, target, options = {}) {
39
39
  }
40
40
 
41
41
  const buildRoot = path.join(project.root, 'build', target);
42
- const unpacked = path.join(buildRoot, 'unpacked');
43
- const appDir = path.join(unpacked, 'app');
42
+ const unpacked = path.join(buildRoot, 'portable');
43
+ const appDir = null;
44
44
  const productName = sanitizeFilename(project.config.productName);
45
45
 
46
46
  console.log(`\nAtomJS build (${target})`);
@@ -50,7 +50,8 @@ async function localBuild(project, target, options = {}) {
50
50
  await fse.remove(buildRoot);
51
51
  await fse.ensureDir(unpacked);
52
52
 
53
- const stageRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'atomjs-stage-'));
53
+ const stageBase = await resolveShortStageBase();
54
+ const stageRoot = await fs.promises.mkdtemp(path.join(stageBase, 's-'));
54
55
  const stagedApp = path.join(stageRoot, 'app');
55
56
  const payloadPath = path.join(stageRoot, 'atom-app.payload.gz');
56
57
 
@@ -59,21 +60,8 @@ async function localBuild(project, target, options = {}) {
59
60
  await vendorFramework(stagedApp, project, target);
60
61
  await installProductionDependencies(stagedApp, options.skipInstall, target);
61
62
 
62
- if (target === 'macos') {
63
- console.log('Embedding application code and production dependencies into the native executable...');
64
- await createApplicationPayload(stagedApp, payloadPath);
65
- } else {
66
- await fse.move(stagedApp, appDir, { overwrite: true });
67
- }
68
-
69
- if (target !== 'macos') {
70
- const runtimeDir = path.join(unpacked, 'runtime');
71
- await fse.ensureDir(runtimeDir);
72
- const runtimeNodeName = target === 'windows' ? 'node.exe' : 'node';
73
- const runtimeNodePath = path.join(runtimeDir, runtimeNodeName);
74
- await fs.promises.copyFile(process.execPath, runtimeNodePath);
75
- if (target !== 'windows') await fs.promises.chmod(runtimeNodePath, 0o755);
76
- }
63
+ console.log('Embedding application code and production dependencies into the executable...');
64
+ await createApplicationPayload(stagedApp, payloadPath);
77
65
 
78
66
  const executableName = target === 'windows' ? `${productName}.exe` : productName;
79
67
  const executablePath = path.join(unpacked, executableName);
@@ -82,7 +70,7 @@ async function localBuild(project, target, options = {}) {
82
70
  appDir,
83
71
  target,
84
72
  productName,
85
- payloadPath: target === 'macos' ? payloadPath : null
73
+ payloadPath
86
74
  });
87
75
 
88
76
  const creditPath = path.join(unpacked, 'ATOMJS-CREDIT.txt');
@@ -138,6 +126,28 @@ async function localBuild(project, target, options = {}) {
138
126
  }
139
127
  }
140
128
 
129
+
130
+ async function resolveShortStageBase() {
131
+ const configured = process.env.ATOMJS_TEMP_DIR || process.env.ATOM_TEMP_DIR;
132
+ const candidates = configured
133
+ ? [path.resolve(configured)]
134
+ : process.platform === 'win32'
135
+ ? [path.join(path.parse(process.cwd()).root, '.atomjs-tmp'), path.join(os.tmpdir(), 'atomjs')]
136
+ : [path.join(os.tmpdir(), 'atomjs')];
137
+
138
+ let lastError;
139
+ for (const candidate of candidates) {
140
+ try {
141
+ await fse.ensureDir(candidate);
142
+ await fs.promises.access(candidate, fs.constants.W_OK);
143
+ return candidate;
144
+ } catch (error) {
145
+ lastError = error;
146
+ }
147
+ }
148
+ throw new Error(`AtomJS could not create a writable short staging directory: ${lastError ? lastError.message : 'unknown error'}`);
149
+ }
150
+
141
151
  async function copyApplication(projectRoot, appDir) {
142
152
  await fse.copy(projectRoot, appDir, {
143
153
  dereference: false,
@@ -284,9 +294,8 @@ async function createSeaLauncher({ executablePath, appDir, target, productName,
284
294
  const blobPath = path.join(work, target === 'windows' ? 'sea-prep.blob.exe' : 'sea-prep.blob');
285
295
  const configPath = path.join(work, 'sea-config.json');
286
296
 
287
- const launcher = payloadPath
288
- ? createEmbeddedLauncherSource(productName)
289
- : createLooseLauncherSource();
297
+ if (!payloadPath) throw new Error('AtomJS SEA payload is required.');
298
+ const launcher = createEmbeddedLauncherSource(productName);
290
299
  await fs.promises.writeFile(launcherPath, launcher, 'utf8');
291
300
 
292
301
  const seaConfig = {
@@ -295,13 +304,16 @@ async function createSeaLauncher({ executablePath, appDir, target, productName,
295
304
  disableExperimentalSEAWarning: true,
296
305
  useSnapshot: false,
297
306
  useCodeCache: false,
298
- ...(payloadPath ? { assets: { 'atom-app': payloadPath } } : {})
307
+ assets: { 'atom-app': payloadPath }
299
308
  };
300
309
  await fs.promises.writeFile(configPath, JSON.stringify(seaConfig, null, 2));
301
310
 
302
311
  await run(process.execPath, ['--experimental-sea-config', configPath], { cwd: work });
303
312
  await fs.promises.copyFile(process.execPath, executablePath);
304
313
 
314
+ if (target === 'windows') {
315
+ await prepareWindowsExecutableForInjection(executablePath);
316
+ }
305
317
  if (target === 'macos' && commandExists('codesign', ['--version'])) {
306
318
  spawnSync('codesign', ['--remove-signature', executablePath], { stdio: 'ignore' });
307
319
  }
@@ -311,38 +323,64 @@ async function createSeaLauncher({ executablePath, appDir, target, productName,
311
323
  machoSegmentName: 'NODE_SEA'
312
324
  });
313
325
 
314
- if (target !== 'windows') await fs.promises.chmod(executablePath, 0o755);
326
+ if (target === 'windows') {
327
+ await setWindowsGuiSubsystem(executablePath);
328
+ } else {
329
+ await fs.promises.chmod(executablePath, 0o755);
330
+ }
315
331
  if (target === 'macos' && commandExists('codesign', ['--version'])) {
316
332
  await run('codesign', ['--force', '--sign', '-', executablePath]);
317
333
  }
318
334
  await fse.remove(work);
319
335
  }
320
336
 
321
- function createLooseLauncherSource() {
322
- return `
323
- 'use strict';
324
- const path = require('node:path');
325
- const fs = require('node:fs');
326
- const { createRequire } = require('node:module');
327
337
 
328
- const executableDir = path.dirname(process.execPath);
329
- const appDir = path.join(executableDir, 'app');
330
- const packagePath = path.join(appDir, 'package.json');
331
- if (!fs.existsSync(packagePath)) {
332
- console.error('AtomJS application files are missing:', appDir);
333
- process.exit(1);
338
+ async function prepareWindowsExecutableForInjection(executablePath) {
339
+ const image = await fs.promises.readFile(executablePath);
340
+ const pe = readPortableExecutableLayout(image);
341
+ const certificateDirectory = pe.dataDirectoryOffset + (8 * 4);
342
+ const certificateOffset = image.readUInt32LE(certificateDirectory);
343
+ const certificateSize = image.readUInt32LE(certificateDirectory + 4);
344
+
345
+ image.writeUInt32LE(0, certificateDirectory);
346
+ image.writeUInt32LE(0, certificateDirectory + 4);
347
+ image.writeUInt32LE(0, pe.optionalHeaderOffset + 64);
348
+
349
+ const certificateEnd = certificateOffset + certificateSize;
350
+ const output = certificateOffset > 0 && certificateSize > 0 && certificateEnd === image.length
351
+ ? image.subarray(0, certificateOffset)
352
+ : image;
353
+ await fs.promises.writeFile(executablePath, output);
334
354
  }
335
355
 
336
- const runtimeNode = path.join(executableDir, 'runtime', process.platform === 'win32' ? 'node.exe' : 'node');
337
- process.chdir(appDir);
338
- process.env.ATOM_PROJECT_ROOT = appDir;
339
- process.env.ATOM_BUILD = '1';
340
- process.env.ATOM_NODE_EXECUTABLE = runtimeNode;
341
- const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
342
- const mainPath = path.resolve(appDir, pkg.main || 'main.js');
343
- const load = createRequire(packagePath);
344
- load(mainPath);
345
- `;
356
+ async function setWindowsGuiSubsystem(executablePath) {
357
+ const image = await fs.promises.readFile(executablePath);
358
+ const pe = readPortableExecutableLayout(image);
359
+ image.writeUInt16LE(2, pe.optionalHeaderOffset + 68);
360
+ image.writeUInt32LE(0, pe.optionalHeaderOffset + 64);
361
+ await fs.promises.writeFile(executablePath, image);
362
+ }
363
+
364
+ function readPortableExecutableLayout(image) {
365
+ if (image.length < 0x100 || image.toString('ascii', 0, 2) !== 'MZ') {
366
+ throw new Error('AtomJS expected a Windows PE executable but the DOS header is missing.');
367
+ }
368
+
369
+ const peOffset = image.readUInt32LE(0x3c);
370
+ if (peOffset < 0x40 || peOffset + 0x100 > image.length || image.toString('ascii', peOffset, peOffset + 4) !== 'PE\\0\\0') {
371
+ throw new Error('AtomJS expected a Windows PE executable but the PE header is invalid.');
372
+ }
373
+
374
+ const optionalHeaderOffset = peOffset + 24;
375
+ const magic = image.readUInt16LE(optionalHeaderOffset);
376
+ if (magic !== 0x10b && magic !== 0x20b) {
377
+ throw new Error(`AtomJS does not support Windows PE optional-header magic 0x${magic.toString(16)}.`);
378
+ }
379
+
380
+ return {
381
+ optionalHeaderOffset,
382
+ dataDirectoryOffset: optionalHeaderOffset + (magic === 0x20b ? 112 : 96)
383
+ };
346
384
  }
347
385
 
348
386
  function createEmbeddedLauncherSource(productName) {
@@ -354,6 +392,7 @@ const os = require('node:os');
354
392
  const crypto = require('node:crypto');
355
393
  const zlib = require('node:zlib');
356
394
  const { createRequire } = require('node:module');
395
+ const { pathToFileURL } = require('node:url');
357
396
  const { getAsset } = require('node:sea');
358
397
 
359
398
  const productName = ${JSON.stringify(productName)};
@@ -397,7 +436,20 @@ const executableDir = path.dirname(process.execPath);
397
436
  process.chdir(appDir);
398
437
  process.env.ATOM_PROJECT_ROOT = appDir;
399
438
  process.env.ATOM_BUILD = '1';
439
+ process.env.ATOM_EMBEDDED_RUNTIME = '1';
440
+ process.env.ATOM_WINDOW_HOST_ENTRY = path.join(appDir, 'vendor', 'atomjs', 'src', 'runtime', 'window-host.mjs');
400
441
  process.env.ATOM_MACOS_HOST_EXECUTABLE = path.join(executableDir, 'AtomJSWindowHost');
442
+ const hostModeIndex = process.argv.indexOf('--atomjs-window-host');
443
+ if (hostModeIndex !== -1) {
444
+ const hostEntry = process.env.ATOM_WINDOW_HOST_ENTRY;
445
+ const configPath = process.argv[hostModeIndex + 1];
446
+ process.argv = [process.argv[0], hostEntry, configPath];
447
+ import(pathToFileURL(hostEntry).href).catch((error) => {
448
+ console.error(error && error.stack ? error.stack : error);
449
+ process.exit(1);
450
+ });
451
+ return;
452
+ }
401
453
  const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
402
454
  const mainPath = path.resolve(appDir, pkg.main || 'main.js');
403
455
  const load = createRequire(packagePath);
@@ -414,40 +466,65 @@ async function packageWindows({ project, buildRoot, unpacked, executableName, pr
414
466
  const nsisPath = path.join(buildRoot, 'installer.nsi');
415
467
  const installerPath = path.join(buildRoot, `${productName} Installer.exe`);
416
468
  const escapedSource = unpacked.replace(/\\/g, '\\\\');
469
+ const version = String(project.packageJson.version || '0.0.0');
470
+ const author = project.packageJson.author;
471
+ const publisher = typeof author === 'string'
472
+ ? author
473
+ : author && typeof author === 'object' && author.name
474
+ ? author.name
475
+ : 'AtomJS application';
476
+ const uninstallKey = `Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${escapeNsis(project.config.appId)}`;
417
477
  const credit = project.config.installerCredit
418
478
  ? `!define MUI_WELCOMEPAGE_TEXT "This installer will install ${escapeNsis(productName)}.$\\r$\\n$\\r$\\nPowered by AtomJS — https://github.com/Atom-js-org/atom"`
419
479
  : '';
420
480
  const script = `
421
481
  Unicode true
482
+ SetCompressor /SOLID lzma
422
483
  !include "MUI2.nsh"
423
484
  Name "${escapeNsis(productName)}"
424
485
  OutFile "${installerPath.replace(/\\/g, '\\\\')}"
425
486
  InstallDir "$LOCALAPPDATA\\Programs\\${escapeNsis(productName)}"
426
487
  RequestExecutionLevel user
488
+ ShowInstDetails show
489
+ ShowUninstDetails show
427
490
  ${credit}
428
491
  !insertmacro MUI_PAGE_WELCOME
429
492
  !insertmacro MUI_PAGE_DIRECTORY
430
493
  !insertmacro MUI_PAGE_INSTFILES
494
+ !define MUI_FINISHPAGE_RUN "$INSTDIR\\${escapeNsis(executableName)}"
431
495
  !insertmacro MUI_PAGE_FINISH
496
+ !insertmacro MUI_UNPAGE_CONFIRM
497
+ !insertmacro MUI_UNPAGE_INSTFILES
432
498
  !insertmacro MUI_LANGUAGE "English"
433
499
  Section "Install"
500
+ SetShellVarContext current
434
501
  SetOutPath "$INSTDIR"
435
502
  File /r "${escapedSource}\\*"
436
503
  CreateDirectory "$SMPROGRAMS\\${escapeNsis(productName)}"
437
504
  CreateShortcut "$SMPROGRAMS\\${escapeNsis(productName)}\\${escapeNsis(productName)}.lnk" "$INSTDIR\\${escapeNsis(executableName)}"
438
505
  CreateShortcut "$DESKTOP\\${escapeNsis(productName)}.lnk" "$INSTDIR\\${escapeNsis(executableName)}"
439
506
  WriteUninstaller "$INSTDIR\\Uninstall.exe"
507
+ WriteRegStr HKCU "${uninstallKey}" "DisplayName" "${escapeNsis(productName)}"
508
+ WriteRegStr HKCU "${uninstallKey}" "DisplayVersion" "${escapeNsis(version)}"
509
+ WriteRegStr HKCU "${uninstallKey}" "Publisher" "${escapeNsis(publisher)}"
510
+ WriteRegStr HKCU "${uninstallKey}" "DisplayIcon" "$INSTDIR\\${escapeNsis(executableName)}"
511
+ WriteRegStr HKCU "${uninstallKey}" "UninstallString" "$\\\"$INSTDIR\\Uninstall.exe$\\\""
512
+ WriteRegDWORD HKCU "${uninstallKey}" "NoModify" 1
513
+ WriteRegDWORD HKCU "${uninstallKey}" "NoRepair" 1
440
514
  SectionEnd
441
515
  Section "Uninstall"
516
+ SetShellVarContext current
442
517
  Delete "$DESKTOP\\${escapeNsis(productName)}.lnk"
443
518
  RMDir /r "$SMPROGRAMS\\${escapeNsis(productName)}"
519
+ DeleteRegKey HKCU "${uninstallKey}"
444
520
  RMDir /r "$INSTDIR"
445
521
  SectionEnd
446
522
  `;
447
523
  await fs.promises.writeFile(nsisPath, script.trimStart(), 'utf8');
448
524
 
449
- if (commandExists('makensis', ['/VERSION'])) {
450
- await run('makensis', [nsisPath]);
525
+ const makensis = resolveNsisExecutable();
526
+ if (makensis) {
527
+ await run(makensis, [nsisPath]);
451
528
  if (fs.existsSync(installerPath)) outputs.push(installerPath);
452
529
  } else {
453
530
  console.warn('NSIS was not found; installer.nsi was generated but the .exe installer was skipped.');
@@ -455,6 +532,19 @@ SectionEnd
455
532
  return outputs;
456
533
  }
457
534
 
535
+ function resolveNsisExecutable() {
536
+ const candidates = [
537
+ process.env.MAKENSIS_PATH,
538
+ process.env.NSIS_HOME && path.join(process.env.NSIS_HOME, 'makensis.exe'),
539
+ process.env['ProgramFiles(x86)'] && path.join(process.env['ProgramFiles(x86)'], 'NSIS', 'makensis.exe'),
540
+ process.env.ProgramFiles && path.join(process.env.ProgramFiles, 'NSIS', 'makensis.exe'),
541
+ process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, 'Programs', 'NSIS', 'makensis.exe')
542
+ ].filter(Boolean);
543
+
544
+ if (commandExists('makensis', ['/VERSION'])) return 'makensis';
545
+ return candidates.find((candidate) => fs.existsSync(candidate)) || null;
546
+ }
547
+
458
548
  async function packageMacOS({ project, buildRoot, unpacked, executableName, productName, hostSource }) {
459
549
  const outputs = [];
460
550
  const appBundle = path.join(buildRoot, `${productName}.app`);
@@ -555,8 +645,6 @@ async function packageLinux({ project, buildRoot, unpacked, executableName, prod
555
645
  await fse.ensureDir(usrBin);
556
646
  await fse.ensureDir(usrLibApp);
557
647
  await fse.copy(path.join(unpacked, executableName), path.join(usrBin, productName));
558
- await fse.copy(path.join(unpacked, 'app'), path.join(usrBin, 'app'));
559
- await fse.copy(path.join(unpacked, 'runtime'), path.join(usrBin, 'runtime'));
560
648
  await fse.copy(path.join(unpacked, 'ATOMJS-CREDIT.txt'), path.join(usrLibApp, 'ATOMJS-CREDIT.txt'));
561
649
 
562
650
  const appRun = `#!/bin/sh\nHERE="$(dirname "$(readlink -f "$0")")"\nexec "$HERE/usr/bin/${productName}" "$@"\n`;
package/src/doctor.cjs CHANGED
@@ -12,8 +12,9 @@ async function doctorCommand(options = {}) {
12
12
 
13
13
  if (process.platform === 'win32') {
14
14
  rows.push(check('PowerShell', commandExists('powershell.exe', ['-NoProfile', '-Command', '$PSVersionTable.PSVersion.ToString()'])));
15
- rows.push(check('WebView2 Runtime', checkWindowsWebView2(), 'required by the native WebView'));
16
- rows.push(check('NSIS (optional installer)', commandExists('makensis', ['/VERSION'])));
15
+ const webView2Version = getWindowsWebView2Version();
16
+ rows.push(check('WebView2 Runtime', Boolean(webView2Version), webView2Version || 'required by the native WebView'));
17
+ rows.push(check('NSIS (optional installer)', Boolean(resolveNsisExecutable())));
17
18
  } else if (process.platform === 'darwin') {
18
19
  rows.push(check('Xcode Command Line Tools', commandExists('/usr/bin/xcrun', ['--version']), 'required to compile the native Cocoa host'));
19
20
  rows.push(check('Clang', commandExists('/usr/bin/xcrun', ['clang', '--version'])));
@@ -69,18 +70,43 @@ function pkgConfigExists(name) {
69
70
  return !result.error && result.status === 0;
70
71
  }
71
72
 
72
- function checkWindowsWebView2() {
73
+ function getWindowsWebView2Version() {
73
74
  const script = `
74
75
  $paths = @(
75
- 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F1E7E4A4-BD05-43A5-BCC0-B7F5E0E9D7F5}',
76
- 'HKLM:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F1E7E4A4-BD05-43A5-BCC0-B7F5E0E9D7F5}',
77
- 'HKCU:\\Software\\Microsoft\\EdgeUpdate\\Clients\\{F1E7E4A4-BD05-43A5-BCC0-B7F5E0E9D7F5}'
76
+ 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}',
77
+ 'HKLM:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}',
78
+ 'HKCU:\\Software\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}'
78
79
  )
79
- foreach ($p in $paths) { if (Test-Path $p) { exit 0 } }
80
+ foreach ($p in $paths) {
81
+ try {
82
+ $version = Get-ItemPropertyValue -Path $p -Name 'pv' -ErrorAction Stop
83
+ if ($version -and $version -ne '0.0.0.0') {
84
+ Write-Output $version
85
+ exit 0
86
+ }
87
+ } catch {}
88
+ }
80
89
  exit 1
81
90
  `;
82
- const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', script], { stdio: 'ignore' });
83
- return !result.error && result.status === 0;
91
+ const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', script], {
92
+ encoding: 'utf8',
93
+ windowsHide: true
94
+ });
95
+ if (result.error || result.status !== 0) return null;
96
+ return String(result.stdout || '').trim() || null;
97
+ }
98
+
99
+ function resolveNsisExecutable() {
100
+ const candidates = [
101
+ process.env.MAKENSIS_PATH,
102
+ process.env.NSIS_HOME && path.join(process.env.NSIS_HOME, 'makensis.exe'),
103
+ process.env['ProgramFiles(x86)'] && path.join(process.env['ProgramFiles(x86)'], 'NSIS', 'makensis.exe'),
104
+ process.env.ProgramFiles && path.join(process.env.ProgramFiles, 'NSIS', 'makensis.exe'),
105
+ process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, 'Programs', 'NSIS', 'makensis.exe')
106
+ ].filter(Boolean);
107
+
108
+ if (commandExists('makensis', ['/VERSION'])) return 'makensis';
109
+ return candidates.find((candidate) => fs.existsSync(candidate)) || null;
84
110
  }
85
111
 
86
112
  module.exports = { doctorCommand };
package/src/utils.cjs CHANGED
@@ -54,9 +54,17 @@ function loadProject(projectInput) {
54
54
  };
55
55
  }
56
56
 
57
+ function normalizeSpawn(command, args) {
58
+ if (process.platform === 'win32' && /\.(?:cmd|bat)$/i.test(command)) {
59
+ return { command: process.env.ComSpec || 'cmd.exe', args: ['/d', '/s', '/c', command, ...args] };
60
+ }
61
+ return { command, args };
62
+ }
63
+
57
64
  function run(command, args, options = {}) {
58
65
  return new Promise((resolve, reject) => {
59
- const child = spawn(command, args, {
66
+ const normalized = normalizeSpawn(command, args);
67
+ const child = spawn(normalized.command, normalized.args, {
60
68
  cwd: options.cwd || process.cwd(),
61
69
  env: options.env || process.env,
62
70
  stdio: options.stdio || 'inherit',
@@ -71,7 +79,8 @@ function run(command, args, options = {}) {
71
79
  }
72
80
 
73
81
  function capture(command, args, options = {}) {
74
- const result = spawnSync(command, args, {
82
+ const normalized = normalizeSpawn(command, args);
83
+ const result = spawnSync(normalized.command, normalized.args, {
75
84
  cwd: options.cwd || process.cwd(),
76
85
  env: options.env || process.env,
77
86
  encoding: 'utf8',
@@ -86,7 +95,8 @@ function capture(command, args, options = {}) {
86
95
  }
87
96
 
88
97
  function commandExists(command, args = ['--version']) {
89
- const result = spawnSync(command, args, { stdio: 'ignore', shell: false });
98
+ const normalized = normalizeSpawn(command, args);
99
+ const result = spawnSync(normalized.command, normalized.args, { stdio: 'ignore', shell: false });
90
100
  return !result.error && result.status === 0;
91
101
  }
92
102