@openagents-org/agent-connector 0.3.0 → 0.3.2
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 +1 -1
- package/src/installer.js +154 -31
package/package.json
CHANGED
package/src/installer.js
CHANGED
|
@@ -118,7 +118,12 @@ class Installer {
|
|
|
118
118
|
throw new Error(`No install command for ${agentType} on ${Installer.platform()}`);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
//
|
|
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
|
|
122
127
|
let cmd = rawCmd;
|
|
123
128
|
if (rawCmd.startsWith('npm install')) {
|
|
124
129
|
const args = rawCmd.replace('npm install', 'install --loglevel=verbose');
|
|
@@ -366,40 +371,158 @@ class Installer {
|
|
|
366
371
|
}
|
|
367
372
|
|
|
368
373
|
/**
|
|
369
|
-
*
|
|
370
|
-
* bundled node + npm-cli.js so installs work on machines without Node.js.
|
|
374
|
+
* Check if Node.js/npm is available on the system.
|
|
371
375
|
*/
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const systemNpm = whichBinary('npm');
|
|
376
|
-
if (systemNpm) return `npm ${args}`;
|
|
377
|
-
|
|
378
|
-
// 2. Use Electron's bundled node to run npm-cli.js
|
|
379
|
-
const nodeExe = process.execPath;
|
|
380
|
-
// Look for npm-cli.js relative to the node binary
|
|
381
|
-
const candidates = [
|
|
382
|
-
// Electron on Windows: resources/app/node_modules/npm/bin/npm-cli.js
|
|
383
|
-
path.join(path.dirname(nodeExe), 'resources', 'app', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
384
|
-
// npm installed alongside node
|
|
385
|
-
path.join(path.dirname(nodeExe), '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
386
|
-
path.join(path.dirname(nodeExe), 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
387
|
-
];
|
|
388
|
-
// Also check if npm is available as a module from the current process
|
|
389
|
-
try {
|
|
390
|
-
const npmCliPath = require.resolve('npm/bin/npm-cli.js');
|
|
391
|
-
if (npmCliPath) candidates.unshift(npmCliPath);
|
|
392
|
-
} catch {}
|
|
376
|
+
hasNodejs() {
|
|
377
|
+
return !!whichBinary('node') && !!whichBinary('npm');
|
|
378
|
+
}
|
|
393
379
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
380
|
+
/**
|
|
381
|
+
* Download and install Node.js LTS. Streams progress via onData callback.
|
|
382
|
+
* After install, updates PATH so npm is available for subsequent commands.
|
|
383
|
+
* @param {function(string)} onData
|
|
384
|
+
* @returns {Promise<void>}
|
|
385
|
+
*/
|
|
386
|
+
async installNodejs(onData) {
|
|
387
|
+
const { spawn: spawnProc } = require('child_process');
|
|
388
|
+
const https = require('https');
|
|
389
|
+
const os = require('os');
|
|
390
|
+
const nodeVersion = 'v22.14.0';
|
|
391
|
+
const plat = Installer.platform();
|
|
392
|
+
|
|
393
|
+
if (onData) onData(`Node.js not found. Installing Node.js ${nodeVersion}...\n\n`);
|
|
394
|
+
|
|
395
|
+
if (plat === 'windows') {
|
|
396
|
+
// Download MSI installer and run silently
|
|
397
|
+
const arch = os.arch() === 'x64' ? 'x64' : 'x86';
|
|
398
|
+
const url = `https://nodejs.org/dist/${nodeVersion}/node-${nodeVersion}-${arch}.msi`;
|
|
399
|
+
const msiPath = path.join(os.tmpdir(), `node-${nodeVersion}.msi`);
|
|
400
|
+
|
|
401
|
+
if (onData) onData(`Downloading ${url}...\n`);
|
|
402
|
+
await this._downloadFile(url, msiPath, onData);
|
|
403
|
+
|
|
404
|
+
if (onData) onData(`\nInstalling Node.js (this may take a minute)...\n`);
|
|
405
|
+
await new Promise((resolve, reject) => {
|
|
406
|
+
const proc = spawnProc('msiexec', ['/i', msiPath, '/quiet', '/norestart'], {
|
|
407
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
408
|
+
});
|
|
409
|
+
if (proc.stdout) proc.stdout.on('data', (d) => { if (onData) onData(d.toString()); });
|
|
410
|
+
if (proc.stderr) proc.stderr.on('data', (d) => { if (onData) onData(d.toString()); });
|
|
411
|
+
proc.on('error', reject);
|
|
412
|
+
proc.on('close', (code) => {
|
|
413
|
+
if (code === 0) resolve();
|
|
414
|
+
else reject(new Error(`MSI installer exited with code ${code}`));
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Add to PATH for this session
|
|
419
|
+
const nodejsDir = path.join(process.env.ProgramFiles || 'C:\\Program Files', 'nodejs');
|
|
420
|
+
const sep = ';';
|
|
421
|
+
if (!(process.env.PATH || '').includes(nodejsDir)) {
|
|
422
|
+
process.env.PATH = nodejsDir + sep + (process.env.PATH || '');
|
|
423
|
+
}
|
|
424
|
+
const npmGlobal = path.join(process.env.APPDATA || '', 'npm');
|
|
425
|
+
if (npmGlobal && !(process.env.PATH || '').includes(npmGlobal)) {
|
|
426
|
+
process.env.PATH = npmGlobal + sep + process.env.PATH;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
} else if (plat === 'macos') {
|
|
430
|
+
// Download pkg and install
|
|
431
|
+
const url = `https://nodejs.org/dist/${nodeVersion}/node-${nodeVersion}.pkg`;
|
|
432
|
+
const pkgPath = path.join(os.tmpdir(), `node-${nodeVersion}.pkg`);
|
|
433
|
+
|
|
434
|
+
if (onData) onData(`Downloading ${url}...\n`);
|
|
435
|
+
await this._downloadFile(url, pkgPath, onData);
|
|
436
|
+
|
|
437
|
+
if (onData) onData(`\nInstalling Node.js...\n`);
|
|
438
|
+
await new Promise((resolve, reject) => {
|
|
439
|
+
const proc = spawnProc('sudo', ['installer', '-pkg', pkgPath, '-target', '/'], {
|
|
440
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
441
|
+
});
|
|
442
|
+
if (proc.stdout) proc.stdout.on('data', (d) => { if (onData) onData(d.toString()); });
|
|
443
|
+
if (proc.stderr) proc.stderr.on('data', (d) => { if (onData) onData(d.toString()); });
|
|
444
|
+
proc.on('error', reject);
|
|
445
|
+
proc.on('close', (code) => {
|
|
446
|
+
if (code === 0) resolve();
|
|
447
|
+
else reject(new Error(`Installer exited with code ${code}`));
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// Add to PATH
|
|
452
|
+
if (!(process.env.PATH || '').includes('/usr/local/bin')) {
|
|
453
|
+
process.env.PATH = '/usr/local/bin:' + (process.env.PATH || '');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
} else {
|
|
457
|
+
// Linux: use NodeSource setup script
|
|
458
|
+
const url = `https://nodejs.org/dist/${nodeVersion}/node-${nodeVersion}-linux-x64.tar.xz`;
|
|
459
|
+
const tarPath = path.join(os.tmpdir(), `node-${nodeVersion}.tar.xz`);
|
|
460
|
+
|
|
461
|
+
if (onData) onData(`Downloading ${url}...\n`);
|
|
462
|
+
await this._downloadFile(url, tarPath, onData);
|
|
463
|
+
|
|
464
|
+
if (onData) onData(`\nExtracting to /usr/local...\n`);
|
|
465
|
+
await new Promise((resolve, reject) => {
|
|
466
|
+
const proc = spawnProc('sudo', ['tar', '-xJf', tarPath, '-C', '/usr/local', '--strip-components=1'], {
|
|
467
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
468
|
+
});
|
|
469
|
+
if (proc.stdout) proc.stdout.on('data', (d) => { if (onData) onData(d.toString()); });
|
|
470
|
+
if (proc.stderr) proc.stderr.on('data', (d) => { if (onData) onData(d.toString()); });
|
|
471
|
+
proc.on('error', reject);
|
|
472
|
+
proc.on('close', (code) => {
|
|
473
|
+
if (code === 0) resolve();
|
|
474
|
+
else reject(new Error(`Extraction failed with code ${code}`));
|
|
475
|
+
});
|
|
476
|
+
});
|
|
400
477
|
}
|
|
401
478
|
|
|
402
|
-
|
|
479
|
+
if (onData) onData(`\nNode.js ${nodeVersion} installed successfully.\n\n`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Download a file with progress reporting.
|
|
484
|
+
*/
|
|
485
|
+
_downloadFile(url, destPath, onData) {
|
|
486
|
+
const https = require('https');
|
|
487
|
+
const http = require('http');
|
|
488
|
+
return new Promise((resolve, reject) => {
|
|
489
|
+
const get = url.startsWith('https') ? https.get : http.get;
|
|
490
|
+
get(url, (res) => {
|
|
491
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
492
|
+
// Follow redirect
|
|
493
|
+
return this._downloadFile(res.headers.location, destPath, onData).then(resolve, reject);
|
|
494
|
+
}
|
|
495
|
+
if (res.statusCode !== 200) {
|
|
496
|
+
return reject(new Error(`Download failed: HTTP ${res.statusCode}`));
|
|
497
|
+
}
|
|
498
|
+
const totalBytes = parseInt(res.headers['content-length'] || '0', 10);
|
|
499
|
+
let downloaded = 0;
|
|
500
|
+
let lastPercent = -1;
|
|
501
|
+
const file = fs.createWriteStream(destPath);
|
|
502
|
+
res.on('data', (chunk) => {
|
|
503
|
+
downloaded += chunk.length;
|
|
504
|
+
if (totalBytes > 0) {
|
|
505
|
+
const pct = Math.floor((downloaded / totalBytes) * 100);
|
|
506
|
+
if (pct !== lastPercent && pct % 10 === 0) {
|
|
507
|
+
lastPercent = pct;
|
|
508
|
+
if (onData) onData(` ${pct}% (${(downloaded / 1024 / 1024).toFixed(1)} MB)\n`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
res.pipe(file);
|
|
513
|
+
file.on('finish', () => { file.close(); resolve(); });
|
|
514
|
+
file.on('error', reject);
|
|
515
|
+
}).on('error', reject);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Resolve the npm CLI command. Uses system npm if available.
|
|
521
|
+
*/
|
|
522
|
+
_resolveNpmCommand(args) {
|
|
523
|
+
const systemNpm = whichBinary('npm');
|
|
524
|
+
if (systemNpm) return `"${systemNpm}" ${args}`;
|
|
525
|
+
// Last resort — npm should be on PATH after installNodejs()
|
|
403
526
|
return `npm ${args}`;
|
|
404
527
|
}
|
|
405
528
|
|