@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/installer.js +149 -34
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-connector",
3
- "version": "0.3.1",
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,48 +371,158 @@ class Installer {
366
371
  }
367
372
 
368
373
  /**
369
- * Resolve the npm CLI path. Prefers system npm, falls back to bundled
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
- _resolveNpmCommand(args) {
373
- // 1. Try system npm
374
- const { whichBinary } = require('./paths');
375
- const systemNpm = whichBinary('npm');
376
- if (systemNpm) return `"${systemNpm}" ${args}`;
376
+ hasNodejs() {
377
+ return !!whichBinary('node') && !!whichBinary('npm');
378
+ }
377
379
 
378
- // 2. Find bundled npm-cli.js (npm is a dependency of the Launcher)
379
- const nodeExe = process.execPath;
380
- const candidates = [];
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
- // Try require.resolve first works when npm is in node_modules
383
- try {
384
- candidates.push(require.resolve('npm/bin/npm-cli.js'));
385
- } catch {}
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
- // Search common locations relative to the app
388
- const searchRoots = [
389
- path.join(path.dirname(nodeExe), 'resources', 'app'), // packaged Electron
390
- path.join(path.dirname(nodeExe), 'resources', 'app.asar.unpacked'), // asar unpacked
391
- path.join(path.dirname(nodeExe), '..'), // portable exe temp dir
392
- process.cwd(), // dev mode
393
- ];
394
- for (const root of searchRoots) {
395
- candidates.push(path.join(root, 'node_modules', 'npm', 'bin', 'npm-cli.js'));
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
- // Also check system node_modules
399
- candidates.push(path.join(path.dirname(nodeExe), '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'));
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
- for (const p of candidates) {
403
- try {
404
- if (p && fs.existsSync(p)) {
405
- return `"${nodeExe}" "${p}" ${args}`;
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
- } catch {}
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
- // 3. Last resort
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