@openagents-org/agent-connector 0.3.1 → 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 +149 -34
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,48 +371,158 @@ class Installer {
|
|
|
366
371
|
}
|
|
367
372
|
|
|
368
373
|
/**
|
|
369
|
-
*
|
|
370
|
-
* npm module run via Electron's node. Works 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 `"${systemNpm}" ${args}`;
|
|
376
|
+
hasNodejs() {
|
|
377
|
+
return !!whichBinary('node') && !!whichBinary('npm');
|
|
378
|
+
}
|
|
377
379
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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();
|
|
381
392
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
+
});
|
|
386
417
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
+
});
|
|
396
477
|
}
|
|
397
478
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
candidates.push(path.join(path.dirname(nodeExe), 'node_modules', 'npm', 'bin', 'npm-cli.js'));
|
|
479
|
+
if (onData) onData(`\nNode.js ${nodeVersion} installed successfully.\n\n`);
|
|
480
|
+
}
|
|
401
481
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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);
|
|
406
494
|
}
|
|
407
|
-
|
|
408
|
-
|
|
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
|
+
}
|
|
409
518
|
|
|
410
|
-
|
|
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()
|
|
411
526
|
return `npm ${args}`;
|
|
412
527
|
}
|
|
413
528
|
|