@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/installer.js +154 -31
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-connector",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Agent management CLI and library for OpenAgents — install, configure, and run AI coding agents",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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
- // Resolve npm to use bundled node if system npm is not available
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
- * Resolve the npm CLI path. Prefers system npm, falls back to Electron's
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
- _resolveNpmCommand(args) {
373
- // 1. Try system npm
374
- const { whichBinary } = require('./paths');
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
- for (const p of candidates) {
395
- try {
396
- if (fs.existsSync(p)) {
397
- return `"${nodeExe}" "${p}" ${args}`;
398
- }
399
- } catch {}
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
- // 3. Last resort: just try npm and hope for the best
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