@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 +1 -1
- package/src/installer.js +185 -16
- package/src/paths.js +18 -2
package/package.json
CHANGED
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
|
-
|
|
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 =
|
|
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
|
-
|
|
239
|
-
let shellCmd = cmd;
|
|
240
|
-
let shellOpt = true;
|
|
410
|
+
let shell = true;
|
|
241
411
|
if (process.platform === 'win32') {
|
|
242
|
-
|
|
243
|
-
shellOpt = false; // we're specifying the shell ourselves
|
|
412
|
+
shell = env.ComSpec || 'C:\\Windows\\System32\\cmd.exe';
|
|
244
413
|
}
|
|
245
414
|
|
|
246
|
-
exec(
|
|
415
|
+
exec(cmd, {
|
|
247
416
|
encoding: 'utf-8',
|
|
248
417
|
timeout: timeoutMs,
|
|
249
|
-
shell
|
|
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)
|
|
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'));
|