@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/installer.js +182 -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.3",
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');
@@ -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
- * 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.
385
+ * Check if Node.js/npm is available on the system.
371
386
  */
372
- _resolveNpmCommand(args) {
373
- // 1. Try system npm
374
- const { whichBinary } = require('./paths');
375
- const systemNpm = whichBinary('npm');
376
- if (systemNpm) return `"${systemNpm}" ${args}`;
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
- // 2. Find bundled npm-cli.js (npm is a dependency of the Launcher)
379
- const nodeExe = process.execPath;
380
- const candidates = [];
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
- // 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 {}
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
- // 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'));
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
- // 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'));
512
+ if (onData) onData(`\nNode.js ${nodeVersion} installed successfully.\n\n`);
513
+ }
401
514
 
402
- for (const p of candidates) {
403
- try {
404
- if (p && fs.existsSync(p)) {
405
- return `"${nodeExe}" "${p}" ${args}`;
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
- } catch {}
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
- // 3. Last resort
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