@openagents-org/agent-connector 0.2.8 → 0.3.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-connector",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
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
@@ -84,16 +84,80 @@ class Installer {
84
84
  throw new Error(`No install definition for agent type: ${agentType}`);
85
85
  }
86
86
 
87
- const cmd = this._getInstallCommand(entry.install);
87
+ let cmd = this._getInstallCommand(entry.install);
88
88
  if (!cmd) {
89
89
  throw new Error(`No install command for ${agentType} on ${Installer.platform()}`);
90
90
  }
91
91
 
92
+ // Use bundled node/npm if system npm not available
93
+ if (cmd.startsWith('npm install')) {
94
+ const args = cmd.replace('npm install', 'install');
95
+ cmd = this._resolveNpmCommand(args);
96
+ }
97
+
92
98
  const output = await this._execShell(cmd);
93
99
  this._markInstalled(agentType);
94
100
  return { success: true, output };
95
101
  }
96
102
 
103
+ /**
104
+ * Install with streaming output via callback.
105
+ * @param {string} agentType
106
+ * @param {function(string)} onData - called with each chunk of output
107
+ * @returns {Promise<{success: boolean, command: string}>}
108
+ */
109
+ async installStreaming(agentType, onData) {
110
+ const { spawn } = require('child_process');
111
+ const entry = this.registry.getEntry(agentType);
112
+ if (!entry || !entry.install) {
113
+ throw new Error(`No install definition for agent type: ${agentType}`);
114
+ }
115
+
116
+ let rawCmd = this._getInstallCommand(entry.install);
117
+ if (!rawCmd) {
118
+ throw new Error(`No install command for ${agentType} on ${Installer.platform()}`);
119
+ }
120
+
121
+ // Resolve npm to use bundled node if system npm is not available
122
+ let cmd = rawCmd;
123
+ if (rawCmd.startsWith('npm install')) {
124
+ const args = rawCmd.replace('npm install', 'install --loglevel=verbose');
125
+ cmd = this._resolveNpmCommand(args);
126
+ } else if (rawCmd.startsWith('pip install') || rawCmd.startsWith('pipx install')) {
127
+ cmd = rawCmd; // pip commands stay as-is
128
+ }
129
+
130
+ if (onData) onData(`$ ${cmd}\n\n`);
131
+
132
+ const env = this._buildShellEnv();
133
+ const shell = process.platform === 'win32'
134
+ ? (process.env.ComSpec || 'C:\\Windows\\System32\\cmd.exe')
135
+ : true;
136
+
137
+ return new Promise((resolve, reject) => {
138
+ const proc = spawn(cmd, [], { shell, env, stdio: ['ignore', 'pipe', 'pipe'] });
139
+
140
+ if (proc.stdout) proc.stdout.setEncoding('utf-8');
141
+ if (proc.stderr) proc.stderr.setEncoding('utf-8');
142
+
143
+ if (proc.stdout) proc.stdout.on('data', (d) => { if (onData) onData(d); });
144
+ if (proc.stderr) proc.stderr.on('data', (d) => { if (onData) onData(d); });
145
+
146
+ proc.on('error', (err) => reject(err));
147
+ proc.on('close', (code) => {
148
+ if (code === 0) {
149
+ this._markInstalled(agentType);
150
+ if (onData) onData(`\nDone! ${agentType} is now installed.\n`);
151
+ resolve({ success: true, command: cmd });
152
+ } else {
153
+ const msg = `Install failed with exit code ${code}`;
154
+ if (onData) onData(`\n${msg}\n`);
155
+ reject(new Error(msg));
156
+ }
157
+ });
158
+ });
159
+ }
160
+
97
161
  /**
98
162
  * Uninstall an agent runtime.
99
163
  * @returns {Promise<{success: boolean, output: string}>}
@@ -115,6 +179,60 @@ class Installer {
115
179
  return { success: true, output };
116
180
  }
117
181
 
182
+ /**
183
+ * Uninstall with streaming output via callback.
184
+ */
185
+ async uninstallStreaming(agentType, onData) {
186
+ const { spawn } = require('child_process');
187
+ const entry = this.registry.getEntry(agentType);
188
+ if (!entry || !entry.install) {
189
+ throw new Error(`No install definition for agent type: ${agentType}`);
190
+ }
191
+
192
+ const installCmd = this._getInstallCommand(entry.install);
193
+ let rawCmd = this._deriveUninstallCommand(installCmd);
194
+ if (!rawCmd) {
195
+ throw new Error(`Cannot derive uninstall command for ${agentType}`);
196
+ }
197
+
198
+ // Resolve npm to use bundled node if system npm is not available
199
+ let cmd = rawCmd;
200
+ if (rawCmd.startsWith('npm uninstall')) {
201
+ const args = rawCmd.replace('npm uninstall', 'uninstall --loglevel=verbose');
202
+ cmd = this._resolveNpmCommand(args);
203
+ }
204
+
205
+ if (onData) onData(`$ ${cmd}\n\n`);
206
+
207
+ const env = this._buildShellEnv();
208
+ const shell = process.platform === 'win32'
209
+ ? (process.env.ComSpec || 'C:\\Windows\\System32\\cmd.exe')
210
+ : true;
211
+
212
+ return new Promise((resolve, reject) => {
213
+ const proc = spawn(cmd, [], { shell, env, stdio: ['ignore', 'pipe', 'pipe'] });
214
+
215
+ if (proc.stdout) proc.stdout.setEncoding('utf-8');
216
+ if (proc.stderr) proc.stderr.setEncoding('utf-8');
217
+
218
+ if (proc.stdout) proc.stdout.on('data', (d) => { if (onData) onData(d); });
219
+ if (proc.stderr) proc.stderr.on('data', (d) => { if (onData) onData(d); });
220
+
221
+ proc.on('error', (err) => reject(err));
222
+ proc.on('close', (code) => {
223
+ if (code === 0) {
224
+ this._markUninstalled(agentType);
225
+ if (onData) onData(`\nDone! ${agentType} has been uninstalled.\n`);
226
+ resolve({ success: true, command: cmd });
227
+ } else {
228
+ const msg = `Uninstall failed with exit code ${code}`;
229
+ if (onData) onData(`\n${msg}\n`);
230
+ reject(new Error(msg));
231
+ }
232
+ });
233
+ });
234
+ }
235
+
118
236
  /**
119
237
  * Get install command for current platform.
120
238
  */
@@ -223,30 +341,81 @@ class Installer {
223
341
  } catch {}
224
342
  }
225
343
 
226
- // -- Shell exec --
344
+ // -- Shell env + exec --
345
+
346
+ _buildShellEnv() {
347
+ const env = { ...process.env };
348
+ const sep = process.platform === 'win32' ? ';' : ':';
349
+ const extraDirs = [];
350
+ try { extraDirs.push(path.dirname(process.execPath)); } catch {}
351
+ if (process.platform === 'win32') {
352
+ const appData = env.APPDATA || '';
353
+ if (appData) extraDirs.push(path.join(appData, 'npm'));
354
+ extraDirs.push(env.ProgramFiles ? path.join(env.ProgramFiles, 'nodejs') : 'C:\\Program Files\\nodejs');
355
+ extraDirs.push(env.SystemRoot ? path.join(env.SystemRoot, 'System32') : 'C:\\Windows\\System32');
356
+ extraDirs.push(env.ProgramFiles ? path.join(env.ProgramFiles, 'Git', 'cmd') : 'C:\\Program Files\\Git\\cmd');
357
+ } else {
358
+ extraDirs.push('/usr/local/bin', '/opt/homebrew/bin');
359
+ }
360
+ for (const d of extraDirs) {
361
+ if (d && !(env.PATH || '').includes(d)) {
362
+ env.PATH = d + sep + (env.PATH || '');
363
+ }
364
+ }
365
+ return env;
366
+ }
367
+
368
+ /**
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.
371
+ */
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 {}
393
+
394
+ for (const p of candidates) {
395
+ try {
396
+ if (fs.existsSync(p)) {
397
+ return `"${nodeExe}" "${p}" ${args}`;
398
+ }
399
+ } catch {}
400
+ }
401
+
402
+ // 3. Last resort: just try npm and hope for the best
403
+ return `npm ${args}`;
404
+ }
227
405
 
228
406
  _execShell(cmd, timeoutMs = 300000) {
229
407
  return new Promise((resolve, reject) => {
230
- const env = getEnhancedEnv();
231
- // Also include Electron's own binary dir
232
- const execDir = path.dirname(process.execPath);
233
- if (execDir && !(env.PATH || '').includes(execDir)) {
234
- const sep = process.platform === 'win32' ? ';' : ':';
235
- env.PATH = execDir + sep + (env.PATH || '');
236
- }
408
+ const env = this._buildShellEnv();
237
409
 
238
- // On Windows, use cmd.exe explicitly with UTF-8 codepage
239
- let shellCmd = cmd;
240
- let shellOpt = true;
410
+ let shell = true;
241
411
  if (process.platform === 'win32') {
242
- shellCmd = `cmd.exe /C "chcp 65001 >nul & ${cmd}"`;
243
- shellOpt = false; // we're specifying the shell ourselves
412
+ shell = env.ComSpec || 'C:\\Windows\\System32\\cmd.exe';
244
413
  }
245
414
 
246
- exec(shellCmd, {
415
+ exec(cmd, {
247
416
  encoding: 'utf-8',
248
417
  timeout: timeoutMs,
249
- shell: shellOpt,
418
+ shell,
250
419
  env,
251
420
  }, (error, stdout, stderr) => {
252
421
  const output = ((stdout || '') + '\n' + (stderr || '')).trim();
package/src/paths.js CHANGED
@@ -36,11 +36,19 @@ function getExtraBinDirs() {
36
36
  // Common: ~/.local/bin (pipx, user installs)
37
37
  _push(dirs, path.join(HOME, '.local', 'bin'));
38
38
 
39
+ // Also add the directory containing the current node binary
40
+ try {
41
+ const nodeDir = path.dirname(process.execPath);
42
+ if (nodeDir) _push(dirs, nodeDir);
43
+ } catch {}
44
+
39
45
  // Filter to existing directories only, deduplicate
40
46
  const seen = new Set();
41
47
  const currentPATH = process.env.PATH || '';
42
48
  return dirs.filter(d => {
43
- if (!d || seen.has(d) || currentPATH.includes(d)) return false;
49
+ if (!d || seen.has(d)) return false;
50
+ // Skip if already in PATH (case-insensitive on Windows)
51
+ if (IS_WINDOWS ? currentPATH.toLowerCase().includes(d.toLowerCase()) : currentPATH.includes(d)) return false;
44
52
  seen.add(d);
45
53
  try {
46
54
  return fs.statSync(d).isDirectory();
@@ -71,10 +79,14 @@ function getEnhancedEnv(baseEnv) {
71
79
  }
72
80
  if (IS_WINDOWS) {
73
81
  // Force UTF-8 output from child processes on non-English Windows locales
74
- // (prevents GBK/Shift-JIS garbled text in error messages)
75
82
  env.PYTHONIOENCODING = env.PYTHONIOENCODING || 'utf-8';
76
83
  env.PYTHONUTF8 = env.PYTHONUTF8 || '1';
77
84
  env.LANG = env.LANG || 'en_US.UTF-8';
85
+ // Ensure ComSpec points to cmd.exe (Electron may not set it)
86
+ if (!env.ComSpec) {
87
+ const sysRoot = env.SystemRoot || 'C:\\Windows';
88
+ env.ComSpec = path.join(sysRoot, 'System32', 'cmd.exe');
89
+ }
78
90
  }
79
91
  return env;
80
92
  }
@@ -104,6 +116,10 @@ function _addWindowsPaths(dirs) {
104
116
  const appData = process.env.APPDATA || '';
105
117
  const localAppData = process.env.LOCALAPPDATA || '';
106
118
  const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
119
+ const sysRoot = process.env.SystemRoot || 'C:\\Windows';
120
+
121
+ // System32 (cmd.exe, powershell, etc) — Electron may not have it
122
+ _push(dirs, path.join(sysRoot, 'System32'));
107
123
 
108
124
  // npm global bin
109
125
  if (appData) _push(dirs, path.join(appData, 'npm'));