@openagents-org/agent-launcher 0.1.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/README.md +86 -0
- package/bin/agent-connector.js +4 -0
- package/package.json +42 -0
- package/registry.json +457 -0
- package/src/adapters/base.js +327 -0
- package/src/adapters/claude.js +420 -0
- package/src/adapters/codex.js +260 -0
- package/src/adapters/index.js +39 -0
- package/src/adapters/openclaw.js +264 -0
- package/src/adapters/utils.js +83 -0
- package/src/adapters/workspace-prompt.js +293 -0
- package/src/autostart.js +178 -0
- package/src/cli.js +556 -0
- package/src/config.js +322 -0
- package/src/daemon.js +666 -0
- package/src/env.js +111 -0
- package/src/index.js +205 -0
- package/src/installer.js +588 -0
- package/src/paths.js +276 -0
- package/src/registry.js +197 -0
- package/src/tui.js +540 -0
- package/src/utils.js +93 -0
- package/src/workspace-client.js +338 -0
package/src/paths.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
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
|
+
// 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
|
+
|
|
45
|
+
// Filter to existing directories only, deduplicate
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
const currentPATH = process.env.PATH || '';
|
|
48
|
+
return dirs.filter(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;
|
|
52
|
+
seen.add(d);
|
|
53
|
+
try {
|
|
54
|
+
return fs.statSync(d).isDirectory();
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build a full PATH string that includes all extra bin dirs prepended.
|
|
63
|
+
*/
|
|
64
|
+
function getEnhancedPATH() {
|
|
65
|
+
const extra = getExtraBinDirs();
|
|
66
|
+
const current = process.env.PATH || '';
|
|
67
|
+
if (extra.length === 0) return current;
|
|
68
|
+
return extra.join(SEP) + SEP + current;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build an env object with enhanced PATH for spawning subprocesses.
|
|
73
|
+
*/
|
|
74
|
+
function getEnhancedEnv(baseEnv) {
|
|
75
|
+
const env = { ...(baseEnv || process.env) };
|
|
76
|
+
const extra = getExtraBinDirs();
|
|
77
|
+
if (extra.length > 0) {
|
|
78
|
+
env.PATH = extra.join(SEP) + SEP + (env.PATH || '');
|
|
79
|
+
}
|
|
80
|
+
if (IS_WINDOWS) {
|
|
81
|
+
// Force UTF-8 output from child processes on non-English Windows locales
|
|
82
|
+
env.PYTHONIOENCODING = env.PYTHONIOENCODING || 'utf-8';
|
|
83
|
+
env.PYTHONUTF8 = env.PYTHONUTF8 || '1';
|
|
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
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return env;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Find a binary by name. Returns full path or null.
|
|
96
|
+
*/
|
|
97
|
+
function whichBinary(name) {
|
|
98
|
+
if (!name) return null;
|
|
99
|
+
const cmd = IS_WINDOWS ? `where ${name}` : `which ${name}`;
|
|
100
|
+
try {
|
|
101
|
+
const result = execSync(cmd, {
|
|
102
|
+
encoding: 'utf-8',
|
|
103
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
104
|
+
env: { ...process.env, PATH: getEnhancedPATH() },
|
|
105
|
+
timeout: 5000,
|
|
106
|
+
}).trim();
|
|
107
|
+
return result.split(/\r?\n/)[0] || null;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---- Windows paths ----
|
|
114
|
+
|
|
115
|
+
function _addWindowsPaths(dirs) {
|
|
116
|
+
const appData = process.env.APPDATA || '';
|
|
117
|
+
const localAppData = process.env.LOCALAPPDATA || '';
|
|
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'));
|
|
123
|
+
|
|
124
|
+
// npm global bin
|
|
125
|
+
if (appData) _push(dirs, path.join(appData, 'npm'));
|
|
126
|
+
|
|
127
|
+
// Node.js install
|
|
128
|
+
_push(dirs, path.join(programFiles, 'nodejs'));
|
|
129
|
+
|
|
130
|
+
// nvm for Windows
|
|
131
|
+
const nvmHome = process.env.NVM_HOME;
|
|
132
|
+
if (nvmHome) {
|
|
133
|
+
_push(dirs, nvmHome);
|
|
134
|
+
// nvm symlink dir
|
|
135
|
+
const nvmSymlink = process.env.NVM_SYMLINK || path.join(programFiles, 'nodejs');
|
|
136
|
+
_push(dirs, nvmSymlink);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// fnm
|
|
140
|
+
if (localAppData) _push(dirs, path.join(localAppData, 'fnm_multishells'));
|
|
141
|
+
const fnmDir = process.env.FNM_DIR || path.join(appData, 'fnm');
|
|
142
|
+
if (fnmDir) {
|
|
143
|
+
// fnm aliases — current version
|
|
144
|
+
try {
|
|
145
|
+
const defaultDir = path.join(fnmDir, 'aliases', 'default');
|
|
146
|
+
if (fs.existsSync(defaultDir)) _push(dirs, defaultDir);
|
|
147
|
+
} catch {}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// volta
|
|
151
|
+
const voltaHome = process.env.VOLTA_HOME || path.join(localAppData, 'Volta');
|
|
152
|
+
_push(dirs, path.join(voltaHome, 'bin'));
|
|
153
|
+
|
|
154
|
+
// Git (needed for some installers)
|
|
155
|
+
_push(dirs, path.join(programFiles, 'Git', 'cmd'));
|
|
156
|
+
_push(dirs, path.join(programFiles, 'Git', 'bin'));
|
|
157
|
+
|
|
158
|
+
// Python/pip
|
|
159
|
+
if (localAppData) {
|
|
160
|
+
_push(dirs, path.join(localAppData, 'Programs', 'Python', 'Python312', 'Scripts'));
|
|
161
|
+
_push(dirs, path.join(localAppData, 'Programs', 'Python', 'Python311', 'Scripts'));
|
|
162
|
+
_push(dirs, path.join(localAppData, 'Programs', 'Python', 'Python310', 'Scripts'));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---- Unix paths ----
|
|
167
|
+
|
|
168
|
+
function _addUnixPaths(dirs) {
|
|
169
|
+
// Standard
|
|
170
|
+
_push(dirs, '/usr/local/bin');
|
|
171
|
+
_push(dirs, '/usr/bin');
|
|
172
|
+
|
|
173
|
+
// npm global (varies by install method)
|
|
174
|
+
_push(dirs, path.join(HOME, '.npm-global', 'bin'));
|
|
175
|
+
_push(dirs, path.join(HOME, '.openagents', 'npm-global', 'bin'));
|
|
176
|
+
|
|
177
|
+
// nvm
|
|
178
|
+
const nvmDir = process.env.NVM_DIR || path.join(HOME, '.nvm');
|
|
179
|
+
try {
|
|
180
|
+
// Find current nvm version
|
|
181
|
+
const defaultPath = path.join(nvmDir, 'alias', 'default');
|
|
182
|
+
if (fs.existsSync(defaultPath)) {
|
|
183
|
+
const version = fs.readFileSync(defaultPath, 'utf-8').trim();
|
|
184
|
+
// Resolve alias like 'lts/*' or version number
|
|
185
|
+
const resolved = _resolveNvmVersion(nvmDir, version);
|
|
186
|
+
if (resolved) _push(dirs, path.join(nvmDir, 'versions', 'node', resolved, 'bin'));
|
|
187
|
+
}
|
|
188
|
+
// Also try current symlink
|
|
189
|
+
_push(dirs, path.join(nvmDir, 'current', 'bin'));
|
|
190
|
+
} catch {}
|
|
191
|
+
|
|
192
|
+
// fnm
|
|
193
|
+
const fnmDir = process.env.FNM_DIR || path.join(HOME, '.fnm');
|
|
194
|
+
try {
|
|
195
|
+
const defaultDir = path.join(fnmDir, 'aliases', 'default');
|
|
196
|
+
if (fs.existsSync(defaultDir)) {
|
|
197
|
+
const target = fs.realpathSync(defaultDir);
|
|
198
|
+
_push(dirs, path.join(target, 'bin'));
|
|
199
|
+
}
|
|
200
|
+
} catch {}
|
|
201
|
+
|
|
202
|
+
// volta
|
|
203
|
+
const voltaHome = process.env.VOLTA_HOME || path.join(HOME, '.volta');
|
|
204
|
+
_push(dirs, path.join(voltaHome, 'bin'));
|
|
205
|
+
|
|
206
|
+
// pip/pipx user installs
|
|
207
|
+
_push(dirs, path.join(HOME, '.local', 'bin'));
|
|
208
|
+
|
|
209
|
+
// cargo
|
|
210
|
+
_push(dirs, path.join(HOME, '.cargo', 'bin'));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---- macOS-specific ----
|
|
214
|
+
|
|
215
|
+
function _addMacPaths(dirs) {
|
|
216
|
+
// Homebrew (Apple Silicon + Intel)
|
|
217
|
+
_push(dirs, '/opt/homebrew/bin');
|
|
218
|
+
_push(dirs, '/opt/homebrew/sbin');
|
|
219
|
+
_push(dirs, '/usr/local/bin');
|
|
220
|
+
_push(dirs, '/usr/local/sbin');
|
|
221
|
+
|
|
222
|
+
// MacPorts
|
|
223
|
+
_push(dirs, '/opt/local/bin');
|
|
224
|
+
|
|
225
|
+
// pkgx
|
|
226
|
+
_push(dirs, path.join(HOME, '.pkgx', 'bin'));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---- Helpers ----
|
|
230
|
+
|
|
231
|
+
function _push(arr, dir) {
|
|
232
|
+
if (dir) arr.push(dir);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function _resolveNvmVersion(nvmDir, alias) {
|
|
236
|
+
// Handle direct version like 'v22.14.0'
|
|
237
|
+
if (alias.startsWith('v')) {
|
|
238
|
+
return alias;
|
|
239
|
+
}
|
|
240
|
+
// Handle aliases like 'lts/*', 'lts/jod', 'default', numeric '22'
|
|
241
|
+
try {
|
|
242
|
+
// Try reading alias file
|
|
243
|
+
const aliasFile = path.join(nvmDir, 'alias', alias.replace('/', path.sep));
|
|
244
|
+
if (fs.existsSync(aliasFile)) {
|
|
245
|
+
const target = fs.readFileSync(aliasFile, 'utf-8').trim();
|
|
246
|
+
return _resolveNvmVersion(nvmDir, target);
|
|
247
|
+
}
|
|
248
|
+
} catch {}
|
|
249
|
+
|
|
250
|
+
// Try finding latest matching version in versions dir
|
|
251
|
+
try {
|
|
252
|
+
const versionsDir = path.join(nvmDir, 'versions', 'node');
|
|
253
|
+
if (fs.existsSync(versionsDir)) {
|
|
254
|
+
const versions = fs.readdirSync(versionsDir)
|
|
255
|
+
.filter(v => v.startsWith('v'))
|
|
256
|
+
.sort()
|
|
257
|
+
.reverse();
|
|
258
|
+
const match = versions.find(v => v.startsWith('v' + alias));
|
|
259
|
+
if (match) return match;
|
|
260
|
+
// Just return the latest
|
|
261
|
+
if (versions.length > 0) return versions[0];
|
|
262
|
+
}
|
|
263
|
+
} catch {}
|
|
264
|
+
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = {
|
|
269
|
+
getExtraBinDirs,
|
|
270
|
+
getEnhancedPATH,
|
|
271
|
+
getEnhancedEnv,
|
|
272
|
+
whichBinary,
|
|
273
|
+
IS_WINDOWS,
|
|
274
|
+
IS_MACOS,
|
|
275
|
+
SEP,
|
|
276
|
+
};
|
package/src/registry.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_REGISTRY_URL = 'https://endpoint.openagents.org/v1/agent-registry';
|
|
7
|
+
const CACHE_FILE = 'agent_catalog.json';
|
|
8
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Agent registry — fetches the catalog of available agent types.
|
|
12
|
+
*
|
|
13
|
+
* Priority: remote API → local cache (24h) → bundled registry.json
|
|
14
|
+
*/
|
|
15
|
+
class Registry {
|
|
16
|
+
constructor(configDir, registryUrl) {
|
|
17
|
+
this.configDir = configDir;
|
|
18
|
+
this.registryUrl = registryUrl || DEFAULT_REGISTRY_URL;
|
|
19
|
+
this.cacheFile = path.join(configDir, CACHE_FILE);
|
|
20
|
+
this._catalog = null; // in-memory cache
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the full agent catalog. Tries remote, then cache, then bundled.
|
|
25
|
+
* @returns {Promise<object[]>}
|
|
26
|
+
*/
|
|
27
|
+
async getCatalog() {
|
|
28
|
+
if (this._catalog) return this._catalog;
|
|
29
|
+
|
|
30
|
+
// Try cache first (avoids network on every call)
|
|
31
|
+
const cached = this._loadCache();
|
|
32
|
+
if (cached) {
|
|
33
|
+
this._catalog = cached;
|
|
34
|
+
// Refresh in background if stale (but still return cached)
|
|
35
|
+
this._refreshInBackground();
|
|
36
|
+
return cached;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// No cache — try remote
|
|
40
|
+
const remote = await this._fetchRemote();
|
|
41
|
+
if (remote) {
|
|
42
|
+
this._catalog = remote;
|
|
43
|
+
return remote;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Fallback to bundled
|
|
47
|
+
this._catalog = this._loadBundled();
|
|
48
|
+
return this._catalog;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get catalog synchronously (cache or bundled only, no network).
|
|
53
|
+
*/
|
|
54
|
+
getCatalogSync() {
|
|
55
|
+
if (this._catalog) return this._catalog;
|
|
56
|
+
const cached = this._loadCache();
|
|
57
|
+
if (cached) { this._catalog = cached; return cached; }
|
|
58
|
+
this._catalog = this._loadBundled();
|
|
59
|
+
return this._catalog;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get env field definitions for an agent type.
|
|
64
|
+
*/
|
|
65
|
+
getEnvFields(agentType) {
|
|
66
|
+
const catalog = this.getCatalogSync();
|
|
67
|
+
const entry = catalog.find((e) => e.name === agentType);
|
|
68
|
+
return entry ? (entry.env_config || []) : [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get resolve_env rules for an agent type.
|
|
73
|
+
*/
|
|
74
|
+
getResolveRules(agentType) {
|
|
75
|
+
const catalog = this.getCatalogSync();
|
|
76
|
+
const entry = catalog.find((e) => e.name === agentType);
|
|
77
|
+
if (!entry || !entry.resolve_env) return [];
|
|
78
|
+
return entry.resolve_env.rules || [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get a single catalog entry by name.
|
|
83
|
+
*/
|
|
84
|
+
getEntry(agentType) {
|
|
85
|
+
const catalog = this.getCatalogSync();
|
|
86
|
+
const entry = catalog.find((e) => e.name === agentType) || null;
|
|
87
|
+
// If the cached entry is missing install info, merge with bundled
|
|
88
|
+
if (entry && !entry.install) {
|
|
89
|
+
const bundled = this._loadBundled();
|
|
90
|
+
const bundledEntry = bundled.find((e) => e.name === agentType);
|
|
91
|
+
if (bundledEntry && bundledEntry.install) {
|
|
92
|
+
return { ...entry, install: bundledEntry.install };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return entry;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Force refresh from remote API.
|
|
100
|
+
*/
|
|
101
|
+
async refresh() {
|
|
102
|
+
const remote = await this._fetchRemote();
|
|
103
|
+
if (remote) this._catalog = remote;
|
|
104
|
+
return this._catalog || this.getCatalogSync();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// -- Internal --
|
|
108
|
+
|
|
109
|
+
_loadCache() {
|
|
110
|
+
try {
|
|
111
|
+
if (!fs.existsSync(this.cacheFile)) return null;
|
|
112
|
+
const stat = fs.statSync(this.cacheFile);
|
|
113
|
+
if (Date.now() - stat.mtimeMs > CACHE_TTL_MS) return null;
|
|
114
|
+
const data = JSON.parse(fs.readFileSync(this.cacheFile, 'utf-8'));
|
|
115
|
+
return Array.isArray(data) ? data : null;
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_saveCache(data) {
|
|
122
|
+
try {
|
|
123
|
+
fs.mkdirSync(this.configDir, { recursive: true });
|
|
124
|
+
fs.writeFileSync(this.cacheFile, JSON.stringify(data, null, 2), 'utf-8');
|
|
125
|
+
} catch {}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_loadBundled() {
|
|
129
|
+
try {
|
|
130
|
+
const bundledPath = path.join(__dirname, '..', 'registry.json');
|
|
131
|
+
if (fs.existsSync(bundledPath)) {
|
|
132
|
+
return JSON.parse(fs.readFileSync(bundledPath, 'utf-8'));
|
|
133
|
+
}
|
|
134
|
+
} catch {}
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async _fetchRemote() {
|
|
139
|
+
try {
|
|
140
|
+
const data = await httpGetJson(this.registryUrl, 5000);
|
|
141
|
+
// API returns { data: [...] } or directly [...]
|
|
142
|
+
const catalog = Array.isArray(data) ? data : (data.data || []);
|
|
143
|
+
if (catalog.length > 0) {
|
|
144
|
+
this._saveCache(catalog);
|
|
145
|
+
}
|
|
146
|
+
return catalog;
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_refreshInBackground() {
|
|
153
|
+
// Check if cache is older than TTL
|
|
154
|
+
try {
|
|
155
|
+
const stat = fs.statSync(this.cacheFile);
|
|
156
|
+
if (Date.now() - stat.mtimeMs < CACHE_TTL_MS) return;
|
|
157
|
+
} catch {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// Fire and forget
|
|
161
|
+
this._fetchRemote().catch(() => {});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Simple HTTP GET that returns parsed JSON. No external dependencies.
|
|
167
|
+
*/
|
|
168
|
+
function httpGetJson(url, timeoutMs = 5000) {
|
|
169
|
+
return new Promise((resolve, reject) => {
|
|
170
|
+
const parsedUrl = new URL(url);
|
|
171
|
+
const transport = parsedUrl.protocol === 'https:' ? require('https') : require('http');
|
|
172
|
+
|
|
173
|
+
const req = transport.get(url, { timeout: timeoutMs }, (res) => {
|
|
174
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
175
|
+
// Follow redirect
|
|
176
|
+
httpGetJson(res.headers.location, timeoutMs).then(resolve, reject);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (res.statusCode >= 400) {
|
|
180
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
181
|
+
res.resume();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
let data = '';
|
|
185
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
186
|
+
res.on('end', () => {
|
|
187
|
+
try { resolve(JSON.parse(data)); }
|
|
188
|
+
catch (e) { reject(e); }
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
req.on('error', reject);
|
|
193
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = { Registry };
|