mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
feat: migrate to openrouter.ai/labs/spawn CDN + release artifact version checks (#2178)
* 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) <noreply@anthropic.com> * style: fix biome formatting in update-check test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
67443aa4b3
commit
61bcedc0eb
15 changed files with 95 additions and 149 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
.github/workflows/cli-release.yml
vendored
10
.github/workflows/cli-release.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openrouter/spawn",
|
||||
"version": "0.12.14",
|
||||
"version": "0.12.15",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"spawn": "cli.js"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<string> {
|
||||
// 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),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ export async function offerGithubAuth(runner: CloudRunner): Promise<void> {
|
|||
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, "'\\''");
|
||||
|
|
|
|||
|
|
@ -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<string | null> {
|
||||
// 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<string | null> {
|
|||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue