@openagents-org/agent-connector 0.2.5 → 0.2.7
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 +8 -2
- package/src/config.js +32 -1
- package/src/daemon.js +47 -17
- package/src/index.js +6 -1
- package/src/installer.js +41 -45
- package/src/paths.js +259 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openagents-org/agent-connector",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
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": {
|
|
@@ -11,7 +11,13 @@
|
|
|
11
11
|
"build:registry": "node scripts/build-registry.js",
|
|
12
12
|
"lint": "eslint src/"
|
|
13
13
|
},
|
|
14
|
-
"keywords": [
|
|
14
|
+
"keywords": [
|
|
15
|
+
"openagents",
|
|
16
|
+
"ai",
|
|
17
|
+
"agent",
|
|
18
|
+
"coding",
|
|
19
|
+
"cli"
|
|
20
|
+
],
|
|
15
21
|
"author": "OpenAgents",
|
|
16
22
|
"license": "MIT",
|
|
17
23
|
"repository": {
|
package/src/config.js
CHANGED
|
@@ -149,7 +149,7 @@ class Config {
|
|
|
149
149
|
try {
|
|
150
150
|
if (!fs.existsSync(this.logFile)) return [];
|
|
151
151
|
const content = fs.readFileSync(this.logFile, 'utf-8');
|
|
152
|
-
let allLines = content.split('\n');
|
|
152
|
+
let allLines = content.split('\n').filter(Boolean);
|
|
153
153
|
if (agentName) {
|
|
154
154
|
allLines = allLines.filter(
|
|
155
155
|
(l) => l.includes(agentName) || l.includes('daemon') || l.includes('Daemon')
|
|
@@ -160,6 +160,37 @@ class Config {
|
|
|
160
160
|
return [];
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Tail log file with optional filter. Returns { lines, size }.
|
|
166
|
+
* @param {Object} opts - { agent, lines, offset }
|
|
167
|
+
* @param {string} [opts.agent] - Filter by agent name
|
|
168
|
+
* @param {number} [opts.lines=100] - Number of lines to return
|
|
169
|
+
* @param {number} [opts.offset=0] - Byte offset for incremental reads (0 = from end)
|
|
170
|
+
*/
|
|
171
|
+
tailLogs(opts = {}) {
|
|
172
|
+
const { agent, lines = 100, offset = 0 } = opts;
|
|
173
|
+
try {
|
|
174
|
+
if (!fs.existsSync(this.logFile)) return { lines: [], size: 0 };
|
|
175
|
+
const stat = fs.statSync(this.logFile);
|
|
176
|
+
if (offset > 0 && offset < stat.size) {
|
|
177
|
+
// Incremental read from offset
|
|
178
|
+
const fd = fs.openSync(this.logFile, 'r');
|
|
179
|
+
const buf = Buffer.alloc(stat.size - offset);
|
|
180
|
+
fs.readSync(fd, buf, 0, buf.length, offset);
|
|
181
|
+
fs.closeSync(fd);
|
|
182
|
+
let newLines = buf.toString('utf-8').split('\n').filter(Boolean);
|
|
183
|
+
if (agent) {
|
|
184
|
+
newLines = newLines.filter(l => l.includes(agent) || l.includes('Daemon'));
|
|
185
|
+
}
|
|
186
|
+
return { lines: newLines, size: stat.size };
|
|
187
|
+
}
|
|
188
|
+
// Full read, return last N
|
|
189
|
+
return { lines: this.getLogs(agent, lines), size: stat.size };
|
|
190
|
+
} catch {
|
|
191
|
+
return { lines: [], size: 0 };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
163
194
|
}
|
|
164
195
|
|
|
165
196
|
// -- YAML parser (compatible with Python SDK's daemon.yaml format) --
|
package/src/daemon.js
CHANGED
|
@@ -5,8 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const { spawn, execSync, execFileSync } = require('child_process');
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const { WorkspaceClient } = require('./workspace-client');
|
|
8
|
-
|
|
9
|
-
const IS_WINDOWS = process.platform === 'win32';
|
|
8
|
+
const { getEnhancedEnv, whichBinary, IS_WINDOWS } = require('./paths');
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* Agent process lifecycle manager.
|
|
@@ -63,7 +62,11 @@ class Daemon {
|
|
|
63
62
|
this._processCommands();
|
|
64
63
|
}, 5000);
|
|
65
64
|
|
|
65
|
+
// Watch config file for hot-reload
|
|
66
|
+
this._watchConfig();
|
|
67
|
+
|
|
66
68
|
this._writeStatus();
|
|
69
|
+
this._cachedAgentNames = new Set(agents.map(a => a.name));
|
|
67
70
|
this._log(`Daemon started with ${agents.length} agent(s)`);
|
|
68
71
|
|
|
69
72
|
// Block until shutdown
|
|
@@ -82,6 +85,7 @@ class Daemon {
|
|
|
82
85
|
|
|
83
86
|
if (this._statusInterval) clearInterval(this._statusInterval);
|
|
84
87
|
if (this._cmdInterval) clearInterval(this._cmdInterval);
|
|
88
|
+
if (this._configWatcher) { try { this._configWatcher.close(); } catch {} }
|
|
85
89
|
|
|
86
90
|
// Kill all child processes
|
|
87
91
|
const kills = Object.keys(this._processes).map((name) =>
|
|
@@ -418,22 +422,22 @@ class Daemon {
|
|
|
418
422
|
const [binary, ...args] = cmd;
|
|
419
423
|
const spawnOpts = {
|
|
420
424
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
421
|
-
env: opts.env,
|
|
425
|
+
env: getEnhancedEnv(opts.env),
|
|
422
426
|
cwd: opts.cwd,
|
|
423
427
|
};
|
|
424
428
|
|
|
425
429
|
if (IS_WINDOWS) {
|
|
426
430
|
// On Windows, always use shell so .cmd/.ps1 shims on PATH are found
|
|
431
|
+
// Use cmd /c with chcp 65001 to force UTF-8 output (fixes GBK garbled text)
|
|
427
432
|
spawnOpts.shell = true;
|
|
428
|
-
// Ensure npm global bin is on PATH
|
|
429
|
-
const npmBin = path.join(process.env.APPDATA || '', 'npm');
|
|
430
|
-
if (npmBin && !(process.env.PATH || '').includes(npmBin)) {
|
|
431
|
-
spawnOpts.env = { ...spawnOpts.env, PATH: npmBin + ';' + (spawnOpts.env.PATH || process.env.PATH || '') };
|
|
432
|
-
}
|
|
433
433
|
}
|
|
434
434
|
|
|
435
435
|
const proc = spawn(binary, args, spawnOpts);
|
|
436
436
|
|
|
437
|
+
// Force UTF-8 decoding on stdout/stderr
|
|
438
|
+
if (proc.stdout) proc.stdout.setEncoding('utf-8');
|
|
439
|
+
if (proc.stderr) proc.stderr.setEncoding('utf-8');
|
|
440
|
+
|
|
437
441
|
// Merge stderr into stdout handler
|
|
438
442
|
if (proc.stderr) {
|
|
439
443
|
proc.stderr.on('data', (chunk) => {
|
|
@@ -555,28 +559,41 @@ class Daemon {
|
|
|
555
559
|
} catch {}
|
|
556
560
|
}
|
|
557
561
|
|
|
562
|
+
_watchConfig() {
|
|
563
|
+
try {
|
|
564
|
+
let debounce = null;
|
|
565
|
+
this._configWatcher = fs.watch(this.config.configFile, () => {
|
|
566
|
+
if (debounce) clearTimeout(debounce);
|
|
567
|
+
debounce = setTimeout(() => this._reload(), 1000);
|
|
568
|
+
});
|
|
569
|
+
this._configWatcher.on('error', () => {});
|
|
570
|
+
} catch {}
|
|
571
|
+
}
|
|
572
|
+
|
|
558
573
|
_reload() {
|
|
559
574
|
this._log('Reloading config...');
|
|
560
|
-
const
|
|
561
|
-
// Re-read config from disk
|
|
562
|
-
const newAgents =
|
|
575
|
+
const oldNames = this._cachedAgentNames || new Set();
|
|
576
|
+
// Re-read config from disk
|
|
577
|
+
const newAgents = this.config.getAgents();
|
|
578
|
+
const newNames = new Set(newAgents.map(a => a.name));
|
|
563
579
|
|
|
564
580
|
// Stop removed agents
|
|
565
|
-
for (const name of
|
|
566
|
-
if (!
|
|
581
|
+
for (const name of oldNames) {
|
|
582
|
+
if (!newNames.has(name)) {
|
|
567
583
|
this.stopAgent(name);
|
|
568
584
|
this._log(`Reload: stopped removed agent '${name}'`);
|
|
569
585
|
}
|
|
570
586
|
}
|
|
571
587
|
|
|
572
588
|
// Start new agents
|
|
573
|
-
for (const
|
|
574
|
-
if (!
|
|
589
|
+
for (const agent of newAgents) {
|
|
590
|
+
if (!oldNames.has(agent.name)) {
|
|
575
591
|
this._launchAgent(agent);
|
|
576
|
-
this._log(`Reload: started new agent '${name}'`);
|
|
592
|
+
this._log(`Reload: started new agent '${agent.name}'`);
|
|
577
593
|
}
|
|
578
594
|
}
|
|
579
595
|
|
|
596
|
+
this._cachedAgentNames = newNames;
|
|
580
597
|
this._writeStatus();
|
|
581
598
|
}
|
|
582
599
|
|
|
@@ -596,13 +613,26 @@ class Daemon {
|
|
|
596
613
|
const line = `${ts} INFO daemon: ${msg}`;
|
|
597
614
|
try {
|
|
598
615
|
fs.appendFileSync(this.config.logFile, line + '\n', 'utf-8');
|
|
616
|
+
this._maybeRotateLog();
|
|
599
617
|
} catch {}
|
|
600
618
|
if (!this._shuttingDown) {
|
|
601
|
-
// Also log to console when running in foreground
|
|
602
619
|
console.log(line);
|
|
603
620
|
}
|
|
604
621
|
}
|
|
605
622
|
|
|
623
|
+
_maybeRotateLog() {
|
|
624
|
+
// Rotate at 10MB, keep 1 backup
|
|
625
|
+
const MAX_SIZE = 10 * 1024 * 1024;
|
|
626
|
+
try {
|
|
627
|
+
const stat = fs.statSync(this.config.logFile);
|
|
628
|
+
if (stat.size > MAX_SIZE) {
|
|
629
|
+
const backup = this.config.logFile + '.1';
|
|
630
|
+
try { fs.unlinkSync(backup); } catch {}
|
|
631
|
+
fs.renameSync(this.config.logFile, backup);
|
|
632
|
+
}
|
|
633
|
+
} catch {}
|
|
634
|
+
}
|
|
635
|
+
|
|
606
636
|
_sleep(ms) {
|
|
607
637
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
608
638
|
}
|
package/src/index.js
CHANGED
|
@@ -56,6 +56,10 @@ class AgentConnector {
|
|
|
56
56
|
return this.installer.isInstalled(agentType);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
healthCheck(agentType) {
|
|
60
|
+
return this.installer.healthCheck(agentType);
|
|
61
|
+
}
|
|
62
|
+
|
|
59
63
|
// -- Agent CRUD --
|
|
60
64
|
|
|
61
65
|
listAgents() {
|
|
@@ -197,4 +201,5 @@ class AgentConnector {
|
|
|
197
201
|
|
|
198
202
|
const adapters = require('./adapters');
|
|
199
203
|
|
|
200
|
-
|
|
204
|
+
const paths = require('./paths');
|
|
205
|
+
module.exports = { AgentConnector, Daemon, WorkspaceClient, adapters, paths };
|
package/src/installer.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { execSync, exec } = require('child_process');
|
|
6
|
+
const { whichBinary, getEnhancedEnv } = require('./paths');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Manages installation and uninstallation of agent runtimes.
|
|
@@ -45,6 +46,34 @@ class Installer {
|
|
|
45
46
|
return this._whichBinary(agentType);
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Health check — binary existence + version.
|
|
51
|
+
* @returns {{ installed: boolean, binary: string|null, version: string|null }}
|
|
52
|
+
*/
|
|
53
|
+
healthCheck(agentType) {
|
|
54
|
+
const binary = this._whichBinary(agentType);
|
|
55
|
+
if (!binary) return { installed: false, binary: null, version: null };
|
|
56
|
+
|
|
57
|
+
const entry = this.registry.getEntry(agentType);
|
|
58
|
+
const checkCmd = entry && entry.install ? entry.install.check_command : null;
|
|
59
|
+
const versionCmd = checkCmd || `${entry && entry.install && entry.install.binary || agentType} --version`;
|
|
60
|
+
|
|
61
|
+
let version = null;
|
|
62
|
+
try {
|
|
63
|
+
const raw = execSync(versionCmd, {
|
|
64
|
+
encoding: 'utf-8',
|
|
65
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
66
|
+
env: getEnhancedEnv(),
|
|
67
|
+
timeout: 10000,
|
|
68
|
+
}).trim();
|
|
69
|
+
// Extract version number (e.g. "openclaw 2024.1.5" → "2024.1.5")
|
|
70
|
+
const match = raw.match(/(\d+[\d.]+\d+)/);
|
|
71
|
+
version = match ? match[1] : raw.split('\n')[0];
|
|
72
|
+
} catch {}
|
|
73
|
+
|
|
74
|
+
return { installed: true, binary, version };
|
|
75
|
+
}
|
|
76
|
+
|
|
48
77
|
/**
|
|
49
78
|
* Install an agent runtime.
|
|
50
79
|
* @returns {Promise<{success: boolean, output: string}>}
|
|
@@ -125,32 +154,12 @@ class Installer {
|
|
|
125
154
|
}
|
|
126
155
|
|
|
127
156
|
/**
|
|
128
|
-
* Find a binary on PATH.
|
|
157
|
+
* Find a binary on PATH (delegates to paths.js for cross-platform detection).
|
|
129
158
|
*/
|
|
130
159
|
_whichBinary(agentType) {
|
|
131
160
|
const entry = this.registry.getEntry(agentType);
|
|
132
161
|
const binary = entry && entry.install ? entry.install.binary : agentType;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
const cmd = process.platform === 'win32' ? `where ${binary}` : `which ${binary}`;
|
|
137
|
-
const env = { ...process.env };
|
|
138
|
-
const extraPaths = ['/usr/local/bin', '/opt/homebrew/bin'];
|
|
139
|
-
if (process.platform === 'win32') {
|
|
140
|
-
const npmBin = path.join(process.env.APPDATA || '', 'npm');
|
|
141
|
-
if (npmBin) extraPaths.push(npmBin);
|
|
142
|
-
extraPaths.push('C:\\Program Files\\nodejs');
|
|
143
|
-
}
|
|
144
|
-
for (const p of extraPaths) {
|
|
145
|
-
if (p && !(env.PATH || '').includes(p)) {
|
|
146
|
-
env.PATH = p + (process.platform === 'win32' ? ';' : ':') + (env.PATH || '');
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
const result = execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], env }).trim();
|
|
150
|
-
return result.split('\n')[0] || null;
|
|
151
|
-
} catch {
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
162
|
+
return whichBinary(binary);
|
|
154
163
|
}
|
|
155
164
|
|
|
156
165
|
// -- Markers --
|
|
@@ -218,31 +227,18 @@ class Installer {
|
|
|
218
227
|
|
|
219
228
|
_execShell(cmd, timeoutMs = 300000) {
|
|
220
229
|
return new Promise((resolve, reject) => {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (process.platform === 'win32') {
|
|
228
|
-
const npmBin = path.join(process.env.APPDATA || '', 'npm');
|
|
229
|
-
if (npmBin) extraPaths.push(npmBin);
|
|
230
|
-
// Common Node.js install locations on Windows
|
|
231
|
-
extraPaths.push('C:\\Program Files\\nodejs');
|
|
232
|
-
extraPaths.push('C:\\Program Files (x86)\\nodejs');
|
|
233
|
-
// Also try to find node via where
|
|
234
|
-
try {
|
|
235
|
-
const nodePath = execSync('where node', { encoding: 'utf-8', timeout: 3000 }).split(/\r?\n/)[0].trim();
|
|
236
|
-
if (nodePath) extraPaths.push(path.dirname(nodePath));
|
|
237
|
-
} catch {}
|
|
238
|
-
}
|
|
239
|
-
for (const p of extraPaths) {
|
|
240
|
-
if (p && !(env.PATH || '').includes(p)) {
|
|
241
|
-
env.PATH = p + (process.platform === 'win32' ? ';' : ':') + (env.PATH || '');
|
|
242
|
-
}
|
|
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 || '');
|
|
243
236
|
}
|
|
244
237
|
|
|
245
|
-
|
|
238
|
+
// On Windows, force UTF-8 codepage to avoid GBK garbled output
|
|
239
|
+
const shellCmd = process.platform === 'win32' ? `chcp 65001 >nul && ${cmd}` : cmd;
|
|
240
|
+
|
|
241
|
+
exec(shellCmd, {
|
|
246
242
|
encoding: 'utf-8',
|
|
247
243
|
timeout: timeoutMs,
|
|
248
244
|
shell: true,
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform PATH detection.
|
|
3
|
+
*
|
|
4
|
+
* Finds binary directories for Node.js version managers (nvm, fnm, volta),
|
|
5
|
+
* package managers (npm, Homebrew, pip), and standard system locations.
|
|
6
|
+
* Used by installer.js (binary detection) and daemon.js (agent spawning).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const { execSync } = require('child_process');
|
|
14
|
+
|
|
15
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
16
|
+
const IS_MACOS = process.platform === 'darwin';
|
|
17
|
+
const SEP = IS_WINDOWS ? ';' : ':';
|
|
18
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || '';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get all extra binary directories that should be checked beyond process.env.PATH.
|
|
22
|
+
* Returns deduplicated list of existing directories.
|
|
23
|
+
*/
|
|
24
|
+
function getExtraBinDirs() {
|
|
25
|
+
const dirs = [];
|
|
26
|
+
|
|
27
|
+
if (IS_WINDOWS) {
|
|
28
|
+
_addWindowsPaths(dirs);
|
|
29
|
+
} else {
|
|
30
|
+
_addUnixPaths(dirs);
|
|
31
|
+
if (IS_MACOS) {
|
|
32
|
+
_addMacPaths(dirs);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Common: ~/.local/bin (pipx, user installs)
|
|
37
|
+
_push(dirs, path.join(HOME, '.local', 'bin'));
|
|
38
|
+
|
|
39
|
+
// Filter to existing directories only, deduplicate
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
const currentPATH = process.env.PATH || '';
|
|
42
|
+
return dirs.filter(d => {
|
|
43
|
+
if (!d || seen.has(d) || currentPATH.includes(d)) return false;
|
|
44
|
+
seen.add(d);
|
|
45
|
+
try {
|
|
46
|
+
return fs.statSync(d).isDirectory();
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build a full PATH string that includes all extra bin dirs prepended.
|
|
55
|
+
*/
|
|
56
|
+
function getEnhancedPATH() {
|
|
57
|
+
const extra = getExtraBinDirs();
|
|
58
|
+
const current = process.env.PATH || '';
|
|
59
|
+
if (extra.length === 0) return current;
|
|
60
|
+
return extra.join(SEP) + SEP + current;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build an env object with enhanced PATH for spawning subprocesses.
|
|
65
|
+
*/
|
|
66
|
+
function getEnhancedEnv(baseEnv) {
|
|
67
|
+
const env = { ...(baseEnv || process.env) };
|
|
68
|
+
const extra = getExtraBinDirs();
|
|
69
|
+
if (extra.length > 0) {
|
|
70
|
+
env.PATH = extra.join(SEP) + SEP + (env.PATH || '');
|
|
71
|
+
}
|
|
72
|
+
if (IS_WINDOWS) {
|
|
73
|
+
// Force UTF-8 output from child processes on non-English Windows locales
|
|
74
|
+
// (prevents GBK/Shift-JIS garbled text in error messages)
|
|
75
|
+
env.PYTHONIOENCODING = env.PYTHONIOENCODING || 'utf-8';
|
|
76
|
+
env.PYTHONUTF8 = env.PYTHONUTF8 || '1';
|
|
77
|
+
env.LANG = env.LANG || 'en_US.UTF-8';
|
|
78
|
+
}
|
|
79
|
+
return env;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Find a binary by name. Returns full path or null.
|
|
84
|
+
*/
|
|
85
|
+
function whichBinary(name) {
|
|
86
|
+
if (!name) return null;
|
|
87
|
+
const cmd = IS_WINDOWS ? `where ${name}` : `which ${name}`;
|
|
88
|
+
try {
|
|
89
|
+
const result = execSync(cmd, {
|
|
90
|
+
encoding: 'utf-8',
|
|
91
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
92
|
+
env: { ...process.env, PATH: getEnhancedPATH() },
|
|
93
|
+
timeout: 5000,
|
|
94
|
+
}).trim();
|
|
95
|
+
return result.split(/\r?\n/)[0] || null;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---- Windows paths ----
|
|
102
|
+
|
|
103
|
+
function _addWindowsPaths(dirs) {
|
|
104
|
+
const appData = process.env.APPDATA || '';
|
|
105
|
+
const localAppData = process.env.LOCALAPPDATA || '';
|
|
106
|
+
const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
|
|
107
|
+
|
|
108
|
+
// npm global bin
|
|
109
|
+
if (appData) _push(dirs, path.join(appData, 'npm'));
|
|
110
|
+
|
|
111
|
+
// Node.js install
|
|
112
|
+
_push(dirs, path.join(programFiles, 'nodejs'));
|
|
113
|
+
|
|
114
|
+
// nvm for Windows
|
|
115
|
+
const nvmHome = process.env.NVM_HOME;
|
|
116
|
+
if (nvmHome) {
|
|
117
|
+
_push(dirs, nvmHome);
|
|
118
|
+
// nvm symlink dir
|
|
119
|
+
const nvmSymlink = process.env.NVM_SYMLINK || path.join(programFiles, 'nodejs');
|
|
120
|
+
_push(dirs, nvmSymlink);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// fnm
|
|
124
|
+
if (localAppData) _push(dirs, path.join(localAppData, 'fnm_multishells'));
|
|
125
|
+
const fnmDir = process.env.FNM_DIR || path.join(appData, 'fnm');
|
|
126
|
+
if (fnmDir) {
|
|
127
|
+
// fnm aliases — current version
|
|
128
|
+
try {
|
|
129
|
+
const defaultDir = path.join(fnmDir, 'aliases', 'default');
|
|
130
|
+
if (fs.existsSync(defaultDir)) _push(dirs, defaultDir);
|
|
131
|
+
} catch {}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// volta
|
|
135
|
+
const voltaHome = process.env.VOLTA_HOME || path.join(localAppData, 'Volta');
|
|
136
|
+
_push(dirs, path.join(voltaHome, 'bin'));
|
|
137
|
+
|
|
138
|
+
// Git (needed for some installers)
|
|
139
|
+
_push(dirs, path.join(programFiles, 'Git', 'cmd'));
|
|
140
|
+
_push(dirs, path.join(programFiles, 'Git', 'bin'));
|
|
141
|
+
|
|
142
|
+
// Python/pip
|
|
143
|
+
if (localAppData) {
|
|
144
|
+
_push(dirs, path.join(localAppData, 'Programs', 'Python', 'Python312', 'Scripts'));
|
|
145
|
+
_push(dirs, path.join(localAppData, 'Programs', 'Python', 'Python311', 'Scripts'));
|
|
146
|
+
_push(dirs, path.join(localAppData, 'Programs', 'Python', 'Python310', 'Scripts'));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---- Unix paths ----
|
|
151
|
+
|
|
152
|
+
function _addUnixPaths(dirs) {
|
|
153
|
+
// Standard
|
|
154
|
+
_push(dirs, '/usr/local/bin');
|
|
155
|
+
_push(dirs, '/usr/bin');
|
|
156
|
+
|
|
157
|
+
// npm global (varies by install method)
|
|
158
|
+
_push(dirs, path.join(HOME, '.npm-global', 'bin'));
|
|
159
|
+
|
|
160
|
+
// nvm
|
|
161
|
+
const nvmDir = process.env.NVM_DIR || path.join(HOME, '.nvm');
|
|
162
|
+
try {
|
|
163
|
+
// Find current nvm version
|
|
164
|
+
const defaultPath = path.join(nvmDir, 'alias', 'default');
|
|
165
|
+
if (fs.existsSync(defaultPath)) {
|
|
166
|
+
const version = fs.readFileSync(defaultPath, 'utf-8').trim();
|
|
167
|
+
// Resolve alias like 'lts/*' or version number
|
|
168
|
+
const resolved = _resolveNvmVersion(nvmDir, version);
|
|
169
|
+
if (resolved) _push(dirs, path.join(nvmDir, 'versions', 'node', resolved, 'bin'));
|
|
170
|
+
}
|
|
171
|
+
// Also try current symlink
|
|
172
|
+
_push(dirs, path.join(nvmDir, 'current', 'bin'));
|
|
173
|
+
} catch {}
|
|
174
|
+
|
|
175
|
+
// fnm
|
|
176
|
+
const fnmDir = process.env.FNM_DIR || path.join(HOME, '.fnm');
|
|
177
|
+
try {
|
|
178
|
+
const defaultDir = path.join(fnmDir, 'aliases', 'default');
|
|
179
|
+
if (fs.existsSync(defaultDir)) {
|
|
180
|
+
const target = fs.realpathSync(defaultDir);
|
|
181
|
+
_push(dirs, path.join(target, 'bin'));
|
|
182
|
+
}
|
|
183
|
+
} catch {}
|
|
184
|
+
|
|
185
|
+
// volta
|
|
186
|
+
const voltaHome = process.env.VOLTA_HOME || path.join(HOME, '.volta');
|
|
187
|
+
_push(dirs, path.join(voltaHome, 'bin'));
|
|
188
|
+
|
|
189
|
+
// pip/pipx user installs
|
|
190
|
+
_push(dirs, path.join(HOME, '.local', 'bin'));
|
|
191
|
+
|
|
192
|
+
// cargo
|
|
193
|
+
_push(dirs, path.join(HOME, '.cargo', 'bin'));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---- macOS-specific ----
|
|
197
|
+
|
|
198
|
+
function _addMacPaths(dirs) {
|
|
199
|
+
// Homebrew (Apple Silicon + Intel)
|
|
200
|
+
_push(dirs, '/opt/homebrew/bin');
|
|
201
|
+
_push(dirs, '/opt/homebrew/sbin');
|
|
202
|
+
_push(dirs, '/usr/local/bin');
|
|
203
|
+
_push(dirs, '/usr/local/sbin');
|
|
204
|
+
|
|
205
|
+
// MacPorts
|
|
206
|
+
_push(dirs, '/opt/local/bin');
|
|
207
|
+
|
|
208
|
+
// pkgx
|
|
209
|
+
_push(dirs, path.join(HOME, '.pkgx', 'bin'));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---- Helpers ----
|
|
213
|
+
|
|
214
|
+
function _push(arr, dir) {
|
|
215
|
+
if (dir) arr.push(dir);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function _resolveNvmVersion(nvmDir, alias) {
|
|
219
|
+
// Handle direct version like 'v22.14.0'
|
|
220
|
+
if (alias.startsWith('v')) {
|
|
221
|
+
return alias;
|
|
222
|
+
}
|
|
223
|
+
// Handle aliases like 'lts/*', 'lts/jod', 'default', numeric '22'
|
|
224
|
+
try {
|
|
225
|
+
// Try reading alias file
|
|
226
|
+
const aliasFile = path.join(nvmDir, 'alias', alias.replace('/', path.sep));
|
|
227
|
+
if (fs.existsSync(aliasFile)) {
|
|
228
|
+
const target = fs.readFileSync(aliasFile, 'utf-8').trim();
|
|
229
|
+
return _resolveNvmVersion(nvmDir, target);
|
|
230
|
+
}
|
|
231
|
+
} catch {}
|
|
232
|
+
|
|
233
|
+
// Try finding latest matching version in versions dir
|
|
234
|
+
try {
|
|
235
|
+
const versionsDir = path.join(nvmDir, 'versions', 'node');
|
|
236
|
+
if (fs.existsSync(versionsDir)) {
|
|
237
|
+
const versions = fs.readdirSync(versionsDir)
|
|
238
|
+
.filter(v => v.startsWith('v'))
|
|
239
|
+
.sort()
|
|
240
|
+
.reverse();
|
|
241
|
+
const match = versions.find(v => v.startsWith('v' + alias));
|
|
242
|
+
if (match) return match;
|
|
243
|
+
// Just return the latest
|
|
244
|
+
if (versions.length > 0) return versions[0];
|
|
245
|
+
}
|
|
246
|
+
} catch {}
|
|
247
|
+
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = {
|
|
252
|
+
getExtraBinDirs,
|
|
253
|
+
getEnhancedPATH,
|
|
254
|
+
getEnhancedEnv,
|
|
255
|
+
whichBinary,
|
|
256
|
+
IS_WINDOWS,
|
|
257
|
+
IS_MACOS,
|
|
258
|
+
SEP,
|
|
259
|
+
};
|