mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-19 08:01:17 +00:00
Drop nounset (set -u) flag — incompatible with env var checks (#27)
The autonomous refactoring added `set -euo pipefail` but the scripts check optional env vars with `[[ -n "$VAR" ]]` which is a fatal error under nounset when the var isn't set (e.g. SPRITE_NAME, OPENROUTER_API_KEY). Fix: downgrade to `set -eo pipefail` across all 42 affected files. Co-authored-by: Sprite <noreply@sprite.dev> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7e952d1310
commit
4087deb14e
50 changed files with 776 additions and 64 deletions
|
|
@ -88,6 +88,14 @@ Research cloud providers with API-based provisioning. To add one:
|
|||
|
||||
```
|
||||
spawn/
|
||||
cli/
|
||||
src/index.ts # CLI entry point (bun/TypeScript)
|
||||
src/manifest.ts # Manifest fetch + cache logic
|
||||
src/commands.ts # All subcommands (interactive, list, run, etc.)
|
||||
src/version.ts # Version constant
|
||||
package.json # npm package (@openrouter/spawn)
|
||||
install.sh # One-liner installer (bun → npm → bash fallback)
|
||||
spawn.sh # Bash fallback CLI (no bun/node required)
|
||||
shared/
|
||||
common.sh # Provider-agnostic shared utilities
|
||||
{cloud}/
|
||||
|
|
|
|||
6
cli/.gitignore
vendored
Normal file
6
cli/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
node_modules/
|
||||
cli.js
|
||||
spawn
|
||||
dist/
|
||||
bun.lock
|
||||
*.tgz
|
||||
110
cli/install.sh
110
cli/install.sh
|
|
@ -4,42 +4,114 @@
|
|||
# Usage:
|
||||
# curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/cli/install.sh | bash
|
||||
#
|
||||
# Override install directory:
|
||||
# This installs spawn via bun (preferred) or npm. If neither is available,
|
||||
# it falls back to downloading the bundled JS file and creating a runner script.
|
||||
#
|
||||
# Override install directory (for fallback method):
|
||||
# SPAWN_INSTALL_DIR=/usr/local/bin curl -fsSL ... | bash
|
||||
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
SPAWN_REPO="OpenRouterTeam/spawn"
|
||||
SPAWN_RAW_BASE="https://raw.githubusercontent.com/$SPAWN_REPO/main"
|
||||
INSTALL_DIR="${SPAWN_INSTALL_DIR:-$HOME/.local/bin}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[spawn]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[spawn]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[spawn]${NC} $1"; }
|
||||
|
||||
# Check curl
|
||||
if ! command -v curl &>/dev/null; then
|
||||
log_error "curl is required but not found"
|
||||
exit 1
|
||||
# --- Method 1: bun install -g (preferred) ---
|
||||
if command -v bun &>/dev/null; then
|
||||
log_info "Installing spawn via bun..."
|
||||
# Clone/download the cli directory and install from it
|
||||
tmpdir=$(mktemp -d)
|
||||
trap "rm -rf '$tmpdir'" EXIT
|
||||
|
||||
log_info "Downloading CLI package..."
|
||||
mkdir -p "$tmpdir/cli/src"
|
||||
curl -fsSL "$SPAWN_RAW_BASE/cli/package.json" -o "$tmpdir/cli/package.json"
|
||||
curl -fsSL "$SPAWN_RAW_BASE/cli/tsconfig.json" -o "$tmpdir/cli/tsconfig.json"
|
||||
curl -fsSL "$SPAWN_RAW_BASE/cli/src/index.ts" -o "$tmpdir/cli/src/index.ts"
|
||||
curl -fsSL "$SPAWN_RAW_BASE/cli/src/manifest.ts" -o "$tmpdir/cli/src/manifest.ts"
|
||||
curl -fsSL "$SPAWN_RAW_BASE/cli/src/commands.ts" -o "$tmpdir/cli/src/commands.ts"
|
||||
curl -fsSL "$SPAWN_RAW_BASE/cli/src/version.ts" -o "$tmpdir/cli/src/version.ts"
|
||||
|
||||
cd "$tmpdir/cli"
|
||||
bun install
|
||||
bun link 2>/dev/null || bun install -g . 2>/dev/null || {
|
||||
# If global install fails, build and copy binary
|
||||
log_warn "Global install failed, building binary..."
|
||||
bun build src/index.ts --compile --outfile spawn
|
||||
INSTALL_DIR="${SPAWN_INSTALL_DIR:-$HOME/.local/bin}"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
mv spawn "$INSTALL_DIR/spawn"
|
||||
log_info "Installed spawn binary to $INSTALL_DIR/spawn"
|
||||
}
|
||||
|
||||
log_info "spawn installed successfully!"
|
||||
echo ""
|
||||
if command -v spawn &>/dev/null; then
|
||||
spawn version
|
||||
echo ""
|
||||
log_info "Run ${BOLD}spawn${NC}${GREEN} to get started${NC}"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create install directory
|
||||
# --- Method 2: npm install -g ---
|
||||
if command -v npm &>/dev/null && command -v node &>/dev/null; then
|
||||
log_info "Installing spawn via npm..."
|
||||
tmpdir=$(mktemp -d)
|
||||
trap "rm -rf '$tmpdir'" EXIT
|
||||
|
||||
log_info "Downloading CLI package..."
|
||||
mkdir -p "$tmpdir/cli/src"
|
||||
curl -fsSL "$SPAWN_RAW_BASE/cli/package.json" -o "$tmpdir/cli/package.json"
|
||||
curl -fsSL "$SPAWN_RAW_BASE/cli/tsconfig.json" -o "$tmpdir/cli/tsconfig.json"
|
||||
curl -fsSL "$SPAWN_RAW_BASE/cli/src/index.ts" -o "$tmpdir/cli/src/index.ts"
|
||||
curl -fsSL "$SPAWN_RAW_BASE/cli/src/manifest.ts" -o "$tmpdir/cli/src/manifest.ts"
|
||||
curl -fsSL "$SPAWN_RAW_BASE/cli/src/commands.ts" -o "$tmpdir/cli/src/commands.ts"
|
||||
curl -fsSL "$SPAWN_RAW_BASE/cli/src/version.ts" -o "$tmpdir/cli/src/version.ts"
|
||||
|
||||
cd "$tmpdir/cli"
|
||||
npm install
|
||||
npm install -g . 2>/dev/null || {
|
||||
log_warn "npm global install requires permissions. Try:"
|
||||
echo ""
|
||||
echo " sudo npm install -g ."
|
||||
echo ""
|
||||
exit 1
|
||||
}
|
||||
|
||||
log_info "spawn installed successfully!"
|
||||
echo ""
|
||||
if command -v spawn &>/dev/null; then
|
||||
spawn version
|
||||
echo ""
|
||||
log_info "Run ${BOLD}spawn${NC}${GREEN} to get started${NC}"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Method 3: Direct download fallback (bash wrapper) ---
|
||||
log_warn "Neither bun nor npm found. Installing bash fallback..."
|
||||
|
||||
INSTALL_DIR="${SPAWN_INSTALL_DIR:-$HOME/.local/bin}"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# Download spawn CLI
|
||||
log_info "Downloading spawn CLI..."
|
||||
if ! curl -fsSL "$SPAWN_RAW_BASE/cli/spawn.sh" -o "$INSTALL_DIR/spawn"; then
|
||||
log_error "Failed to download spawn CLI"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod +x "$INSTALL_DIR/spawn"
|
||||
log_info "Installed spawn to $INSTALL_DIR/spawn"
|
||||
log_info "Installed spawn (bash) to $INSTALL_DIR/spawn"
|
||||
|
||||
# Check if install dir is in PATH
|
||||
if ! echo "$PATH" | tr ':' '\n' | grep -qx "$INSTALL_DIR"; then
|
||||
|
|
@ -47,20 +119,12 @@ if ! echo "$PATH" | tr ':' '\n' | grep -qx "$INSTALL_DIR"; then
|
|||
echo ""
|
||||
echo "Add it by running one of:"
|
||||
echo ""
|
||||
|
||||
# Detect shell and suggest appropriate config
|
||||
case "${SHELL:-/bin/bash}" in
|
||||
*/zsh)
|
||||
echo " echo 'export PATH=\"$INSTALL_DIR:\$PATH\"' >> ~/.zshrc && source ~/.zshrc"
|
||||
;;
|
||||
*/fish)
|
||||
echo " fish_add_path $INSTALL_DIR"
|
||||
;;
|
||||
*)
|
||||
echo " echo 'export PATH=\"$INSTALL_DIR:\$PATH\"' >> ~/.bashrc && source ~/.bashrc"
|
||||
;;
|
||||
*/zsh) echo " echo 'export PATH=\"$INSTALL_DIR:\$PATH\"' >> ~/.zshrc && source ~/.zshrc" ;;
|
||||
*/fish) echo " fish_add_path $INSTALL_DIR" ;;
|
||||
*) echo " echo 'export PATH=\"$INSTALL_DIR:\$PATH\"' >> ~/.bashrc && source ~/.bashrc" ;;
|
||||
esac
|
||||
echo ""
|
||||
else
|
||||
log_info "Run 'spawn' to get started"
|
||||
log_info "Run ${BOLD}spawn${NC}${GREEN} to get started${NC}"
|
||||
fi
|
||||
|
|
|
|||
20
cli/package.json
Normal file
20
cli/package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "@openrouter/spawn",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"spawn": "cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
"build": "bun build src/index.ts --outfile cli.js --target node --minify",
|
||||
"compile": "bun build src/index.ts --compile --outfile spawn"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.10.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.0"
|
||||
}
|
||||
}
|
||||
371
cli/src/commands.ts
Normal file
371
cli/src/commands.ts
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
import * as p from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { spawn } from "child_process";
|
||||
import {
|
||||
loadManifest,
|
||||
agentKeys,
|
||||
cloudKeys,
|
||||
matrixStatus,
|
||||
countImplemented,
|
||||
RAW_BASE,
|
||||
REPO,
|
||||
CACHE_DIR,
|
||||
type Manifest,
|
||||
} from "./manifest.js";
|
||||
import { VERSION } from "./version.js";
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
function handleCancel(): never {
|
||||
p.cancel("Cancelled.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async function withSpinner<T>(msg: string, fn: () => Promise<T>): Promise<T> {
|
||||
const s = p.spinner();
|
||||
s.start(msg);
|
||||
try {
|
||||
const result = await fn();
|
||||
s.stop(msg);
|
||||
return result;
|
||||
} catch (err) {
|
||||
s.stop(pc.red("Failed"));
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Interactive ────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function cmdInteractive() {
|
||||
p.intro(pc.inverse(` spawn v${VERSION} `));
|
||||
|
||||
const manifest = await withSpinner("Loading manifest...", loadManifest);
|
||||
|
||||
const agents = agentKeys(manifest);
|
||||
const agentChoice = await p.select({
|
||||
message: "Select an agent",
|
||||
options: agents.map((key) => ({
|
||||
value: key,
|
||||
label: manifest.agents[key].name,
|
||||
hint: manifest.agents[key].description,
|
||||
})),
|
||||
});
|
||||
if (p.isCancel(agentChoice)) handleCancel();
|
||||
|
||||
// Only show clouds where this agent is implemented
|
||||
const clouds = cloudKeys(manifest).filter(
|
||||
(c) => matrixStatus(manifest, c, agentChoice) === "implemented"
|
||||
);
|
||||
|
||||
if (clouds.length === 0) {
|
||||
p.log.error(`No clouds available for ${manifest.agents[agentChoice].name}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const cloudChoice = await p.select({
|
||||
message: "Select a cloud provider",
|
||||
options: clouds.map((key) => ({
|
||||
value: key,
|
||||
label: manifest.clouds[key].name,
|
||||
hint: manifest.clouds[key].description,
|
||||
})),
|
||||
});
|
||||
if (p.isCancel(cloudChoice)) handleCancel();
|
||||
|
||||
const agentName = manifest.agents[agentChoice].name;
|
||||
const cloudName = manifest.clouds[cloudChoice].name;
|
||||
p.log.step(`Launching ${pc.bold(agentName)} on ${pc.bold(cloudName)}`);
|
||||
p.outro("Handing off to spawn script...");
|
||||
|
||||
await execScript(cloudChoice, agentChoice);
|
||||
}
|
||||
|
||||
// ── Run ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function cmdRun(agent: string, cloud: string) {
|
||||
const manifest = await withSpinner("Loading manifest...", loadManifest);
|
||||
|
||||
if (!manifest.agents[agent]) {
|
||||
p.log.error(`Unknown agent: ${pc.bold(agent)}`);
|
||||
p.log.info(`Run ${pc.cyan("spawn agents")} to see available agents.`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!manifest.clouds[cloud]) {
|
||||
p.log.error(`Unknown cloud: ${pc.bold(cloud)}`);
|
||||
p.log.info(`Run ${pc.cyan("spawn clouds")} to see available clouds.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const status = matrixStatus(manifest, cloud, agent);
|
||||
if (status !== "implemented") {
|
||||
p.log.error(
|
||||
`${manifest.agents[agent].name} on ${manifest.clouds[cloud].name} is not yet implemented.`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const agentName = manifest.agents[agent].name;
|
||||
const cloudName = manifest.clouds[cloud].name;
|
||||
p.log.step(`Launching ${pc.bold(agentName)} on ${pc.bold(cloudName)}...`);
|
||||
|
||||
await execScript(cloud, agent);
|
||||
}
|
||||
|
||||
async function execScript(cloud: string, agent: string): Promise<void> {
|
||||
const url = `https://openrouter.ai/lab/spawn/${cloud}/${agent}.sh`;
|
||||
|
||||
// Download script then execute, preserving stdin/stdout/stderr for interactive use
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
// Fallback to GitHub raw
|
||||
const ghUrl = `${RAW_BASE}/${cloud}/${agent}.sh`;
|
||||
const ghRes = await fetch(ghUrl);
|
||||
if (!ghRes.ok) {
|
||||
p.log.error(`Failed to download script from ${url}`);
|
||||
process.exit(1);
|
||||
}
|
||||
await runBash(await ghRes.text());
|
||||
return;
|
||||
}
|
||||
await runBash(await res.text());
|
||||
}
|
||||
|
||||
function runBash(script: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn("bash", ["-c", script], {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(`Script exited with code ${code}`));
|
||||
});
|
||||
child.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
// ── List ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function cmdList() {
|
||||
const manifest = await withSpinner("Loading manifest...", loadManifest);
|
||||
|
||||
const agents = agentKeys(manifest);
|
||||
const clouds = cloudKeys(manifest);
|
||||
|
||||
// Calculate column widths
|
||||
const agentColWidth = Math.max(16, ...agents.map((a) => manifest.agents[a].name.length + 2));
|
||||
const cloudColWidth = Math.max(
|
||||
10,
|
||||
...clouds.map((c) => manifest.clouds[c].name.length + 2)
|
||||
);
|
||||
|
||||
// Header
|
||||
let header = "".padEnd(agentColWidth);
|
||||
for (const c of clouds) {
|
||||
header += pc.bold(manifest.clouds[c].name.padEnd(cloudColWidth));
|
||||
}
|
||||
console.log();
|
||||
console.log(header);
|
||||
|
||||
// Separator
|
||||
let sep = "".padEnd(agentColWidth);
|
||||
for (const _ of clouds) {
|
||||
sep += pc.dim("─".repeat(cloudColWidth - 2) + " ");
|
||||
}
|
||||
console.log(sep);
|
||||
|
||||
// Rows
|
||||
for (const a of agents) {
|
||||
let row = pc.bold(manifest.agents[a].name.padEnd(agentColWidth));
|
||||
for (const c of clouds) {
|
||||
const status = matrixStatus(manifest, c, a);
|
||||
if (status === "implemented") {
|
||||
row += pc.green(" \u2713".padEnd(cloudColWidth));
|
||||
} else {
|
||||
row += pc.dim(" \u2013".padEnd(cloudColWidth));
|
||||
}
|
||||
}
|
||||
console.log(row);
|
||||
}
|
||||
|
||||
// Summary
|
||||
const impl = countImplemented(manifest);
|
||||
const total = agents.length * clouds.length;
|
||||
console.log();
|
||||
console.log(pc.green(`${impl}/${total} combinations implemented`));
|
||||
console.log();
|
||||
}
|
||||
|
||||
// ── Agents ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function cmdAgents() {
|
||||
const manifest = await withSpinner("Loading manifest...", loadManifest);
|
||||
|
||||
console.log();
|
||||
console.log(pc.bold("Agents"));
|
||||
console.log();
|
||||
for (const key of agentKeys(manifest)) {
|
||||
const a = manifest.agents[key];
|
||||
console.log(` ${pc.green(a.name.padEnd(18))} ${pc.dim(a.description)}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// ── Clouds ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function cmdClouds() {
|
||||
const manifest = await withSpinner("Loading manifest...", loadManifest);
|
||||
|
||||
console.log();
|
||||
console.log(pc.bold("Cloud Providers"));
|
||||
console.log();
|
||||
for (const key of cloudKeys(manifest)) {
|
||||
const c = manifest.clouds[key];
|
||||
console.log(` ${pc.green(c.name.padEnd(18))} ${pc.dim(c.description)}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// ── Agent Info ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function cmdAgentInfo(agent: string) {
|
||||
const manifest = await withSpinner("Loading manifest...", loadManifest);
|
||||
|
||||
if (!manifest.agents[agent]) {
|
||||
p.log.error(`Unknown agent: ${pc.bold(agent)}`);
|
||||
p.log.info(`Run ${pc.cyan("spawn agents")} to see available agents.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const a = manifest.agents[agent];
|
||||
console.log();
|
||||
console.log(`${pc.bold(a.name)} ${pc.dim("\u2014")} ${a.description}`);
|
||||
console.log();
|
||||
console.log(pc.bold("Available clouds:"));
|
||||
console.log();
|
||||
|
||||
let found = false;
|
||||
for (const cloud of cloudKeys(manifest)) {
|
||||
const status = matrixStatus(manifest, cloud, agent);
|
||||
if (status === "implemented") {
|
||||
const c = manifest.clouds[cloud];
|
||||
console.log(` ${pc.green(c.name.padEnd(18))} ${pc.dim("spawn " + agent + " " + cloud)}`);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
console.log(pc.dim(" No implemented clouds yet."));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// ── Improve ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function cmdImprove(args: string[]) {
|
||||
const { existsSync: exists } = await import("fs");
|
||||
|
||||
let repoDir: string;
|
||||
|
||||
// Check if we're in a spawn checkout
|
||||
if (exists("./improve.sh") && exists("./manifest.json")) {
|
||||
repoDir = ".";
|
||||
} else {
|
||||
const { join } = await import("path");
|
||||
repoDir = join(CACHE_DIR, "repo");
|
||||
|
||||
if (exists(join(repoDir, ".git"))) {
|
||||
p.log.step("Updating spawn repo...");
|
||||
const { execSync } = await import("child_process");
|
||||
try {
|
||||
execSync("git pull --ff-only", { cwd: repoDir, stdio: "pipe" });
|
||||
} catch {
|
||||
// ignore pull failures
|
||||
}
|
||||
} else {
|
||||
p.log.step("Cloning spawn repo...");
|
||||
const { execSync } = await import("child_process");
|
||||
execSync(`git clone https://github.com/${REPO}.git ${repoDir}`, { stdio: "inherit" });
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const child = spawn("bash", ["improve.sh", ...args], {
|
||||
cwd: repoDir,
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(`improve.sh exited with code ${code}`));
|
||||
});
|
||||
child.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Update ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function cmdUpdate() {
|
||||
const s = p.spinner();
|
||||
s.start("Checking for updates...");
|
||||
|
||||
try {
|
||||
const res = await fetch(`${RAW_BASE}/cli/package.json`, {
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
if (!res.ok) throw new Error("fetch failed");
|
||||
const pkg = (await res.json()) as { version: string };
|
||||
const remoteVersion = pkg.version;
|
||||
|
||||
if (remoteVersion === VERSION) {
|
||||
s.stop(`Already up to date ${pc.dim(`(v${VERSION})`)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
s.message(`Updating v${VERSION} \u2192 v${remoteVersion}...`);
|
||||
|
||||
// Run the install script to update
|
||||
const installRes = await fetch(`${RAW_BASE}/cli/install.sh`);
|
||||
if (!installRes.ok) throw new Error("fetch install.sh failed");
|
||||
const installScript = await installRes.text();
|
||||
|
||||
s.stop(`Update available: v${VERSION} \u2192 v${remoteVersion}`);
|
||||
p.log.info(`Run this to update:`);
|
||||
console.log();
|
||||
console.log(
|
||||
` ${pc.cyan(`curl -fsSL ${RAW_BASE}/cli/install.sh | bash`)}`
|
||||
);
|
||||
console.log();
|
||||
} catch {
|
||||
s.stop(pc.red("Failed to check for updates"));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Help ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function cmdHelp() {
|
||||
console.log(`
|
||||
${pc.bold("spawn")} \u2014 Launch any AI coding agent on any cloud
|
||||
|
||||
${pc.bold("USAGE")}
|
||||
spawn Interactive agent + cloud picker
|
||||
spawn <agent> <cloud> Launch agent on cloud directly
|
||||
spawn <agent> Show available clouds for agent
|
||||
spawn list Full matrix table
|
||||
spawn agents List all agents with descriptions
|
||||
spawn clouds List all cloud providers
|
||||
spawn improve [--loop] Run improvement system
|
||||
spawn update Check for CLI updates
|
||||
spawn version Show version
|
||||
|
||||
${pc.bold("EXAMPLES")}
|
||||
spawn ${pc.dim("# Pick interactively")}
|
||||
spawn claude sprite ${pc.dim("# Launch Claude Code on Sprite")}
|
||||
spawn aider hetzner ${pc.dim("# Launch Aider on Hetzner Cloud")}
|
||||
spawn claude ${pc.dim("# Show which clouds support Claude")}
|
||||
spawn list ${pc.dim("# See the full agent x cloud matrix")}
|
||||
|
||||
${pc.bold("INSTALL")}
|
||||
curl -fsSL ${RAW_BASE}/cli/install.sh | bash
|
||||
`);
|
||||
}
|
||||
95
cli/src/index.ts
Normal file
95
cli/src/index.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
#!/usr/bin/env bun
|
||||
import {
|
||||
cmdInteractive,
|
||||
cmdRun,
|
||||
cmdList,
|
||||
cmdAgents,
|
||||
cmdClouds,
|
||||
cmdAgentInfo,
|
||||
cmdImprove,
|
||||
cmdUpdate,
|
||||
cmdHelp,
|
||||
} from "./commands.js";
|
||||
import { loadManifest } from "./manifest.js";
|
||||
import { VERSION } from "./version.js";
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const cmd = args[0];
|
||||
|
||||
try {
|
||||
if (!cmd) {
|
||||
// No args + interactive terminal → picker
|
||||
if (process.stdin.isTTY && process.stdout.isTTY) {
|
||||
await cmdInteractive();
|
||||
} else {
|
||||
cmdHelp();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (cmd) {
|
||||
case "help":
|
||||
case "--help":
|
||||
case "-h":
|
||||
cmdHelp();
|
||||
break;
|
||||
|
||||
case "version":
|
||||
case "--version":
|
||||
case "-v":
|
||||
case "-V":
|
||||
console.log(`spawn v${VERSION}`);
|
||||
break;
|
||||
|
||||
case "list":
|
||||
case "ls":
|
||||
await cmdList();
|
||||
break;
|
||||
|
||||
case "agents":
|
||||
await cmdAgents();
|
||||
break;
|
||||
|
||||
case "clouds":
|
||||
await cmdClouds();
|
||||
break;
|
||||
|
||||
case "improve":
|
||||
await cmdImprove(args.slice(1));
|
||||
break;
|
||||
|
||||
case "update":
|
||||
await cmdUpdate();
|
||||
break;
|
||||
|
||||
default: {
|
||||
// spawn <agent> or spawn <agent> <cloud>
|
||||
const agent = args[0];
|
||||
const cloud = args[1];
|
||||
|
||||
// Quick check: is this a known agent?
|
||||
const manifest = await loadManifest();
|
||||
if (!manifest.agents[agent]) {
|
||||
console.error(`Unknown command or agent: ${agent}`);
|
||||
console.error(`Run 'spawn help' for usage.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (cloud) {
|
||||
await cmdRun(agent, cloud);
|
||||
} else {
|
||||
await cmdAgentInfo(agent);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
console.error(`Error: ${err.message}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
133
cli/src/manifest.ts
Normal file
133
cli/src/manifest.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { homedir } from "os";
|
||||
|
||||
// ── Types ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface AgentDef {
|
||||
name: string;
|
||||
description: string;
|
||||
url: string;
|
||||
install: string;
|
||||
launch: string;
|
||||
env: Record<string, string>;
|
||||
pre_launch?: string;
|
||||
deps?: string[];
|
||||
config_files?: Record<string, unknown>;
|
||||
interactive_prompts?: Record<string, { prompt: string; default: string }>;
|
||||
dotenv?: { path: string; values: Record<string, string> };
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface CloudDef {
|
||||
name: string;
|
||||
description: string;
|
||||
url: string;
|
||||
type: string;
|
||||
auth: string;
|
||||
provision_method: string;
|
||||
exec_method: string;
|
||||
interactive_method: string;
|
||||
defaults?: Record<string, unknown>;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface Manifest {
|
||||
agents: Record<string, AgentDef>;
|
||||
clouds: Record<string, CloudDef>;
|
||||
matrix: Record<string, string>;
|
||||
}
|
||||
|
||||
// ── Constants ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const REPO = "OpenRouterTeam/spawn";
|
||||
const RAW_BASE = `https://raw.githubusercontent.com/${REPO}/main`;
|
||||
const CACHE_DIR = join(process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), "spawn");
|
||||
const CACHE_FILE = join(CACHE_DIR, "manifest.json");
|
||||
const CACHE_TTL = 3600; // 1 hour in seconds
|
||||
|
||||
// ── Cache helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
function cacheAge(): number {
|
||||
try {
|
||||
const st = statSync(CACHE_FILE);
|
||||
return (Date.now() - st.mtimeMs) / 1000;
|
||||
} catch {
|
||||
return Infinity;
|
||||
}
|
||||
}
|
||||
|
||||
function readCache(): Manifest | null {
|
||||
try {
|
||||
return JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeCache(data: Manifest): void {
|
||||
mkdirSync(CACHE_DIR, { recursive: true });
|
||||
writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
// ── Public API ─────────────────────────────────────────────────────────────────
|
||||
|
||||
let _cached: Manifest | null = null;
|
||||
|
||||
export async function loadManifest(forceRefresh = false): Promise<Manifest> {
|
||||
if (_cached && !forceRefresh) return _cached;
|
||||
|
||||
// Check disk cache first
|
||||
if (!forceRefresh && cacheAge() < CACHE_TTL) {
|
||||
const cached = readCache();
|
||||
if (cached) {
|
||||
_cached = cached;
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch from GitHub
|
||||
try {
|
||||
const res = await fetch(`${RAW_BASE}/manifest.json`, {
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = (await res.json()) as Manifest;
|
||||
// Validate basic structure
|
||||
if (data.agents && data.clouds && data.matrix) {
|
||||
writeCache(data);
|
||||
_cached = data;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Network error — fall through to cache
|
||||
}
|
||||
|
||||
// Offline fallback: use stale cache
|
||||
const stale = readCache();
|
||||
if (stale) {
|
||||
_cached = stale;
|
||||
return stale;
|
||||
}
|
||||
|
||||
throw new Error("Cannot load manifest. Check your internet connection.");
|
||||
}
|
||||
|
||||
export function agentKeys(m: Manifest): string[] {
|
||||
return Object.keys(m.agents);
|
||||
}
|
||||
|
||||
export function cloudKeys(m: Manifest): string[] {
|
||||
return Object.keys(m.clouds);
|
||||
}
|
||||
|
||||
export function matrixStatus(m: Manifest, cloud: string, agent: string): string {
|
||||
return m.matrix[`${cloud}/${agent}`] ?? "missing";
|
||||
}
|
||||
|
||||
export function countImplemented(m: Manifest): number {
|
||||
return Object.values(m.matrix).filter((v) => v === "implemented").length;
|
||||
}
|
||||
|
||||
export { RAW_BASE, REPO, CACHE_DIR };
|
||||
1
cli/src/version.ts
Normal file
1
cli/src/version.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const VERSION = "0.1.0";
|
||||
14
cli/tsconfig.json
Normal file
14
cli/tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"types": ["bun"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Common bash functions for DigitalOcean spawn scripts
|
||||
|
||||
# Bash safety flags
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
# ============================================================
|
||||
# Provider-agnostic functions
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
# Common bash functions for Hetzner Cloud spawn scripts
|
||||
|
||||
# ============================================================
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then source "$SCRIPT_DIR/lib/common.sh"
|
||||
else eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/linode/lib/common.sh)"; fi
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then source "$SCRIPT_DIR/lib/common.sh"
|
||||
else eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/linode/lib/common.sh)"; fi
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then source "$SCRIPT_DIR/lib/common.sh"
|
||||
else eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/linode/lib/common.sh)"; fi
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then source "$SCRIPT_DIR/lib/common.sh"
|
||||
else eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/linode/lib/common.sh)"; fi
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then source "$SCRIPT_DIR/lib/common.sh"
|
||||
else eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/linode/lib/common.sh)"; fi
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Common bash functions for Linode (Akamai) spawn scripts
|
||||
|
||||
# Bash safety flags
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
# ============================================================
|
||||
# Provider-agnostic functions
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then source "$SCRIPT_DIR/lib/common.sh"
|
||||
else eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/linode/lib/common.sh)"; fi
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then source "$SCRIPT_DIR/lib/common.sh"
|
||||
else eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/linode/lib/common.sh)"; fi
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
# Provider-agnostic utilities for logging, input, OAuth, etc.
|
||||
#
|
||||
# This file is meant to be sourced by cloud provider-specific common.sh files.
|
||||
# It does not set bash flags (like set -euo pipefail) as those should be set
|
||||
# It does not set bash flags (like set -eo pipefail) as those should be set
|
||||
# by the scripts that source this file.
|
||||
|
||||
# ============================================================
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
# Common bash functions shared between spawn scripts
|
||||
|
||||
# Source shared provider-agnostic functions (local or remote fallback)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Common bash functions for Vultr spawn scripts
|
||||
|
||||
# Bash safety flags
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
# ============================================================
|
||||
# Provider-agnostic functions
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue