From 61bcedc0eb3ea6d1a4e5de8f4644e071faee0c64 Mon Sep 17 00:00:00 2001 From: L <6723574+louisgv@users.noreply.github.com> Date: Wed, 4 Mar 2026 02:34:58 -0500 Subject: [PATCH] feat: migrate to openrouter.ai/labs/spawn CDN + release artifact version checks (#2178) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: migrate shell script URLs to openrouter.ai/labs/spawn CDN Users on older CLI versions can't auto-update because the repo was restructured (cli/ → packages/cli/), so old version-check URLs 404. This decouples the CLI from the repo's internal directory structure: - Shell script URLs (install, agent scripts, github-auth) now use openrouter.ai/labs/spawn/* as primary with GitHub raw as fallback - Version checks now use GitHub release artifact (cli-latest/version) as primary — a static URL that never changes regardless of repo layout - CI workflow updated to publish a `version` file alongside cli.js - Remove GITHUB_RAW_URL_PATTERN validation (no longer needed since install URL is now a hardcoded CDN string, not interpolated) Co-Authored-By: Claude Opus 4.6 (1M context) * style: fix biome formatting in update-check test Co-Authored-By: Claude Opus 4.6 (1M context) * fix: CLAUDE.md says biome lint but should say biome check biome lint only checks lint rules, not formatting. biome check does both. The hooks and CI already run biome check — the docs were out of sync. Co-Authored-By: Claude Opus 4.6 (1M context) * fix(hooks): PostToolUse hook wasn't running biome on CLI source files Two bugs in validate-file.ts: 1. Config search only checked 1-2 levels up from the edited file, but biome.json is at packages/cli/ — 3 levels above src/__tests__/*.ts. Fix: walk up directories until biome.json is found (or hit root). 2. Ran `biome format` (prints formatted output, always exits 0) instead of `biome format --check` (exits non-zero if file needs formatting). Fix: use `biome check` which does lint + format check in one pass. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .claude/rules/shell-scripts.md | 4 +- .claude/scripts/validate-file.ts | 40 ++++------- .github/workflows/cli-release.yml | 10 ++- CLAUDE.md | 2 +- packages/cli/package.json | 2 +- .../commands-update-download.test.ts | 32 +++------ .../cli/src/__tests__/update-check.test.ts | 70 ++----------------- packages/cli/src/commands/help.ts | 4 +- packages/cli/src/commands/run.ts | 4 +- packages/cli/src/commands/update.ts | 20 +++++- packages/cli/src/manifest.ts | 6 +- packages/cli/src/shared/agent-setup.ts | 2 +- packages/cli/src/update-check.ts | 37 +++++----- sh/cli/install.ps1 | 4 +- sh/cli/install.sh | 7 +- 15 files changed, 95 insertions(+), 149 deletions(-) diff --git a/.claude/rules/shell-scripts.md b/.claude/rules/shell-scripts.md index 4133e129..51e9e7a0 100644 --- a/.claude/rules/shell-scripts.md +++ b/.claude/rules/shell-scripts.md @@ -18,7 +18,9 @@ macOS ships bash 3.2. All scripts MUST work on it: ## Conventions - `#!/bin/bash` + `set -eo pipefail` (no `u` flag) - Use `${VAR:-}` for all optional env var checks (`OPENROUTER_API_KEY`, cloud tokens, etc.) -- Remote fallback URL: `https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/{path}` (shell scripts are under `sh/`, e.g., `sh/{cloud}/{agent}.sh`) +- Primary script URL: `https://openrouter.ai/labs/spawn/{path}` (CDN proxy, maps to repo `sh/`, e.g., `{cloud}/{agent}.sh`) +- Fallback script URL: `https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sh/{path}` +- Version check URL: `https://github.com/OpenRouterTeam/spawn/releases/download/cli-latest/version` (GitHub release artifact) - All env vars documented in the cloud's `sh/{cloud}/README.md` ## Use Bun + TypeScript for Inline Scripting — NEVER python/python3 diff --git a/.claude/scripts/validate-file.ts b/.claude/scripts/validate-file.ts index 09d626d8..42e1c2ff 100644 --- a/.claude/scripts/validate-file.ts +++ b/.claude/scripts/validate-file.ts @@ -89,24 +89,26 @@ if (file.endsWith(".ts")) { } } - // Find biome config - const dir = dirname(file); + // Find biome config by walking up from the file's directory to the repo root let biomeDir: string | null = null; - - if (existsSync(resolve(dir, "biome.json")) || existsSync(resolve(dir, "biome.jsonc"))) { - biomeDir = dir; - } else if (existsSync(resolve(dir, "..", "biome.json")) || existsSync(resolve(dir, "..", "biome.jsonc"))) { - biomeDir = resolve(dir, ".."); + let searchDir = dirname(file); + const root = resolve("/"); + while (searchDir !== root) { + if (existsSync(resolve(searchDir, "biome.json")) || existsSync(resolve(searchDir, "biome.jsonc"))) { + biomeDir = searchDir; + break; + } + searchDir = resolve(searchDir, ".."); } if (biomeDir) { - // Run biome lint + // Run biome check (lint + format) in a single pass try { run( "bunx", [ "@biomejs/biome", - "lint", + "check", file, ], { @@ -115,25 +117,7 @@ if (file.endsWith(".ts")) { ); } catch (e) { const msg = e instanceof Error ? e.message : String(e); - fail(`BIOME LINT FAILED for ${file}\n${msg}`); - } - - // Run biome format - try { - run( - "bunx", - [ - "@biomejs/biome", - "format", - file, - ], - { - cwd: biomeDir, - }, - ); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - fail(`BIOME FORMAT FAILED for ${file}\n${msg}`); + fail(`BIOME CHECK FAILED for ${file}\n${msg}`); } } } diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml index 0abf8bc4..8d98f131 100644 --- a/.github/workflows/cli-release.yml +++ b/.github/workflows/cli-release.yml @@ -41,6 +41,10 @@ jobs: working-directory: packages/cli run: echo "version=$(jq -r .version package.json)" >> "$GITHUB_OUTPUT" + - name: Create version file + working-directory: packages/cli + run: jq -r .version package.json > version + - name: Update cli-latest release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -50,17 +54,19 @@ jobs: git tag -d cli-latest 2>/dev/null || true git push origin :refs/tags/cli-latest 2>/dev/null || true - # Create new release with built cli.js + # Create new release with built cli.js and version file gh release create cli-latest \ --title "CLI v${{ steps.version.outputs.version }}" \ --notes "Pre-built CLI binary (auto-updated on every push to main). This release is used as a fallback by \`install.sh\` when the local build fails (e.g. Termux proot). + The \`version\` file is used by the CLI's auto-update check. **Version:** ${{ steps.version.outputs.version }} **Built:** $(date -u +%Y-%m-%dT%H:%M:%SZ)" \ --prerelease \ - packages/cli/cli.js + packages/cli/cli.js \ + packages/cli/version - name: Upload cloud bundles env: diff --git a/CLAUDE.md b/CLAUDE.md index a0d48471..e732d923 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,7 +67,7 @@ All cloud provisioning and agent setup logic lives in TypeScript under `packages ## After Each Change 1. `bash -n {file}` syntax check on all modified scripts -2. `cd packages/cli && bunx @biomejs/biome lint src/` — **must pass with zero errors** on all modified TypeScript +2. `cd packages/cli && bunx @biomejs/biome check src/` — **must pass with zero errors** (lint + format) on all modified TypeScript 3. Update `manifest.json` matrix status to `"implemented"` 4. Update the cloud's `sh/{cloud}/README.md` with usage instructions 5. Commit with a descriptive message diff --git a/packages/cli/package.json b/packages/cli/package.json index 8593231a..abf49b3b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.12.14", + "version": "0.12.15", "type": "module", "bin": { "spawn": "cli.js" diff --git a/packages/cli/src/__tests__/commands-update-download.test.ts b/packages/cli/src/__tests__/commands-update-download.test.ts index 5b1d4ba5..9fe1ea35 100644 --- a/packages/cli/src/__tests__/commands-update-download.test.ts +++ b/packages/cli/src/__tests__/commands-update-download.test.ts @@ -78,12 +78,8 @@ describe("cmdUpdate", () => { it("should report up-to-date when remote version matches current", async () => { global.fetch = mock(async (url: string) => { - if (isString(url) && url.includes("package.json")) { - return new Response( - JSON.stringify({ - version: VERSION, - }), - ); + if (isString(url) && url.includes("/version")) { + return new Response(`${VERSION}\n`); } return new Response("Not Found", { status: 404, @@ -101,12 +97,8 @@ describe("cmdUpdate", () => { it("should report available update when remote version differs", async () => { global.fetch = mock(async (url: string) => { - if (isString(url) && url.includes("package.json")) { - return new Response( - JSON.stringify({ - version: "99.99.99", - }), - ); + if (isString(url) && url.includes("/version")) { + return new Response("99.99.99\n"); } return new Response("Not Found", { status: 404, @@ -154,12 +146,8 @@ describe("cmdUpdate", () => { it("should handle update failure gracefully", async () => { global.fetch = mock(async (url: string) => { - if (isString(url) && url.includes("package.json")) { - return new Response( - JSON.stringify({ - version: "99.99.99", - }), - ); + if (isString(url) && url.includes("/version")) { + return new Response("99.99.99\n"); } return new Response("Not Found", { status: 404, @@ -193,12 +181,8 @@ describe("cmdUpdate", () => { it("should show version in spinner stop during update", async () => { global.fetch = mock(async (url: string) => { - if (isString(url) && url.includes("package.json")) { - return new Response( - JSON.stringify({ - version: "2.0.0", - }), - ); + if (isString(url) && url.includes("/version")) { + return new Response("2.0.0\n"); } return new Response("Error", { status: 500, diff --git a/packages/cli/src/__tests__/update-check.test.ts b/packages/cli/src/__tests__/update-check.test.ts index da6a1bf5..b97f39bb 100644 --- a/packages/cli/src/__tests__/update-check.test.ts +++ b/packages/cli/src/__tests__/update-check.test.ts @@ -77,15 +77,7 @@ describe("update-check", () => { }); it("should check for updates on every run", async () => { - const mockFetch = mock(() => - Promise.resolve( - new Response( - JSON.stringify({ - version: "99.0.0", - }), - ), - ), - ); + const mockFetch = mock(() => Promise.resolve(new Response("99.0.0\n"))); const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch); // Mock execFileSync to prevent actual update + re-exec @@ -101,15 +93,7 @@ describe("update-check", () => { }); it("should auto-update when newer version is available", async () => { - const mockFetch = mock(() => - Promise.resolve( - new Response( - JSON.stringify({ - version: "99.0.0", - }), - ), - ), - ); + const mockFetch = mock(() => Promise.resolve(new Response("99.0.0\n"))); const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch); // Mock execFileSync to prevent actual update + re-exec @@ -137,15 +121,7 @@ describe("update-check", () => { }); it("should not update when up to date", async () => { - const mockFetch = mock(() => - Promise.resolve( - new Response( - JSON.stringify({ - version: "0.2.3", - }), - ), - ), - ); + const mockFetch = mock(() => Promise.resolve(new Response("0.2.3\n"))); const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch); // Mock executor to prevent actual commands @@ -177,15 +153,7 @@ describe("update-check", () => { }); it("should handle update failures gracefully", async () => { - const mockFetch = mock(() => - Promise.resolve( - new Response( - JSON.stringify({ - version: "99.0.0", - }), - ), - ), - ); + const mockFetch = mock(() => Promise.resolve(new Response("99.0.0\n"))); const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch); // Mock execFileSync to throw an error (curl fetch fails) @@ -237,15 +205,7 @@ describe("update-check", () => { "sprite", ]; - const mockFetch = mock(() => - Promise.resolve( - new Response( - JSON.stringify({ - version: "99.0.0", - }), - ), - ), - ); + const mockFetch = mock(() => Promise.resolve(new Response("99.0.0\n"))); const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch); const { executor } = await import("../update-check.js"); @@ -308,15 +268,7 @@ describe("update-check", () => { "sprite", ]; - const mockFetch = mock(() => - Promise.resolve( - new Response( - JSON.stringify({ - version: "99.0.0", - }), - ), - ), - ); + const mockFetch = mock(() => Promise.resolve(new Response("99.0.0\n"))); const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch); const { executor } = await import("../update-check.js"); @@ -351,15 +303,7 @@ describe("update-check", () => { "/usr/local/bin/spawn", ]; - const mockFetch = mock(() => - Promise.resolve( - new Response( - JSON.stringify({ - version: "99.0.0", - }), - ), - ), - ); + const mockFetch = mock(() => Promise.resolve(new Response("99.0.0\n"))); const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch); const { executor } = await import("../update-check.js"); diff --git a/packages/cli/src/commands/help.ts b/packages/cli/src/commands/help.ts index 2923bb31..38666b2a 100644 --- a/packages/cli/src/commands/help.ts +++ b/packages/cli/src/commands/help.ts @@ -1,5 +1,5 @@ import pc from "picocolors"; -import { RAW_BASE, REPO } from "../manifest.js"; +import { SPAWN_CDN, REPO } from "../manifest.js"; function getHelpUsageSection(): string { return `${pc.bold("USAGE")} @@ -69,7 +69,7 @@ function getHelpAuthSection(): string { function getHelpInstallSection(): string { return `${pc.bold("INSTALL")} - curl -fsSL ${RAW_BASE}/sh/cli/install.sh | bash`; + curl -fsSL ${SPAWN_CDN}/cli/install.sh | bash`; } function getHelpTroubleshootingSection(): string { diff --git a/packages/cli/src/commands/run.ts b/packages/cli/src/commands/run.ts index 3d69f79a..5b0a32b0 100644 --- a/packages/cli/src/commands/run.ts +++ b/packages/cli/src/commands/run.ts @@ -4,7 +4,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { spawn, spawnSync } from "node:child_process"; import type { Manifest } from "../manifest.js"; -import { loadManifest, RAW_BASE, REPO } from "../manifest.js"; +import { loadManifest, SPAWN_CDN, RAW_BASE, REPO } from "../manifest.js"; import { validateIdentifier, validateScriptContent, @@ -176,7 +176,7 @@ function showDryRunPreview(manifest: Manifest, agent: string, cloud: string, pro printDryRunSection("Agent", buildAgentLines(manifest.agents[agent])); printDryRunSection("Cloud", buildCloudLines(manifest.clouds[cloud])); printDryRunSection("Script", [ - ` URL: ${RAW_BASE}/sh/${cloud}/${agent}.sh`, + ` URL: ${SPAWN_CDN}/${cloud}/${agent}.sh`, ]); const envLines = buildEnvironmentLines(manifest, agent); diff --git a/packages/cli/src/commands/update.ts b/packages/cli/src/commands/update.ts index f14108df..bb83f584 100644 --- a/packages/cli/src/commands/update.ts +++ b/packages/cli/src/commands/update.ts @@ -1,12 +1,28 @@ import * as p from "@clack/prompts"; import pc from "picocolors"; import { parseJsonWith } from "../shared/parse.js"; -import { RAW_BASE } from "../manifest.js"; +import { SPAWN_CDN, VERSION_URL, RAW_BASE } from "../manifest.js"; import { VERSION, PkgVersionSchema, getErrorMessage } from "./shared.js"; -const INSTALL_CMD = `curl -fsSL ${RAW_BASE}/sh/cli/install.sh | bash`; +const INSTALL_CMD = `curl -fsSL ${SPAWN_CDN}/cli/install.sh | bash`; async function fetchRemoteVersion(): Promise { + // Primary: plain-text version file from GitHub release artifact (static URL) + try { + const res = await fetch(VERSION_URL, { + signal: AbortSignal.timeout(10_000), + }); + if (res.ok) { + const text = (await res.text()).trim(); + if (text && /^\d+\.\d+\.\d+/.test(text)) { + return text; + } + } + } catch { + // Fall through to GitHub raw fallback + } + + // Fallback: package.json from GitHub raw const res = await fetch(`${RAW_BASE}/packages/cli/package.json`, { signal: AbortSignal.timeout(10_000), }); diff --git a/packages/cli/src/manifest.ts b/packages/cli/src/manifest.ts index 87bb41bb..6c025f18 100644 --- a/packages/cli/src/manifest.ts +++ b/packages/cli/src/manifest.ts @@ -66,6 +66,10 @@ export interface Manifest { const REPO = "OpenRouterTeam/spawn"; const RAW_BASE = `https://raw.githubusercontent.com/${REPO}/main` as const; +/** Primary CDN for shell scripts — maps openrouter.ai/labs/spawn/* → repo sh/* */ +const SPAWN_CDN = "https://openrouter.ai/labs/spawn" as const; +/** Static URL for version checks — GitHub release artifact, never changes with repo structure */ +const VERSION_URL = `https://github.com/${REPO}/releases/download/cli-latest/version` as const; // Dynamic getters so tests can override XDG_CACHE_HOME at runtime function getCacheDir(): string { return join(process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), "spawn"); @@ -311,4 +315,4 @@ export function _resetCacheForTesting(): void { _staleCache = false; } -export { RAW_BASE, REPO, stripDangerousKeys }; +export { RAW_BASE, REPO, SPAWN_CDN, VERSION_URL, stripDangerousKeys }; diff --git a/packages/cli/src/shared/agent-setup.ts b/packages/cli/src/shared/agent-setup.ts index 8aa7924b..add96501 100644 --- a/packages/cli/src/shared/agent-setup.ts +++ b/packages/cli/src/shared/agent-setup.ts @@ -208,7 +208,7 @@ export async function offerGithubAuth(runner: CloudRunner): Promise { return; } - let ghCmd = "curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sh/shared/github-auth.sh | bash"; + let ghCmd = "curl -fsSL https://openrouter.ai/labs/spawn/shared/github-auth.sh | bash"; let localTmpFile = ""; if (githubToken) { const escaped = githubToken.replace(/'/g, "'\\''"); diff --git a/packages/cli/src/update-check.ts b/packages/cli/src/update-check.ts index 0436bcd0..6c4ad840 100644 --- a/packages/cli/src/update-check.ts +++ b/packages/cli/src/update-check.ts @@ -9,7 +9,7 @@ import * as v from "valibot"; import { parseJsonWith } from "./shared/parse"; import { hasStatus } from "./shared/type-guards"; import pkg from "../package.json" with { type: "json" }; -import { RAW_BASE } from "./manifest.js"; +import { SPAWN_CDN, VERSION_URL, RAW_BASE } from "./manifest.js"; const VERSION = pkg.version; @@ -29,13 +29,6 @@ const PkgVersionSchema = v.object({ version: v.string(), }); -// Validate RAW_BASE matches expected GitHub raw content URL pattern (defense-in-depth, CWE-78) -const GITHUB_RAW_URL_PATTERN = - /^https:\/\/raw\.githubusercontent\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/; -if (!GITHUB_RAW_URL_PATTERN.test(RAW_BASE)) { - throw new Error(`RAW_BASE URL does not match expected GitHub raw URL pattern: ${RAW_BASE}`); -} - // Use ASCII-safe symbols when unicode is disabled (SSH, dumb terminals) const isAscii = process.env.TERM === "linux"; const CHECK_MARK = isAscii ? "*" : "\u2713"; @@ -44,6 +37,22 @@ const CROSS_MARK = isAscii ? "x" : "\u2717"; // ── Helpers ──────────────────────────────────────────────────────────────────── async function fetchLatestVersion(): Promise { + // Primary: plain-text version file from GitHub release artifact (static URL) + try { + const res = await fetch(VERSION_URL, { + signal: AbortSignal.timeout(FETCH_TIMEOUT), + }); + if (res.ok) { + const text = (await res.text()).trim(); + if (text && /^\d+\.\d+\.\d+/.test(text)) { + return text; + } + } + } catch { + // Fall through to GitHub raw fallback + } + + // Fallback: package.json from GitHub raw try { const res = await fetch(`${RAW_BASE}/packages/cli/package.json`, { signal: AbortSignal.timeout(FETCH_TIMEOUT), @@ -51,7 +60,6 @@ async function fetchLatestVersion(): Promise { if (!res.ok) { return null; } - const data = parseJsonWith(await res.text(), PkgVersionSchema); return data?.version ?? null; } catch { @@ -202,19 +210,16 @@ function reExecWithArgs(): void { function performAutoUpdate(latestVersion: string): void { printUpdateBanner(latestVersion); - // Validate RAW_BASE immediately before use to prevent command injection (CWE-78, #1819) - if (!GITHUB_RAW_URL_PATTERN.test(RAW_BASE)) { - throw new Error(`Security: RAW_BASE failed pre-execution validation: ${RAW_BASE}`); - } + // Hardcoded CDN URL — no variable interpolation, eliminates CWE-78 concern entirely + const installUrl = `${SPAWN_CDN}/cli/install.sh`; try { // Two-step approach: fetch script bytes with curl, then execute via bash -c - // This eliminates shell interpolation of RAW_BASE entirely (CWE-78, #2161) const scriptBytes = executor.execFileSync( "curl", [ "-fsSL", - `${RAW_BASE}/sh/cli/install.sh`, + installUrl, ], { encoding: "utf8", @@ -247,7 +252,7 @@ function performAutoUpdate(latestVersion: string): void { console.error(pc.red(pc.bold(`${CROSS_MARK} Auto-update failed`))); console.error(pc.dim(" Please update manually:")); console.error(); - console.error(pc.cyan(` curl -fsSL ${RAW_BASE}/sh/cli/install.sh | bash`)); + console.error(pc.cyan(` curl -fsSL ${installUrl} | bash`)); console.error(); // Continue with original command despite update failure } diff --git a/sh/cli/install.ps1 b/sh/cli/install.ps1 index a1a7ec48..b7c71291 100644 --- a/sh/cli/install.ps1 +++ b/sh/cli/install.ps1 @@ -1,10 +1,10 @@ # Spawn CLI installer for Windows PowerShell # # Usage (PowerShell): -# irm https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sh/cli/install.ps1 | iex +# irm https://openrouter.ai/labs/spawn/cli/install.ps1 | iex # # Or download and run: -# Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sh/cli/install.ps1 -OutFile install.ps1 +# Invoke-WebRequest -Uri https://openrouter.ai/labs/spawn/cli/install.ps1 -OutFile install.ps1 # .\install.ps1 # # Override install directory: diff --git a/sh/cli/install.sh b/sh/cli/install.sh index 09cd2a16..3529d8dc 100755 --- a/sh/cli/install.sh +++ b/sh/cli/install.sh @@ -2,7 +2,7 @@ # Installer for the spawn CLI # # Usage: -# curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sh/cli/install.sh | bash +# curl -fsSL https://openrouter.ai/labs/spawn/cli/install.sh | bash # # This installs spawn via bun. If bun is not available, it auto-installs it first. # @@ -12,6 +12,7 @@ set -eo pipefail SPAWN_REPO="OpenRouterTeam/spawn" +SPAWN_CDN="https://openrouter.ai/labs/spawn" SPAWN_RAW_BASE="https://raw.githubusercontent.com/${SPAWN_REPO}/main" MIN_BUN_VERSION="1.2.0" @@ -62,7 +63,7 @@ ensure_min_bun_version() { echo " bun upgrade" echo "" echo "Then re-run:" - echo " curl -fsSL ${SPAWN_RAW_BASE}/sh/cli/install.sh | bash" + echo " curl -fsSL ${SPAWN_CDN}/cli/install.sh | bash" exit 1 fi log_info "bun upgraded to ${current}" @@ -238,7 +239,7 @@ if ! command -v bun &>/dev/null; then echo " curl -fsSL https://bun.sh/install | bash" echo "" echo "Then reopen your terminal and re-run:" - echo " curl -fsSL ${SPAWN_RAW_BASE}/sh/cli/install.sh | bash" + echo " curl -fsSL ${SPAWN_CDN}/cli/install.sh | bash" exit 1 fi