From 990d243dbd5ae78838e09d67270d7dedd7561264 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Thu, 23 Apr 2026 11:10:30 +0300 Subject: [PATCH] fix(setup): bypass rate-limited GitHub API when installing onecli CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The upstream onecli.sh/cli/install script resolves the latest release via api.github.com/repos/onecli/onecli-cli/releases/latest — anonymous callers get throttled to 60 req/hour per IP, and once exhausted the installer dies with "curl: (56) 403 / Error: could not determine latest release". Shared IPs (corporate NAT, public Wi-Fi) hit this without ever running the installer themselves. Reproduced locally: rate_limit remaining=0 → upstream installer returns the exact user error. Fallback path when upstream fails: 1. Resolve version via `curl -fsSL -o /dev/null -w '%{url_effective}' \ https://github.com/onecli/onecli-cli/releases/latest`. That endpoint 302s to /tag/vX.Y.Z — parses the version without an API call. 2. If the redirect probe also fails, install a pinned fallback version (ONECLI_CLI_FALLBACK_VERSION, currently 1.3.0). 3. Download the archive from /releases/download/vX.Y.Z/… directly (the CDN path isn't API-throttled), extract, and install to /usr/local/bin or ~/.local/bin mirroring upstream's install-dir logic. Gateway install (onecli.sh/install, docker-compose based) is untouched — it doesn't hit the API. Co-Authored-By: Claude Opus 4.7 (1M context) --- setup/onecli.ts | 132 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 122 insertions(+), 10 deletions(-) diff --git a/setup/onecli.ts b/setup/onecli.ts index 6be722a..3ceb1e8 100644 --- a/setup/onecli.ts +++ b/setup/onecli.ts @@ -97,25 +97,137 @@ function writeEnvOnecliUrl(url: string): void { fs.writeFileSync(envFile, content); } +// Last-known-good CLI release. Used only if BOTH the upstream installer +// and the redirect-based version probe fail. Bump deliberately when a +// new CLI release ships. +const ONECLI_CLI_FALLBACK_VERSION = '1.3.0'; +const ONECLI_CLI_REPO = 'onecli/onecli-cli'; + function installOnecli(): { stdout: string; ok: boolean } { - // OneCLI's own install script handles gateway + CLI + PATH. - // We run the two canonical installers in sequence and capture stdout so - // we can extract the printed URL as a fallback to `onecli config get`. let stdout = ''; + + // Gateway install (docker-compose based, no rate-limit concerns). + const gw = runInstall('curl -fsSL onecli.sh/install | sh'); + stdout += gw.stdout; + if (!gw.ok) { + log.error('OneCLI gateway install failed', { stderr: gw.stderr }); + return { stdout: stdout + (gw.stderr ?? ''), ok: false }; + } + + // CLI install. The upstream script calls the GitHub releases API + // (api.github.com) to resolve the latest tag — which 403s anonymous + // callers after 60 requests/hour per IP. Try upstream first; on failure + // resolve the version ourselves (via HTTP redirect, which isn't + // API-throttled) and download the release archive directly. + const upstream = runInstall('curl -fsSL onecli.sh/cli/install | sh'); + stdout += upstream.stdout; + if (upstream.ok) return { stdout, ok: true }; + + log.warn('Upstream CLI installer failed — falling back to direct download', { + stderr: upstream.stderr, + }); + stdout += (upstream.stderr ?? '') + '\n'; + + const fallback = installOnecliCliDirect(); + stdout += fallback.stdout; + if (!fallback.ok) { + log.error('OneCLI CLI install failed (both upstream and direct fallback)'); + return { stdout, ok: false }; + } + return { stdout, ok: true }; +} + +function runInstall(cmd: string): { stdout: string; stderr?: string; ok: boolean } { try { - stdout += execSync('curl -fsSL onecli.sh/install | sh', { - encoding: 'utf-8', - stdio: ['ignore', 'pipe', 'pipe'], - }); - stdout += execSync('curl -fsSL onecli.sh/cli/install | sh', { + const stdout = execSync(cmd, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], }); return { stdout, ok: true }; } catch (err) { const e = err as { stdout?: string; stderr?: string }; - log.error('OneCLI install failed', { stderr: e.stderr }); - return { stdout: stdout + (e.stdout ?? '') + (e.stderr ?? ''), ok: false }; + return { stdout: e.stdout ?? '', stderr: e.stderr, ok: false }; + } +} + +/** + * Reinstate the OneCLI CLI install without hitting GitHub's rate-limited + * releases API. Resolves the version via the HTTP redirect from + * /releases/latest → /releases/tag/vX.Y.Z, then downloads the archive + * directly. Falls back to ONECLI_CLI_FALLBACK_VERSION if the redirect + * probe also fails. + */ +function installOnecliCliDirect(): { stdout: string; ok: boolean } { + const lines: string[] = []; + const append = (s: string): void => { + lines.push(s); + }; + + const osName = + process.platform === 'darwin' ? 'darwin' : process.platform === 'linux' ? 'linux' : null; + if (!osName) { + append(`Unsupported platform: ${process.platform}`); + return { stdout: lines.join('\n'), ok: false }; + } + const arch = + process.arch === 'x64' ? 'amd64' : process.arch === 'arm64' ? 'arm64' : null; + if (!arch) { + append(`Unsupported arch: ${process.arch}`); + return { stdout: lines.join('\n'), ok: false }; + } + + let version: string | null = null; + try { + const redirect = execSync( + `curl -fsSL -o /dev/null -w '%{url_effective}' https://github.com/${ONECLI_CLI_REPO}/releases/latest`, + { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }, + ).trim(); + const m = redirect.match(/\/tag\/v?([^/]+)$/); + if (m) version = m[1]; + } catch { + // redirect probe failed — we'll pin the fallback + } + if (!version) { + version = ONECLI_CLI_FALLBACK_VERSION; + append(`Version probe failed; installing pinned fallback ${version}.`); + } else { + append(`Resolved onecli CLI ${version} via release redirect.`); + } + + const archive = `onecli_${version}_${osName}_${arch}.tar.gz`; + const url = `https://github.com/${ONECLI_CLI_REPO}/releases/download/v${version}/${archive}`; + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'onecli-')); + const archivePath = path.join(tmpDir, archive); + + try { + append(`Downloading ${url}`); + execSync( + `curl -fsSL -o ${JSON.stringify(archivePath)} ${JSON.stringify(url)}`, + { stdio: ['ignore', 'pipe', 'pipe'] }, + ); + execSync(`tar -xzf ${JSON.stringify(archivePath)} -C ${JSON.stringify(tmpDir)}`, { + stdio: ['ignore', 'pipe', 'pipe'], + }); + + let installDir = '/usr/local/bin'; + try { + fs.accessSync(installDir, fs.constants.W_OK); + } catch { + installDir = LOCAL_BIN; + fs.mkdirSync(installDir, { recursive: true }); + } + const binSrc = path.join(tmpDir, 'onecli'); + const binDest = path.join(installDir, 'onecli'); + fs.copyFileSync(binSrc, binDest); + fs.chmodSync(binDest, 0o755); + append(`onecli ${version} installed to ${binDest}.`); + return { stdout: lines.join('\n'), ok: true }; + } catch (err) { + const e = err as { stdout?: string; stderr?: string; message?: string }; + append(`Direct install failed: ${e.stderr ?? e.message ?? String(err)}`); + return { stdout: lines.join('\n'), ok: false }; + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); } }