diff --git a/cli/src/commands/install.ts b/cli/src/commands/install.ts index 846e10225ab..1667fe3b3bb 100644 --- a/cli/src/commands/install.ts +++ b/cli/src/commands/install.ts @@ -1,4 +1,5 @@ import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; @@ -136,8 +137,28 @@ export function resolveGitInstallRequest(options: InstallOptions): { repo: strin return { repo, ref, pinned: /^[0-9a-f]{7,40}$/i.test(ref) }; } +async function runGitHubCurl( + args: string[], + runCommand: CommandRunner, + options?: Parameters[2], +): Promise<{ stdout: string; stderr: string }> { + // Anonymous GitHub requests are rate-limited per source IP (CI runners and + // corporate NAT exhaust the shared quota); honor an ambient token when present. + // The token travels via a curl --config file so it never appears in process args. + const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN; + if (!token) return runCommand("curl", args, options); + const configDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclipai-gh-")); + const configFile = path.join(configDir, "headers"); + try { + fs.writeFileSync(configFile, `header = "Authorization: Bearer ${token}"\n`, { mode: 0o600 }); + return await runCommand("curl", ["--config", configFile, ...args], options); + } finally { + fs.rmSync(configDir, { recursive: true, force: true }); + } +} + export async function resolveGitHubRef(repo: string, ref: string, runCommand: CommandRunner): Promise { - const result = await runCommand("curl", ["--fail", "--silent", "--show-error", "--location", "--header", "Accept: application/vnd.github+json", "--header", "User-Agent: paperclipai-install", `https://api.github.com/repos/${repo}/commits/${encodeURIComponent(ref)}`], { maxBuffer: 4 * 1024 * 1024 }); + const result = await runGitHubCurl(["--fail", "--silent", "--show-error", "--location", "--header", "Accept: application/vnd.github+json", "--header", "User-Agent: paperclipai-install", `https://api.github.com/repos/${repo}/commits/${encodeURIComponent(ref)}`], runCommand, { maxBuffer: 4 * 1024 * 1024 }); let sha: unknown; try { sha = (JSON.parse(result.stdout) as { sha?: unknown }).sha; } catch { throw new Error(`GitHub returned an invalid response while resolving ${repo}@${ref}.`); } if (typeof sha !== "string" || !/^[0-9a-f]{40}$/i.test(sha)) throw new Error(`GitHub did not return a full commit SHA for ${repo}@${ref}.`); @@ -251,7 +272,7 @@ export async function installGitPayload(repo: string, sha: string, runCommand: C const buildEnv = (extra: NodeJS.ProcessEnv = {}) => gitBuildEnv({ PATH: [pnpmShimDir, process.env.PATH].filter(Boolean).join(path.delimiter), ...extra }); try { - await runCommand("curl", ["--fail", "--silent", "--show-error", "--location", "--output", archivePath, `https://codeload.github.com/${repo}/tar.gz/${sha}`], { maxBuffer: 4 * 1024 * 1024 }); + await runGitHubCurl(["--fail", "--silent", "--show-error", "--location", "--output", archivePath, `https://codeload.github.com/${repo}/tar.gz/${sha}`], runCommand, { maxBuffer: 4 * 1024 * 1024 }); await runCommand("tar", ["-xzf", archivePath, "--strip-components=1", "-C", checkoutPath], { maxBuffer: 4 * 1024 * 1024 }); await runCommand("corepack", ["enable", "pnpm", "--install-directory", pnpmShimDir], { cwd: checkoutPath, env: buildEnv(), maxBuffer: 4 * 1024 * 1024 }); await runCommand("corepack", ["pnpm", "install", "--frozen-lockfile"], { cwd: checkoutPath, env: buildEnv(), maxBuffer: 32 * 1024 * 1024 }); diff --git a/scripts/e2e-install-lifecycle.sh b/scripts/e2e-install-lifecycle.sh index ea0cbf4df11..954fb04a947 100755 --- a/scripts/e2e-install-lifecycle.sh +++ b/scripts/e2e-install-lifecycle.sh @@ -177,10 +177,11 @@ else skip_ "8 service lifecycle" "no systemd user bus at /run/user/$(id -u)/bus" else note "8. service lifecycle ($(uname -s): systemd/launchd)" - if shim service install --json; then - pass "8a service install exits 0" + # Real quickstart path: onboard with defaults, then install + start the service. + if shim onboard --yes --install-service; then + pass "8a onboard --yes --install-service exits 0" else - fail_ "8a service install exits 0" + fail_ "8a onboard --yes --install-service exits 0" fi DEADLINE=$(( $(date +%s) + E2E_SERVICE_TIMEOUT_SECS )) ACTIVE=0 @@ -203,9 +204,12 @@ else fi note "9. installer script guardrails (from the bootstrap checkout)" -if bash "$BOOT/scripts/install.sh" --ref deadbeef 2>&1 | grep -qi "not supported"; then +# Capture first: under pipefail, install.sh's expected exit 1 would fail the pipeline. +GUARD_OUT="$(bash "$BOOT/scripts/install.sh" --ref deadbeef 2>&1 || true)" +if echo "$GUARD_OUT" | grep -qi "not supported"; then pass "9a install.sh rejects --ref with guidance to npx path" else + echo "$GUARD_OUT" | tail -3 fail_ "9a install.sh rejects --ref" fi