@openagents-org/agent-connector 0.3.1 → 0.3.3
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 +182 -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');
|
|
@@ -348,7 +353,18 @@ class Installer {
|
|
|
348
353
|
const sep = process.platform === 'win32' ? ';' : ':';
|
|
349
354
|
const extraDirs = [];
|
|
350
355
|
try { extraDirs.push(path.dirname(process.execPath)); } catch {}
|
|
356
|
+
|
|
357
|
+
// Check for bundled Node.js in ~/.openagents/nodejs/
|
|
351
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 {}
|
|
352
368
|
const appData = env.APPDATA || '';
|
|
353
369
|
if (appData) extraDirs.push(path.join(appData, 'npm'));
|
|
354
370
|
extraDirs.push(env.ProgramFiles ? path.join(env.ProgramFiles, 'nodejs') : 'C:\\Program Files\\nodejs');
|
|
@@ -366,48 +382,180 @@ class Installer {
|
|
|
366
382
|
}
|
|
367
383
|
|
|
368
384
|
/**
|
|
369
|
-
*
|
|
370
|
-
* npm module run via Electron's node. Works on machines without Node.js.
|
|
385
|
+
* Check if Node.js/npm is available on the system.
|
|
371
386
|
*/
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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();
|
|
377
417
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
+
});
|
|
381
447
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
} catch {}
|
|
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 = ';';
|
|
386
451
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
process.
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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 if (plat === 'macos') {
|
|
463
|
+
// Download pkg and install
|
|
464
|
+
const url = `https://nodejs.org/dist/${nodeVersion}/node-${nodeVersion}.pkg`;
|
|
465
|
+
const pkgPath = path.join(os.tmpdir(), `node-${nodeVersion}.pkg`);
|
|
466
|
+
|
|
467
|
+
if (onData) onData(`Downloading ${url}...\n`);
|
|
468
|
+
await this._downloadFile(url, pkgPath, onData);
|
|
469
|
+
|
|
470
|
+
if (onData) onData(`\nInstalling Node.js...\n`);
|
|
471
|
+
await new Promise((resolve, reject) => {
|
|
472
|
+
const proc = spawnProc('sudo', ['installer', '-pkg', pkgPath, '-target', '/'], {
|
|
473
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
474
|
+
});
|
|
475
|
+
if (proc.stdout) proc.stdout.on('data', (d) => { if (onData) onData(d.toString()); });
|
|
476
|
+
if (proc.stderr) proc.stderr.on('data', (d) => { if (onData) onData(d.toString()); });
|
|
477
|
+
proc.on('error', reject);
|
|
478
|
+
proc.on('close', (code) => {
|
|
479
|
+
if (code === 0) resolve();
|
|
480
|
+
else reject(new Error(`Installer exited with code ${code}`));
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Add to PATH
|
|
485
|
+
if (!(process.env.PATH || '').includes('/usr/local/bin')) {
|
|
486
|
+
process.env.PATH = '/usr/local/bin:' + (process.env.PATH || '');
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
} else {
|
|
490
|
+
// Linux: use NodeSource setup script
|
|
491
|
+
const url = `https://nodejs.org/dist/${nodeVersion}/node-${nodeVersion}-linux-x64.tar.xz`;
|
|
492
|
+
const tarPath = path.join(os.tmpdir(), `node-${nodeVersion}.tar.xz`);
|
|
493
|
+
|
|
494
|
+
if (onData) onData(`Downloading ${url}...\n`);
|
|
495
|
+
await this._downloadFile(url, tarPath, onData);
|
|
496
|
+
|
|
497
|
+
if (onData) onData(`\nExtracting to /usr/local...\n`);
|
|
498
|
+
await new Promise((resolve, reject) => {
|
|
499
|
+
const proc = spawnProc('sudo', ['tar', '-xJf', tarPath, '-C', '/usr/local', '--strip-components=1'], {
|
|
500
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
501
|
+
});
|
|
502
|
+
if (proc.stdout) proc.stdout.on('data', (d) => { if (onData) onData(d.toString()); });
|
|
503
|
+
if (proc.stderr) proc.stderr.on('data', (d) => { if (onData) onData(d.toString()); });
|
|
504
|
+
proc.on('error', reject);
|
|
505
|
+
proc.on('close', (code) => {
|
|
506
|
+
if (code === 0) resolve();
|
|
507
|
+
else reject(new Error(`Extraction failed with code ${code}`));
|
|
508
|
+
});
|
|
509
|
+
});
|
|
396
510
|
}
|
|
397
511
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
candidates.push(path.join(path.dirname(nodeExe), 'node_modules', 'npm', 'bin', 'npm-cli.js'));
|
|
512
|
+
if (onData) onData(`\nNode.js ${nodeVersion} installed successfully.\n\n`);
|
|
513
|
+
}
|
|
401
514
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
515
|
+
/**
|
|
516
|
+
* Download a file with progress reporting.
|
|
517
|
+
*/
|
|
518
|
+
_downloadFile(url, destPath, onData) {
|
|
519
|
+
const https = require('https');
|
|
520
|
+
const http = require('http');
|
|
521
|
+
return new Promise((resolve, reject) => {
|
|
522
|
+
const get = url.startsWith('https') ? https.get : http.get;
|
|
523
|
+
get(url, (res) => {
|
|
524
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
525
|
+
// Follow redirect
|
|
526
|
+
return this._downloadFile(res.headers.location, destPath, onData).then(resolve, reject);
|
|
406
527
|
}
|
|
407
|
-
|
|
408
|
-
|
|
528
|
+
if (res.statusCode !== 200) {
|
|
529
|
+
return reject(new Error(`Download failed: HTTP ${res.statusCode}`));
|
|
530
|
+
}
|
|
531
|
+
const totalBytes = parseInt(res.headers['content-length'] || '0', 10);
|
|
532
|
+
let downloaded = 0;
|
|
533
|
+
let lastPercent = -1;
|
|
534
|
+
const file = fs.createWriteStream(destPath);
|
|
535
|
+
res.on('data', (chunk) => {
|
|
536
|
+
downloaded += chunk.length;
|
|
537
|
+
if (totalBytes > 0) {
|
|
538
|
+
const pct = Math.floor((downloaded / totalBytes) * 100);
|
|
539
|
+
if (pct !== lastPercent && pct % 10 === 0) {
|
|
540
|
+
lastPercent = pct;
|
|
541
|
+
if (onData) onData(` ${pct}% (${(downloaded / 1024 / 1024).toFixed(1)} MB)\n`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
res.pipe(file);
|
|
546
|
+
file.on('finish', () => { file.close(); resolve(); });
|
|
547
|
+
file.on('error', reject);
|
|
548
|
+
}).on('error', reject);
|
|
549
|
+
});
|
|
550
|
+
}
|
|
409
551
|
|
|
410
|
-
|
|
552
|
+
/**
|
|
553
|
+
* Resolve the npm CLI command. Uses system npm if available.
|
|
554
|
+
*/
|
|
555
|
+
_resolveNpmCommand(args) {
|
|
556
|
+
const systemNpm = whichBinary('npm');
|
|
557
|
+
if (systemNpm) return `"${systemNpm}" ${args}`;
|
|
558
|
+
// Last resort — npm should be on PATH after installNodejs()
|
|
411
559
|
return `npm ${args}`;
|
|
412
560
|
}
|
|
413
561
|
|