@openagents-org/agent-launcher 0.2.158 → 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/installer.js +86 -17
- package/src/mirrors.js +119 -0
- package/src/update-check.js +18 -2
package/package.json
CHANGED
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
|
*/
|