@openagents-org/agent-launcher 0.2.157 → 0.2.159
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/cli.js +23 -2
- package/src/daemon.js +37 -2
- package/src/installer.js +86 -17
- package/src/mirrors.js +119 -0
- package/src/update-check.js +18 -2
- package/src/workspace-client.js +1 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -311,7 +311,28 @@ async function cmdList(connector) {
|
|
|
311
311
|
table(rows, ['NAME', 'TYPE', 'ROLE', 'NETWORK']);
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
async function cmdRuntimes(connector) {
|
|
314
|
+
async function cmdRuntimes(connector, flags) {
|
|
315
|
+
// Machine-readable detection for every supported type: installed? logged-in?
|
|
316
|
+
// Used by the daemon to report node runtimes to the workspace (Add-agent
|
|
317
|
+
// gallery). Emits ONLY JSON on stdout.
|
|
318
|
+
if (flags && flags.json) {
|
|
319
|
+
const runtimes = connector.registry.getCatalogSync().map((e) => {
|
|
320
|
+
let h;
|
|
321
|
+
try { h = connector.healthCheck(e.name); } catch { h = {}; }
|
|
322
|
+
return {
|
|
323
|
+
type: e.name,
|
|
324
|
+
installed: !!h.installed,
|
|
325
|
+
ready: !!h.ready,
|
|
326
|
+
version: h.version || null,
|
|
327
|
+
reason: h.reason || null,
|
|
328
|
+
message: h.message || null,
|
|
329
|
+
authStatus: h.auth_status || null,
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
process.stdout.write(JSON.stringify(runtimes));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
315
336
|
let catalog;
|
|
316
337
|
try {
|
|
317
338
|
catalog = await connector.getCatalog();
|
|
@@ -922,7 +943,7 @@ async function main() {
|
|
|
922
943
|
install: () => cmdInstall(connector, flags, positional),
|
|
923
944
|
uninstall: () => cmdUninstall(connector, flags, positional),
|
|
924
945
|
search: () => cmdSearch(connector, flags, positional),
|
|
925
|
-
runtimes: () => cmdRuntimes(connector),
|
|
946
|
+
runtimes: () => cmdRuntimes(connector, flags),
|
|
926
947
|
connect: () => cmdConnect(connector, flags, positional),
|
|
927
948
|
node: () => cmdNode(connector, flags, positional),
|
|
928
949
|
disconnect: () => cmdDisconnect(connector, flags, positional),
|
package/src/daemon.js
CHANGED
|
@@ -33,6 +33,8 @@ class Daemon {
|
|
|
33
33
|
this._nodeHeartbeatInterval = null; // device-level heartbeat (connect-a-node)
|
|
34
34
|
this._nodeClient = null; // lazily-created WorkspaceClient for the node
|
|
35
35
|
this._runningCommands = new Set(); // node-command ids currently executing
|
|
36
|
+
this._runtimes = []; // detected agent runtimes for the node view
|
|
37
|
+
this._runtimesInterval = null;
|
|
36
38
|
this._reloadInFlight = null; // serialize concurrent _reload() calls
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -49,7 +51,7 @@ class Daemon {
|
|
|
49
51
|
if (!this._nodeClient) {
|
|
50
52
|
this._nodeClient = new WorkspaceClient(n.endpoint);
|
|
51
53
|
}
|
|
52
|
-
const info = { ...nodeCfg.gatherDeviceInfo(), agents: this._buildRoster() };
|
|
54
|
+
const info = { ...nodeCfg.gatherDeviceInfo(), agents: this._buildRoster(), runtimes: this._runtimes };
|
|
53
55
|
const resp = await this._nodeClient.nodeHeartbeat(n.node_id, n.token, info);
|
|
54
56
|
// The heartbeat response is our push channel: run any queued remote
|
|
55
57
|
// agent-management commands the workspace enqueued for this node. Fire
|
|
@@ -86,6 +88,24 @@ class Daemon {
|
|
|
86
88
|
return roster;
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Detect, for every supported agent type, whether its runtime is installed and
|
|
93
|
+
* logged-in/ready on this device. Runs `agn runtimes --json` in a CHILD process
|
|
94
|
+
* so the (synchronous, execSync-heavy) version/login probes never block the
|
|
95
|
+
* daemon's event loop. Result is reported to the workspace on the heartbeat.
|
|
96
|
+
*/
|
|
97
|
+
async _refreshRuntimes() {
|
|
98
|
+
try {
|
|
99
|
+
const r = await this._runAgn(['runtimes', '--json']);
|
|
100
|
+
if (r.code === 0 && r.stdout) {
|
|
101
|
+
const parsed = JSON.parse(r.stdout.trim());
|
|
102
|
+
if (Array.isArray(parsed)) this._runtimes = parsed;
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// keep the previous snapshot on failure
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
89
109
|
/**
|
|
90
110
|
* Execute one remote command by shelling out to this same launcher's CLI, so
|
|
91
111
|
* all of create/install/connect/start/stop/remove reuses the exact code path a
|
|
@@ -100,7 +120,9 @@ class Daemon {
|
|
|
100
120
|
try {
|
|
101
121
|
if (action === 'create_agent') {
|
|
102
122
|
const type = (args.type || '').trim();
|
|
103
|
-
const
|
|
123
|
+
const createArgs = ['create', name, '--type', type, '--install'];
|
|
124
|
+
if (args.workingDir) createArgs.push('--path', String(args.workingDir));
|
|
125
|
+
const r1 = await this._runAgn(createArgs);
|
|
104
126
|
if (r1.code !== 0) throw new Error(r1.stderr || r1.stdout || 'create failed');
|
|
105
127
|
// Optional credentials for API-key agents (generic → provider mapping
|
|
106
128
|
// happens in env resolution).
|
|
@@ -124,6 +146,13 @@ class Daemon {
|
|
|
124
146
|
const r = await this._runAgn(['remove', name]);
|
|
125
147
|
ok = r.code === 0;
|
|
126
148
|
message = ok ? `Agent '${name}' removed` : (r.stderr || r.stdout || 'remove failed');
|
|
149
|
+
} else if (action === 'detect_runtimes') {
|
|
150
|
+
await this._refreshRuntimes();
|
|
151
|
+
// Push the fresh detection right away rather than waiting for the next
|
|
152
|
+
// heartbeat, so the workspace's Add-agent gallery updates promptly.
|
|
153
|
+
this._nodeHeartbeat();
|
|
154
|
+
ok = true;
|
|
155
|
+
message = `Detected ${this._runtimes.length} runtime(s)`;
|
|
127
156
|
} else {
|
|
128
157
|
message = `Unknown action '${action}'`;
|
|
129
158
|
}
|
|
@@ -226,6 +255,11 @@ class Daemon {
|
|
|
226
255
|
this._nodeHeartbeat();
|
|
227
256
|
this._nodeHeartbeatInterval = setInterval(() => this._nodeHeartbeat(), 10000);
|
|
228
257
|
|
|
258
|
+
// Detect installed/logged-in agent runtimes for the Add-agent gallery. Runs
|
|
259
|
+
// in a child process (off the event loop), refreshed periodically.
|
|
260
|
+
this._refreshRuntimes();
|
|
261
|
+
this._runtimesInterval = setInterval(() => this._refreshRuntimes(), 120000);
|
|
262
|
+
|
|
229
263
|
// Watch config file for hot-reload
|
|
230
264
|
this._watchConfig();
|
|
231
265
|
|
|
@@ -252,6 +286,7 @@ class Daemon {
|
|
|
252
286
|
if (this._statusInterval) clearInterval(this._statusInterval);
|
|
253
287
|
if (this._cmdInterval) clearInterval(this._cmdInterval);
|
|
254
288
|
if (this._nodeHeartbeatInterval) clearInterval(this._nodeHeartbeatInterval);
|
|
289
|
+
if (this._runtimesInterval) clearInterval(this._runtimesInterval);
|
|
255
290
|
if (this._configWatcher) { try { this._configWatcher.close(); } catch {} }
|
|
256
291
|
|
|
257
292
|
// Kill all child processes
|
package/src/installer.js
CHANGED
|
@@ -6,6 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const { execSync, exec } = require('child_process');
|
|
7
7
|
const { whichBinary, getEnhancedEnv, getRuntimePrefix, clearBinaryLookupCache, aiderBinDirs, resolveBinaryInKnownDirs } = require('./paths');
|
|
8
8
|
const { EnvManager } = require('./env');
|
|
9
|
+
const { nodeDistUrls, installRegistry } = require('./mirrors');
|
|
9
10
|
const { readinessReason, REASON } = require('./adapters/health-status');
|
|
10
11
|
|
|
11
12
|
const STATUS_CACHE_TTL_MS = 10000;
|
|
@@ -1013,8 +1014,14 @@ class Installer {
|
|
|
1013
1014
|
.split(/\s+/)
|
|
1014
1015
|
.filter(Boolean);
|
|
1015
1016
|
// Use --save so npm tracks the package in package.json (prevents pruning
|
|
1016
|
-
// on next install).
|
|
1017
|
-
|
|
1017
|
+
// on next install). --registry is added only when we'd be helping — see
|
|
1018
|
+
// installRegistry(); a user's own registry choice is never overridden.
|
|
1019
|
+
const registry = installRegistry();
|
|
1020
|
+
const npmArgs = [
|
|
1021
|
+
'install', '--loglevel=verbose', '--save', '--prefix', prefixDir,
|
|
1022
|
+
...(registry ? ['--registry', registry] : []),
|
|
1023
|
+
...pkgArgs,
|
|
1024
|
+
];
|
|
1018
1025
|
displayCmd = `npm ${npmArgs.join(' ')}`;
|
|
1019
1026
|
|
|
1020
1027
|
const direct = this._resolveNodeNpmCli();
|
|
@@ -1031,7 +1038,8 @@ class Installer {
|
|
|
1031
1038
|
}
|
|
1032
1039
|
} else {
|
|
1033
1040
|
// Legacy fallback: quoted prefix inside a shell string.
|
|
1034
|
-
const
|
|
1041
|
+
const registryArg = registry ? `--registry ${registry} ` : '';
|
|
1042
|
+
const args = `install --loglevel=verbose --save --prefix "${prefixDir}" ${registryArg}${pkgArgs.join(' ')}`;
|
|
1035
1043
|
spawnFile = this._wrapForWindowsShell(this._resolveNpmCommand(args));
|
|
1036
1044
|
useShell = isWin ? (env.ComSpec || 'C:\\Windows\\System32\\cmd.exe') : true;
|
|
1037
1045
|
displayCmd = spawnFile;
|
|
@@ -1623,11 +1631,13 @@ class Installer {
|
|
|
1623
1631
|
if (plat === 'windows') {
|
|
1624
1632
|
// Download portable zip — no admin required
|
|
1625
1633
|
const arch = os.arch() === 'x64' ? 'x64' : 'x86';
|
|
1626
|
-
const url = `https://nodejs.org/dist/${nodeVersion}/node-${nodeVersion}-win-${arch}.zip`;
|
|
1627
1634
|
const zipPath = path.join(os.tmpdir(), `node-${nodeVersion}.zip`);
|
|
1628
1635
|
|
|
1629
|
-
|
|
1630
|
-
|
|
1636
|
+
await this._downloadFirst(
|
|
1637
|
+
nodeDistUrls(`${nodeVersion}/node-${nodeVersion}-win-${arch}.zip`),
|
|
1638
|
+
zipPath,
|
|
1639
|
+
onData
|
|
1640
|
+
);
|
|
1631
1641
|
|
|
1632
1642
|
// Extract to ~/.openagents/nodejs/
|
|
1633
1643
|
const nodejsDir = path.join(this.configDir, 'nodejs');
|
|
@@ -1688,11 +1698,13 @@ class Installer {
|
|
|
1688
1698
|
const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
|
|
1689
1699
|
const ext = plat === 'macos' ? 'tar.gz' : 'tar.xz';
|
|
1690
1700
|
const platName = plat === 'macos' ? 'darwin' : 'linux';
|
|
1691
|
-
const url = `https://nodejs.org/dist/${nodeVersion}/node-${nodeVersion}-${platName}-${arch}.${ext}`;
|
|
1692
1701
|
const tarPath = path.join(os.tmpdir(), `node-${nodeVersion}.${ext}`);
|
|
1693
1702
|
|
|
1694
|
-
|
|
1695
|
-
|
|
1703
|
+
await this._downloadFirst(
|
|
1704
|
+
nodeDistUrls(`${nodeVersion}/node-${nodeVersion}-${platName}-${arch}.${ext}`),
|
|
1705
|
+
tarPath,
|
|
1706
|
+
onData
|
|
1707
|
+
);
|
|
1696
1708
|
|
|
1697
1709
|
const nodeDir = path.join(this.configDir, 'nodejs');
|
|
1698
1710
|
fs.mkdirSync(nodeDir, { recursive: true });
|
|
@@ -1725,23 +1737,61 @@ class Installer {
|
|
|
1725
1737
|
/**
|
|
1726
1738
|
* Download a file with progress reporting.
|
|
1727
1739
|
*/
|
|
1740
|
+
/**
|
|
1741
|
+
* Try each candidate URL in order until one delivers the file. Mirrors carry
|
|
1742
|
+
* identical bytes, so a slow or unreachable origin costs one timeout instead
|
|
1743
|
+
* of failing the whole install.
|
|
1744
|
+
*/
|
|
1745
|
+
async _downloadFirst(urls, destPath, onData) {
|
|
1746
|
+
let lastErr = null;
|
|
1747
|
+
for (const url of urls) {
|
|
1748
|
+
try {
|
|
1749
|
+
if (onData) onData(`Downloading ${url}...\n`);
|
|
1750
|
+
await this._downloadFile(url, destPath, onData);
|
|
1751
|
+
return url;
|
|
1752
|
+
} catch (err) {
|
|
1753
|
+
lastErr = err;
|
|
1754
|
+
if (onData) onData(` failed (${err.message}) — trying next source\n`);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
throw lastErr || new Error('Download failed: no sources available');
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
/**
|
|
1761
|
+
* Download a file with progress reporting.
|
|
1762
|
+
*
|
|
1763
|
+
* Writes to `${destPath}.part` and renames on success: a partial file left at
|
|
1764
|
+
* the final path reads as "already installed" on the next run and produces a
|
|
1765
|
+
* corrupt runtime. A stall timeout and a short-read check are what turn a
|
|
1766
|
+
* silently truncated transfer into a retryable error.
|
|
1767
|
+
*/
|
|
1728
1768
|
_downloadFile(url, destPath, onData) {
|
|
1729
1769
|
const https = require('https');
|
|
1730
1770
|
const http = require('http');
|
|
1771
|
+
const STALL_TIMEOUT_MS = 30000;
|
|
1772
|
+
const tmpPath = `${destPath}.part`;
|
|
1773
|
+
try { fs.unlinkSync(tmpPath); } catch {}
|
|
1774
|
+
|
|
1731
1775
|
return new Promise((resolve, reject) => {
|
|
1732
1776
|
const get = url.startsWith('https') ? https.get : http.get;
|
|
1733
|
-
|
|
1777
|
+
const fail = (err) => {
|
|
1778
|
+
try { fs.unlinkSync(tmpPath); } catch {}
|
|
1779
|
+
reject(err);
|
|
1780
|
+
};
|
|
1781
|
+
const req = get(url, (res) => {
|
|
1734
1782
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
1735
|
-
|
|
1736
|
-
|
|
1783
|
+
res.resume();
|
|
1784
|
+
const next = new URL(res.headers.location, url).toString();
|
|
1785
|
+
return this._downloadFile(next, destPath, onData).then(resolve, reject);
|
|
1737
1786
|
}
|
|
1738
1787
|
if (res.statusCode !== 200) {
|
|
1739
|
-
|
|
1788
|
+
res.resume();
|
|
1789
|
+
return fail(new Error(`Download failed: HTTP ${res.statusCode}`));
|
|
1740
1790
|
}
|
|
1741
1791
|
const totalBytes = parseInt(res.headers['content-length'] || '0', 10);
|
|
1742
1792
|
let downloaded = 0;
|
|
1743
1793
|
let lastPercent = -1;
|
|
1744
|
-
const file = fs.createWriteStream(
|
|
1794
|
+
const file = fs.createWriteStream(tmpPath);
|
|
1745
1795
|
res.on('data', (chunk) => {
|
|
1746
1796
|
downloaded += chunk.length;
|
|
1747
1797
|
if (totalBytes > 0) {
|
|
@@ -1752,10 +1802,29 @@ class Installer {
|
|
|
1752
1802
|
}
|
|
1753
1803
|
}
|
|
1754
1804
|
});
|
|
1805
|
+
res.on('error', fail);
|
|
1806
|
+
file.on('error', fail);
|
|
1755
1807
|
res.pipe(file);
|
|
1756
|
-
file.on('finish', () => {
|
|
1757
|
-
|
|
1758
|
-
|
|
1808
|
+
file.on('finish', () => {
|
|
1809
|
+
file.close(() => {
|
|
1810
|
+
if (totalBytes > 0 && downloaded !== totalBytes) {
|
|
1811
|
+
return fail(new Error(`Truncated download: ${downloaded} of ${totalBytes} bytes`));
|
|
1812
|
+
}
|
|
1813
|
+
try {
|
|
1814
|
+
fs.renameSync(tmpPath, destPath);
|
|
1815
|
+
} catch (err) {
|
|
1816
|
+
return fail(err);
|
|
1817
|
+
}
|
|
1818
|
+
resolve();
|
|
1819
|
+
});
|
|
1820
|
+
});
|
|
1821
|
+
});
|
|
1822
|
+
// Guard the connection itself: without this a black-holed origin keeps
|
|
1823
|
+
// the promise pending forever and the install just sits there.
|
|
1824
|
+
req.setTimeout(STALL_TIMEOUT_MS, () => {
|
|
1825
|
+
req.destroy(new Error(`Download timed out after ${STALL_TIMEOUT_MS / 1000}s`));
|
|
1826
|
+
});
|
|
1827
|
+
req.on('error', fail);
|
|
1759
1828
|
});
|
|
1760
1829
|
}
|
|
1761
1830
|
|
package/src/mirrors.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Region-aware download origins.
|
|
5
|
+
*
|
|
6
|
+
* The core downloads a portable Node.js runtime (nodejs.org) and talks to the
|
|
7
|
+
* npm registry. Both are slow to unusable from mainland China, which shows up
|
|
8
|
+
* as an install that appears to hang. Mirrors carry byte-identical copies under
|
|
9
|
+
* the same path layout, so the only decision is which origin to try first —
|
|
10
|
+
* every list keeps the official one as the final fallback.
|
|
11
|
+
*
|
|
12
|
+
* Where the region comes from, in order:
|
|
13
|
+
* 1. OPENAGENTS_NODE_MIRRORS / OPENAGENTS_NPM_MIRRORS — explicit, comma
|
|
14
|
+
* separated base URLs. The launcher sets these from its own Settings →
|
|
15
|
+
* Network preference so the core inherits the user's actual choice
|
|
16
|
+
* instead of guessing again in a child process.
|
|
17
|
+
* 2. OPENAGENTS_DOWNLOAD_REGION = cn | global — explicit override for CLI
|
|
18
|
+
* users and support.
|
|
19
|
+
* 3. Timezone / locale detection (best effort).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const OFFICIAL_NODE = 'https://nodejs.org/dist';
|
|
23
|
+
const CN_NODE = [
|
|
24
|
+
'https://cdn.npmmirror.com/binaries/node',
|
|
25
|
+
'https://mirrors.aliyun.com/nodejs-release',
|
|
26
|
+
'https://mirrors.huaweicloud.com/nodejs',
|
|
27
|
+
];
|
|
28
|
+
const OFFICIAL_NPM = 'https://registry.npmjs.org';
|
|
29
|
+
const CN_NPM = [
|
|
30
|
+
'https://registry.npmmirror.com',
|
|
31
|
+
'https://mirrors.cloud.tencent.com/npm',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function envBases(name) {
|
|
35
|
+
return (process.env[name] || '')
|
|
36
|
+
.split(',')
|
|
37
|
+
.map((s) => s.trim().replace(/\/+$/, ''))
|
|
38
|
+
.filter((s) => /^https?:\/\//.test(s));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function inChina() {
|
|
42
|
+
const region = (process.env.OPENAGENTS_DOWNLOAD_REGION || '').toLowerCase();
|
|
43
|
+
if (region === 'cn') return true;
|
|
44
|
+
if (region === 'global') return false;
|
|
45
|
+
try {
|
|
46
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
|
|
47
|
+
if (/Shanghai|Chongqing|Chungking|Urumqi|Harbin|Kashgar|PRC/i.test(tz)) return true;
|
|
48
|
+
} catch {}
|
|
49
|
+
const locale = (process.env.LC_ALL || process.env.LANG || '').toLowerCase();
|
|
50
|
+
if (locale.startsWith('zh_cn') || locale.startsWith('zh-cn')) return true;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function bases(envName, official, mirrors) {
|
|
55
|
+
const fromEnv = envBases(envName);
|
|
56
|
+
if (fromEnv.length) {
|
|
57
|
+
// Trust the caller's list, but never drop the official origin entirely —
|
|
58
|
+
// a mirror outage must still degrade to "slow", not "broken".
|
|
59
|
+
return fromEnv.includes(official) ? fromEnv : [...fromEnv, official];
|
|
60
|
+
}
|
|
61
|
+
return inChina() ? [...mirrors, official] : [official];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Candidate URLs for a Node dist path, e.g. "v22.22.3/node-v22.22.3-darwin-arm64.tar.gz". */
|
|
65
|
+
function nodeDistUrls(relPath) {
|
|
66
|
+
return bases('OPENAGENTS_NODE_MIRRORS', OFFICIAL_NODE, CN_NODE).map(
|
|
67
|
+
(base) => `${base}/${relPath}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Candidate URLs for an npm registry path, e.g. "@scope/pkg/latest". */
|
|
72
|
+
function npmUrls(relPath) {
|
|
73
|
+
return bases('OPENAGENTS_NPM_MIRRORS', OFFICIAL_NPM, CN_NPM).map(
|
|
74
|
+
(base) => `${base}/${relPath}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Registry base for `npm install --registry`. */
|
|
79
|
+
function npmRegistry() {
|
|
80
|
+
return process.env.npm_config_registry || npmUrls('').slice(0, 1)[0].replace(/\/$/, '');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A registry the user (or the launcher) has already chosen, if any. */
|
|
84
|
+
function userConfiguredRegistry() {
|
|
85
|
+
if (process.env.npm_config_registry) return process.env.npm_config_registry;
|
|
86
|
+
try {
|
|
87
|
+
const fs = require('fs');
|
|
88
|
+
const os = require('os');
|
|
89
|
+
const path = require('path');
|
|
90
|
+
const rc = fs.readFileSync(path.join(os.homedir(), '.npmrc'), 'utf-8');
|
|
91
|
+
const match = /^\s*registry\s*=\s*(\S+)/m.exec(rc);
|
|
92
|
+
if (match) return match[1];
|
|
93
|
+
} catch {}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Registry to force via `--registry` when installing an agent runtime, or null
|
|
99
|
+
* to leave npm's own resolution alone.
|
|
100
|
+
*
|
|
101
|
+
* Agent runtimes are the biggest download of a first run (a CLI plus its whole
|
|
102
|
+
* dependency tree), and they install through a child npm process where our own
|
|
103
|
+
* mirror logic can't reach — the flag is the only lever. It stays null unless
|
|
104
|
+
* we'd actually be helping: an explicit npm_config_registry or a registry in
|
|
105
|
+
* the user's .npmrc (corporate proxy, private mirror) is honoured as-is.
|
|
106
|
+
*/
|
|
107
|
+
function installRegistry() {
|
|
108
|
+
if (userConfiguredRegistry()) return null;
|
|
109
|
+
return inChina() ? CN_NPM[0] : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
nodeDistUrls,
|
|
114
|
+
npmUrls,
|
|
115
|
+
npmRegistry,
|
|
116
|
+
installRegistry,
|
|
117
|
+
userConfiguredRegistry,
|
|
118
|
+
inChina,
|
|
119
|
+
};
|
package/src/update-check.js
CHANGED
|
@@ -14,6 +14,7 @@ const os = require('os');
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const readline = require('readline');
|
|
16
16
|
const { execSync, spawnSync } = require('child_process');
|
|
17
|
+
const { npmUrls } = require('./mirrors');
|
|
17
18
|
|
|
18
19
|
const PKG_NAME = '@openagents-org/agent-launcher';
|
|
19
20
|
const CACHE_FILE = path.join(os.homedir(), '.openagents', '.update-check.json');
|
|
@@ -51,10 +52,10 @@ function saveCache(latest) {
|
|
|
51
52
|
} catch {}
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
function
|
|
55
|
+
function fetchLatestFrom(url, timeoutMs) {
|
|
55
56
|
return new Promise((resolve) => {
|
|
56
57
|
const req = https.request(
|
|
57
|
-
|
|
58
|
+
url,
|
|
58
59
|
{ method: 'GET', timeout: timeoutMs, headers: { Accept: 'application/json' } },
|
|
59
60
|
(res) => {
|
|
60
61
|
let body = '';
|
|
@@ -73,6 +74,21 @@ function fetchLatest(timeoutMs = 2500) {
|
|
|
73
74
|
});
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Ask each registry in turn (mirror first in China) with a per-origin budget,
|
|
79
|
+
* so the check either answers quickly or gives up quietly — it must never be
|
|
80
|
+
* what makes the CLI feel slow to start.
|
|
81
|
+
*/
|
|
82
|
+
async function fetchLatest(timeoutMs = 2500) {
|
|
83
|
+
const urls = npmUrls(`${PKG_NAME}/latest`);
|
|
84
|
+
const perOrigin = Math.max(1200, Math.floor(timeoutMs / urls.length));
|
|
85
|
+
for (const url of urls) {
|
|
86
|
+
const version = await fetchLatestFrom(url, perOrigin);
|
|
87
|
+
if (version) return version;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
76
92
|
/**
|
|
77
93
|
* Return { current, latest, isNewer } or null if the check failed / offline.
|
|
78
94
|
*/
|
package/src/workspace-client.js
CHANGED
|
@@ -134,6 +134,7 @@ class WorkspaceClient {
|
|
|
134
134
|
if (deviceInfo.os) body.os = deviceInfo.os;
|
|
135
135
|
if (deviceInfo.launcherVersion) body.launcher_version = deviceInfo.launcherVersion;
|
|
136
136
|
if (Array.isArray(deviceInfo.agents)) body.agents = deviceInfo.agents;
|
|
137
|
+
if (Array.isArray(deviceInfo.runtimes)) body.runtimes = deviceInfo.runtimes;
|
|
137
138
|
const data = await this._post('/v1/nodes/heartbeat', body, this._wsHeaders(token));
|
|
138
139
|
return data.data || data;
|
|
139
140
|
}
|