@openagents-org/agent-launcher 0.1.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,588 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execSync, exec } = require('child_process');
6
+ const { whichBinary, getEnhancedEnv } = require('./paths');
7
+
8
+ /**
9
+ * Manages installation and uninstallation of agent runtimes.
10
+ *
11
+ * Install markers are stored in two places for compatibility with the Python SDK:
12
+ * 1. ~/.openagents/installed_agents.json (JSON array of names)
13
+ * 2. ~/.openagents/installed/<name> (empty marker files)
14
+ */
15
+ class Installer {
16
+ constructor(registry, configDir) {
17
+ this.registry = registry;
18
+ this.configDir = configDir;
19
+ this.markersFile = path.join(configDir, 'installed_agents.json');
20
+ this.markersDir = path.join(configDir, 'installed');
21
+ }
22
+
23
+ /**
24
+ * Get the current platform key: 'macos', 'linux', or 'windows'.
25
+ */
26
+ static platform() {
27
+ const p = process.platform;
28
+ if (p === 'darwin') return 'macos';
29
+ if (p === 'win32') return 'windows';
30
+ return 'linux';
31
+ }
32
+
33
+ /**
34
+ * Check if an agent type is installed.
35
+ * Checks binary on PATH first, then marker files.
36
+ */
37
+ isInstalled(agentType) {
38
+ if (this._whichBinary(agentType)) return true;
39
+ return this._hasMarker(agentType);
40
+ }
41
+
42
+ /**
43
+ * Find the binary path for an agent type.
44
+ */
45
+ which(agentType) {
46
+ return this._whichBinary(agentType);
47
+ }
48
+
49
+ /**
50
+ * Health check — binary existence + version.
51
+ * @returns {{ installed: boolean, binary: string|null, version: string|null }}
52
+ */
53
+ healthCheck(agentType) {
54
+ const binary = this._whichBinary(agentType);
55
+ if (!binary) return { installed: false, binary: null, version: null };
56
+
57
+ const entry = this.registry.getEntry(agentType);
58
+ const checkCmd = entry && entry.install ? entry.install.check_command : null;
59
+ const versionCmd = checkCmd || `${entry && entry.install && entry.install.binary || agentType} --version`;
60
+
61
+ let version = null;
62
+ try {
63
+ const raw = execSync(versionCmd, {
64
+ encoding: 'utf-8',
65
+ stdio: ['pipe', 'pipe', 'pipe'],
66
+ env: getEnhancedEnv(),
67
+ timeout: 10000,
68
+ }).trim();
69
+ // Extract version number (e.g. "openclaw 2024.1.5" → "2024.1.5")
70
+ const match = raw.match(/(\d+[\d.]+\d+)/);
71
+ version = match ? match[1] : raw.split('\n')[0];
72
+ } catch {}
73
+
74
+ return { installed: true, binary, version };
75
+ }
76
+
77
+ /**
78
+ * Install an agent runtime.
79
+ * @returns {Promise<{success: boolean, output: string}>}
80
+ */
81
+ async install(agentType) {
82
+ const entry = this.registry.getEntry(agentType);
83
+ if (!entry || !entry.install) {
84
+ throw new Error(`No install definition for agent type: ${agentType}`);
85
+ }
86
+
87
+ let cmd = this._getInstallCommand(entry.install);
88
+ if (!cmd) {
89
+ throw new Error(`No install command for ${agentType} on ${Installer.platform()}`);
90
+ }
91
+
92
+ // Use bundled node/npm if system npm not available
93
+ if (cmd.startsWith('npm install')) {
94
+ const args = cmd.replace('npm install', 'install');
95
+ cmd = this._resolveNpmCommand(args);
96
+ }
97
+
98
+ const output = await this._execShell(cmd);
99
+ this._markInstalled(agentType);
100
+ return { success: true, output };
101
+ }
102
+
103
+ /**
104
+ * Install with streaming output via callback.
105
+ * @param {string} agentType
106
+ * @param {function(string)} onData - called with each chunk of output
107
+ * @returns {Promise<{success: boolean, command: string}>}
108
+ */
109
+ async installStreaming(agentType, onData) {
110
+ const { spawn } = require('child_process');
111
+ const entry = this.registry.getEntry(agentType);
112
+ if (!entry || !entry.install) {
113
+ throw new Error(`No install definition for agent type: ${agentType}`);
114
+ }
115
+
116
+ let rawCmd = this._getInstallCommand(entry.install);
117
+ if (!rawCmd) {
118
+ throw new Error(`No install command for ${agentType} on ${Installer.platform()}`);
119
+ }
120
+
121
+ // Auto-install Node.js if this is an npm-based agent and Node.js is missing
122
+ if (rawCmd.startsWith('npm install') && !this.hasNodejs()) {
123
+ await this.installNodejs(onData);
124
+ }
125
+
126
+ // Resolve npm command
127
+ let cmd = rawCmd;
128
+ if (rawCmd.startsWith('npm install')) {
129
+ const args = rawCmd.replace('npm install', 'install --loglevel=verbose');
130
+ cmd = this._resolveNpmCommand(args);
131
+ } else if (rawCmd.startsWith('pip install') || rawCmd.startsWith('pipx install')) {
132
+ cmd = rawCmd; // pip commands stay as-is
133
+ }
134
+
135
+ if (onData) onData(`$ ${cmd}\n\n`);
136
+
137
+ const env = this._buildShellEnv();
138
+ const shell = process.platform === 'win32'
139
+ ? (process.env.ComSpec || 'C:\\Windows\\System32\\cmd.exe')
140
+ : true;
141
+
142
+ return new Promise((resolve, reject) => {
143
+ const proc = spawn(cmd, [], { shell, env, stdio: ['ignore', 'pipe', 'pipe'] });
144
+
145
+ if (proc.stdout) proc.stdout.setEncoding('utf-8');
146
+ if (proc.stderr) proc.stderr.setEncoding('utf-8');
147
+
148
+ if (proc.stdout) proc.stdout.on('data', (d) => { if (onData) onData(d); });
149
+ if (proc.stderr) proc.stderr.on('data', (d) => { if (onData) onData(d); });
150
+
151
+ proc.on('error', (err) => reject(err));
152
+ proc.on('close', (code) => {
153
+ if (code === 0) {
154
+ this._markInstalled(agentType);
155
+ if (onData) onData(`\nDone! ${agentType} is now installed.\n`);
156
+ resolve({ success: true, command: cmd });
157
+ } else {
158
+ const msg = `Install failed with exit code ${code}`;
159
+ if (onData) onData(`\n${msg}\n`);
160
+ reject(new Error(msg));
161
+ }
162
+ });
163
+ });
164
+ }
165
+
166
+ /**
167
+ * Uninstall an agent runtime.
168
+ * @returns {Promise<{success: boolean, output: string}>}
169
+ */
170
+ async uninstall(agentType) {
171
+ const entry = this.registry.getEntry(agentType);
172
+ if (!entry || !entry.install) {
173
+ throw new Error(`No install definition for agent type: ${agentType}`);
174
+ }
175
+
176
+ const installCmd = this._getInstallCommand(entry.install);
177
+ const uninstallCmd = this._deriveUninstallCommand(installCmd);
178
+ if (!uninstallCmd) {
179
+ throw new Error(`Cannot derive uninstall command for ${agentType}`);
180
+ }
181
+
182
+ const output = await this._execShell(uninstallCmd);
183
+ this._markUninstalled(agentType);
184
+ return { success: true, output };
185
+ }
186
+
187
+ /**
188
+ * Uninstall with streaming output via callback.
189
+ */
190
+ async uninstallStreaming(agentType, onData) {
191
+ const { spawn } = require('child_process');
192
+ const entry = this.registry.getEntry(agentType);
193
+ if (!entry || !entry.install) {
194
+ throw new Error(`No install definition for agent type: ${agentType}`);
195
+ }
196
+
197
+ const installCmd = this._getInstallCommand(entry.install);
198
+ let rawCmd = this._deriveUninstallCommand(installCmd);
199
+ if (!rawCmd) {
200
+ throw new Error(`Cannot derive uninstall command for ${agentType}`);
201
+ }
202
+
203
+ // Resolve npm to use bundled node if system npm is not available
204
+ let cmd = rawCmd;
205
+ if (rawCmd.startsWith('npm uninstall')) {
206
+ const args = rawCmd.replace('npm uninstall', 'uninstall --loglevel=verbose');
207
+ cmd = this._resolveNpmCommand(args);
208
+ }
209
+
210
+ if (onData) onData(`$ ${cmd}\n\n`);
211
+
212
+ const env = this._buildShellEnv();
213
+ const shell = process.platform === 'win32'
214
+ ? (process.env.ComSpec || 'C:\\Windows\\System32\\cmd.exe')
215
+ : true;
216
+
217
+ return new Promise((resolve, reject) => {
218
+ const proc = spawn(cmd, [], { shell, env, stdio: ['ignore', 'pipe', 'pipe'] });
219
+
220
+ if (proc.stdout) proc.stdout.setEncoding('utf-8');
221
+ if (proc.stderr) proc.stderr.setEncoding('utf-8');
222
+
223
+ if (proc.stdout) proc.stdout.on('data', (d) => { if (onData) onData(d); });
224
+ if (proc.stderr) proc.stderr.on('data', (d) => { if (onData) onData(d); });
225
+
226
+ proc.on('error', (err) => reject(err));
227
+ proc.on('close', (code) => {
228
+ if (code === 0) {
229
+ this._markUninstalled(agentType);
230
+ if (onData) onData(`\nDone! ${agentType} has been uninstalled.\n`);
231
+ resolve({ success: true, command: cmd });
232
+ } else {
233
+ const msg = `Uninstall failed with exit code ${code}`;
234
+ if (onData) onData(`\n${msg}\n`);
235
+ reject(new Error(msg));
236
+ }
237
+ });
238
+ });
239
+ }
240
+
241
+ /**
242
+ * Get install command for current platform.
243
+ */
244
+ _getInstallCommand(installCfg) {
245
+ const plat = Installer.platform();
246
+ return installCfg[plat] || installCfg.command || null;
247
+ }
248
+
249
+ /**
250
+ * Derive uninstall command from install command.
251
+ */
252
+ _deriveUninstallCommand(installCmd) {
253
+ if (!installCmd) return null;
254
+
255
+ // npm install -g <pkg> → npm uninstall -g <pkg>
256
+ if (installCmd.includes('npm install')) {
257
+ return installCmd
258
+ .replace('npm install -g', 'npm uninstall -g')
259
+ .replace('npm install', 'npm uninstall')
260
+ .replace(/@latest/g, '')
261
+ .replace(/@[\d.]+/g, '');
262
+ }
263
+
264
+ // pip install <pkg> → pip uninstall -y <pkg>
265
+ if (installCmd.includes('pip install') || installCmd.includes('pip3 install')) {
266
+ return installCmd
267
+ .replace('pip install', 'pip uninstall -y')
268
+ .replace('pip3 install', 'pip3 uninstall -y');
269
+ }
270
+
271
+ // pipx install <pkg> → pipx uninstall <pkg>
272
+ if (installCmd.includes('pipx install')) {
273
+ return installCmd.replace('pipx install', 'pipx uninstall');
274
+ }
275
+
276
+ return null;
277
+ }
278
+
279
+ /**
280
+ * Find a binary on PATH (delegates to paths.js for cross-platform detection).
281
+ */
282
+ _whichBinary(agentType) {
283
+ const entry = this.registry.getEntry(agentType);
284
+ const binary = entry && entry.install ? entry.install.binary : agentType;
285
+ return whichBinary(binary);
286
+ }
287
+
288
+ // -- Markers --
289
+
290
+ _hasMarker(agentType) {
291
+ // Check per-agent marker file first (faster)
292
+ try {
293
+ if (fs.existsSync(path.join(this.markersDir, agentType))) return true;
294
+ } catch {}
295
+
296
+ // Check JSON markers file
297
+ try {
298
+ if (fs.existsSync(this.markersFile)) {
299
+ const data = JSON.parse(fs.readFileSync(this.markersFile, 'utf-8'));
300
+ if (Array.isArray(data) && data.includes(agentType)) return true;
301
+ }
302
+ } catch {}
303
+
304
+ return false;
305
+ }
306
+
307
+ _markInstalled(agentType) {
308
+ // JSON file
309
+ try {
310
+ fs.mkdirSync(this.configDir, { recursive: true });
311
+ let markers = [];
312
+ try {
313
+ markers = JSON.parse(fs.readFileSync(this.markersFile, 'utf-8'));
314
+ if (!Array.isArray(markers)) markers = [];
315
+ } catch {}
316
+ if (!markers.includes(agentType)) {
317
+ markers.push(agentType);
318
+ markers.sort();
319
+ }
320
+ fs.writeFileSync(this.markersFile, JSON.stringify(markers), 'utf-8');
321
+ } catch {}
322
+
323
+ // Per-agent marker file
324
+ try {
325
+ fs.mkdirSync(this.markersDir, { recursive: true });
326
+ fs.writeFileSync(path.join(this.markersDir, agentType), '', 'utf-8');
327
+ } catch {}
328
+ }
329
+
330
+ _markUninstalled(agentType) {
331
+ // JSON file
332
+ try {
333
+ if (fs.existsSync(this.markersFile)) {
334
+ let markers = JSON.parse(fs.readFileSync(this.markersFile, 'utf-8'));
335
+ if (Array.isArray(markers)) {
336
+ markers = markers.filter((m) => m !== agentType);
337
+ fs.writeFileSync(this.markersFile, JSON.stringify(markers), 'utf-8');
338
+ }
339
+ }
340
+ } catch {}
341
+
342
+ // Per-agent marker file
343
+ try {
344
+ const markerFile = path.join(this.markersDir, agentType);
345
+ if (fs.existsSync(markerFile)) fs.unlinkSync(markerFile);
346
+ } catch {}
347
+ }
348
+
349
+ // -- Shell env + exec --
350
+
351
+ _buildShellEnv() {
352
+ const env = { ...process.env };
353
+ const sep = process.platform === 'win32' ? ';' : ':';
354
+ const extraDirs = [];
355
+ try { extraDirs.push(path.dirname(process.execPath)); } catch {}
356
+
357
+ // Check for bundled Node.js in ~/.openagents/nodejs/
358
+ if (process.platform === 'win32') {
359
+ try {
360
+ const bundledDir = path.join(this.configDir, 'nodejs');
361
+ if (fs.existsSync(bundledDir)) {
362
+ const entries = fs.readdirSync(bundledDir).filter(e => e.startsWith('node-'));
363
+ if (entries.length > 0) {
364
+ extraDirs.push(path.join(bundledDir, entries[0]));
365
+ }
366
+ }
367
+ } catch {}
368
+ const appData = env.APPDATA || '';
369
+ if (appData) extraDirs.push(path.join(appData, 'npm'));
370
+ extraDirs.push(env.ProgramFiles ? path.join(env.ProgramFiles, 'nodejs') : 'C:\\Program Files\\nodejs');
371
+ extraDirs.push(env.SystemRoot ? path.join(env.SystemRoot, 'System32') : 'C:\\Windows\\System32');
372
+ extraDirs.push(env.ProgramFiles ? path.join(env.ProgramFiles, 'Git', 'cmd') : 'C:\\Program Files\\Git\\cmd');
373
+ } else {
374
+ extraDirs.push('/usr/local/bin', '/opt/homebrew/bin');
375
+ }
376
+ for (const d of extraDirs) {
377
+ if (d && !(env.PATH || '').includes(d)) {
378
+ env.PATH = d + sep + (env.PATH || '');
379
+ }
380
+ }
381
+ return env;
382
+ }
383
+
384
+ /**
385
+ * Check if Node.js/npm is available on the system.
386
+ */
387
+ hasNodejs() {
388
+ if (whichBinary('node') && whichBinary('npm')) return true;
389
+ // Check bundled Node.js in ~/.openagents/nodejs/
390
+ if (process.platform === 'win32') {
391
+ try {
392
+ const bundledDir = path.join(this.configDir, 'nodejs');
393
+ if (fs.existsSync(bundledDir)) {
394
+ const entries = fs.readdirSync(bundledDir).filter(e => e.startsWith('node-'));
395
+ if (entries.length > 0) {
396
+ const nodeExe = path.join(bundledDir, entries[0], 'node.exe');
397
+ return fs.existsSync(nodeExe);
398
+ }
399
+ }
400
+ } catch {}
401
+ }
402
+ return false;
403
+ }
404
+
405
+ /**
406
+ * Download and install Node.js LTS. Streams progress via onData callback.
407
+ * After install, updates PATH so npm is available for subsequent commands.
408
+ * @param {function(string)} onData
409
+ * @returns {Promise<void>}
410
+ */
411
+ async installNodejs(onData) {
412
+ const { spawn: spawnProc } = require('child_process');
413
+ const https = require('https');
414
+ const os = require('os');
415
+ const nodeVersion = 'v22.14.0';
416
+ const plat = Installer.platform();
417
+
418
+ if (onData) onData(`Node.js not found. Installing Node.js ${nodeVersion}...\n\n`);
419
+
420
+ if (plat === 'windows') {
421
+ // Download portable zip — no admin required
422
+ const arch = os.arch() === 'x64' ? 'x64' : 'x86';
423
+ const url = `https://nodejs.org/dist/${nodeVersion}/node-${nodeVersion}-win-${arch}.zip`;
424
+ const zipPath = path.join(os.tmpdir(), `node-${nodeVersion}.zip`);
425
+
426
+ if (onData) onData(`Downloading ${url}...\n`);
427
+ await this._downloadFile(url, zipPath, onData);
428
+
429
+ // Extract to ~/.openagents/nodejs/
430
+ const nodejsDir = path.join(this.configDir, 'nodejs');
431
+ fs.mkdirSync(nodejsDir, { recursive: true });
432
+
433
+ if (onData) onData(`\nExtracting Node.js to ${nodejsDir}...\n`);
434
+ await new Promise((resolve, reject) => {
435
+ const proc = spawnProc('powershell', [
436
+ '-NoProfile', '-Command',
437
+ `Expand-Archive -Path '${zipPath}' -DestinationPath '${nodejsDir}' -Force`
438
+ ], { stdio: ['ignore', 'pipe', 'pipe'] });
439
+ if (proc.stdout) proc.stdout.on('data', (d) => { if (onData) onData(d.toString()); });
440
+ if (proc.stderr) proc.stderr.on('data', (d) => { if (onData) onData(d.toString()); });
441
+ proc.on('error', reject);
442
+ proc.on('close', (code) => {
443
+ if (code === 0) resolve();
444
+ else reject(new Error(`Extraction failed with code ${code}`));
445
+ });
446
+ });
447
+
448
+ // The zip extracts to node-vX.X.X-win-x64/ subfolder
449
+ const extractedDir = path.join(nodejsDir, `node-${nodeVersion}-win-${arch}`);
450
+ const sep = ';';
451
+
452
+ // Add extracted node dir to PATH for this session
453
+ if (!(process.env.PATH || '').includes(extractedDir)) {
454
+ process.env.PATH = extractedDir + sep + (process.env.PATH || '');
455
+ }
456
+ // npm global installs go to %APPDATA%\npm
457
+ const npmGlobal = path.join(process.env.APPDATA || '', 'npm');
458
+ if (npmGlobal && !(process.env.PATH || '').includes(npmGlobal)) {
459
+ process.env.PATH = npmGlobal + sep + process.env.PATH;
460
+ }
461
+
462
+ } else {
463
+ // macOS / Linux: download portable tar.gz/tar.xz, extract to ~/.openagents/nodejs/
464
+ const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
465
+ const ext = plat === 'macos' ? 'tar.gz' : 'tar.xz';
466
+ const platName = plat === 'macos' ? 'darwin' : 'linux';
467
+ const url = `https://nodejs.org/dist/${nodeVersion}/node-${nodeVersion}-${platName}-${arch}.${ext}`;
468
+ const tarPath = path.join(os.tmpdir(), `node-${nodeVersion}.${ext}`);
469
+
470
+ if (onData) onData(`Downloading ${url}...\n`);
471
+ await this._downloadFile(url, tarPath, onData);
472
+
473
+ const nodeDir = path.join(this.configDir, 'nodejs');
474
+ fs.mkdirSync(nodeDir, { recursive: true });
475
+
476
+ if (onData) onData(`\nExtracting to ${nodeDir}...\n`);
477
+ const tarFlag = ext === 'tar.gz' ? '-xzf' : '-xJf';
478
+ await new Promise((resolve, reject) => {
479
+ const proc = spawnProc('tar', [tarFlag, tarPath, '-C', nodeDir, '--strip-components=1'], {
480
+ stdio: ['ignore', 'pipe', 'pipe'],
481
+ });
482
+ if (proc.stdout) proc.stdout.on('data', (d) => { if (onData) onData(d.toString()); });
483
+ if (proc.stderr) proc.stderr.on('data', (d) => { if (onData) onData(d.toString()); });
484
+ proc.on('error', reject);
485
+ proc.on('close', (code) => {
486
+ if (code === 0) resolve();
487
+ else reject(new Error(`Extraction failed with code ${code}`));
488
+ });
489
+ });
490
+
491
+ // Add portable node bin to PATH
492
+ const nodeBin = path.join(nodeDir, 'bin');
493
+ if (!(process.env.PATH || '').includes(nodeBin)) {
494
+ process.env.PATH = nodeBin + ':' + (process.env.PATH || '');
495
+ }
496
+ }
497
+
498
+ if (onData) onData(`\nNode.js ${nodeVersion} installed successfully.\n\n`);
499
+ }
500
+
501
+ /**
502
+ * Download a file with progress reporting.
503
+ */
504
+ _downloadFile(url, destPath, onData) {
505
+ const https = require('https');
506
+ const http = require('http');
507
+ return new Promise((resolve, reject) => {
508
+ const get = url.startsWith('https') ? https.get : http.get;
509
+ get(url, (res) => {
510
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
511
+ // Follow redirect
512
+ return this._downloadFile(res.headers.location, destPath, onData).then(resolve, reject);
513
+ }
514
+ if (res.statusCode !== 200) {
515
+ return reject(new Error(`Download failed: HTTP ${res.statusCode}`));
516
+ }
517
+ const totalBytes = parseInt(res.headers['content-length'] || '0', 10);
518
+ let downloaded = 0;
519
+ let lastPercent = -1;
520
+ const file = fs.createWriteStream(destPath);
521
+ res.on('data', (chunk) => {
522
+ downloaded += chunk.length;
523
+ if (totalBytes > 0) {
524
+ const pct = Math.floor((downloaded / totalBytes) * 100);
525
+ if (pct !== lastPercent && pct % 10 === 0) {
526
+ lastPercent = pct;
527
+ if (onData) onData(` ${pct}% (${(downloaded / 1024 / 1024).toFixed(1)} MB)\n`);
528
+ }
529
+ }
530
+ });
531
+ res.pipe(file);
532
+ file.on('finish', () => { file.close(); resolve(); });
533
+ file.on('error', reject);
534
+ }).on('error', reject);
535
+ });
536
+ }
537
+
538
+ /**
539
+ * Resolve the npm CLI command. Uses system npm if available.
540
+ */
541
+ _resolveNpmCommand(args) {
542
+ const systemNpm = whichBinary('npm');
543
+ const npmBin = systemNpm ? `"${systemNpm}"` : 'npm';
544
+
545
+ // On macOS/Linux, use a user-writable prefix to avoid sudo for global installs
546
+ if (process.platform !== 'win32' && args.includes('-g')) {
547
+ const globalDir = path.join(this.configDir, 'npm-global');
548
+ fs.mkdirSync(globalDir, { recursive: true });
549
+ // Add the bin dir to PATH so installed binaries are found
550
+ const binDir = path.join(globalDir, 'bin');
551
+ if (!(process.env.PATH || '').includes(binDir)) {
552
+ process.env.PATH = binDir + ':' + (process.env.PATH || '');
553
+ }
554
+ return `${npmBin} --prefix "${globalDir}" ${args}`;
555
+ }
556
+
557
+ return `${npmBin} ${args}`;
558
+ }
559
+
560
+ _execShell(cmd, timeoutMs = 300000) {
561
+ return new Promise((resolve, reject) => {
562
+ const env = this._buildShellEnv();
563
+
564
+ let shell = true;
565
+ if (process.platform === 'win32') {
566
+ shell = env.ComSpec || 'C:\\Windows\\System32\\cmd.exe';
567
+ }
568
+
569
+ exec(cmd, {
570
+ encoding: 'utf-8',
571
+ timeout: timeoutMs,
572
+ shell,
573
+ env,
574
+ }, (error, stdout, stderr) => {
575
+ const output = ((stdout || '') + '\n' + (stderr || '')).trim();
576
+ if (error) {
577
+ const err = new Error(output || error.message);
578
+ err.exitCode = error.code;
579
+ reject(err);
580
+ } else {
581
+ resolve(output);
582
+ }
583
+ });
584
+ });
585
+ }
586
+ }
587
+
588
+ module.exports = { Installer };