diff --git a/INTERACTIVE_CURL.md b/INTERACTIVE_CURL.md new file mode 100644 index 00000000..6fc804b0 --- /dev/null +++ b/INTERACTIVE_CURL.md @@ -0,0 +1,241 @@ +# Interactive Execution via curl + +## The Problem with Piping + +When you run `curl URL | bash`: +- curl outputs the script to stdout +- stdout is piped to bash's stdin +- bash reads the script from stdin +- **stdin is no longer available for the script to read user input** + +This is why `/dev/tty` tricks and other workarounds are needed. + +## The Solution: Process Substitution + +Use bash process substitution instead of piping: + +```bash +bash <(curl -fsSL URL) +``` + +### How It Works + +1. `<(curl -fsSL URL)` - Process substitution creates a temporary file descriptor +2. curl downloads the script and writes to this descriptor +3. bash reads the script from the file descriptor +4. **stdin remains connected to your terminal** for interactive input! + +## Comparison + +### ❌ Piping (Not Interactive) +```bash +curl URL | bash +# - Script goes to bash's stdin +# - No stdin available for user input +# - Need /dev/tty workarounds +``` + +### ✅ Process Substitution (Fully Interactive) +```bash +bash <(curl -fsSL URL) +# - Script goes to file descriptor +# - stdin available for user input +# - Works like a normal bash script +``` + +## Updated Usage Examples + +### Claude Code Setup (Interactive) + +```bash +# Recommended - Fully interactive +bash <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sprite/claude.sh) + +# Alternative - Non-interactive with env vars +SPRITE_NAME=dev-mk1 bash <(curl -fsSL https://raw.githubusercontent.com/.../claude.sh) +``` + +### OpenClaw Setup (Interactive) + +```bash +# Recommended - Fully interactive +bash <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sprite/openclaw.sh) + +# Alternative - Non-interactive +SPRITE_NAME=dev-mk1 bash <(curl -fsSL https://raw.githubusercontent.com/.../openclaw.sh) +``` + +## Why This Is Better + +### Process Substitution Advantages + +✅ **Fully interactive** - stdin available for prompts +✅ **No special code needed** - Regular `read` commands work +✅ **Cleaner implementation** - No `/dev/tty` fallbacks required +✅ **Works everywhere** - bash/zsh on Linux/macOS +✅ **Better UX** - Users can see prompts and type naturally + +### Piping Disadvantages + +❌ **Not interactive by default** - stdin consumed by bash +❌ **Requires workarounds** - Need `/dev/tty`, `safe_read()`, etc. +❌ **Fragile** - TTY may not be available (Docker, CI/CD) +❌ **Poor UX** - Needs env vars or fails with cryptic errors + +## Implementation Notes + +### Current Implementation (Supports Both) + +Our scripts now support BOTH approaches: + +1. **Process substitution** (recommended): + ```bash + bash <(curl -fsSL URL) + # Uses regular read commands, fully interactive + ``` + +2. **Piping** (fallback): + ```bash + curl URL | bash + # Fails gracefully, shows helpful error: + # "Set SPRITE_NAME environment variable for non-interactive usage" + ``` + +3. **Non-interactive** (CI/CD): + ```bash + SPRITE_NAME=dev-mk1 curl URL | bash + # Works without prompts + ``` + +### Simplification Opportunity + +If we only recommend process substitution, we could: +- Remove `safe_read()` complexity +- Remove environment variable checks +- Use simple `read -p "prompt: " var` everywhere +- Simpler codebase + +**But we keep both because:** +- Some tutorials/docs use piping pattern +- CI/CD needs non-interactive mode +- Environment variables are useful anyway +- Graceful degradation is better UX + +## Platform Support + +### ✅ Supported Platforms + +- **bash** (v3.0+) - Linux, macOS, WSL, Git Bash +- **zsh** - macOS, Linux +- Anywhere with `/dev/fd` support + +### ❌ Unsupported + +- Very old shells without process substitution +- `sh` (POSIX shell) - doesn't support `<(...)` +- Some embedded/minimal environments + +**Fallback:** Download the script first: +```bash +curl -fsSL URL -o script.sh +bash script.sh +rm script.sh +``` + +## Examples + +### Test Interactive Mode + +```bash +# This will prompt you for sprite name and guide through OAuth +bash <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sprite/claude.sh) +``` + +Expected output: +``` +Claude Code on Sprite + +Installing sprite CLI... +Enter sprite name: █ +``` + +### Test with Pre-Set Name + +```bash +# Skips sprite name prompt, but OAuth is interactive +SPRITE_NAME=dev-mk1 bash <(curl -fsSL https://raw.githubusercontent.com/.../claude.sh) +``` + +### Fully Automated + +```bash +# No prompts at all +SPRITE_NAME=ci-test \ +OPENROUTER_API_KEY=sk-or-v1-xxxxx \ + bash <(curl -fsSL https://raw.githubusercontent.com/.../claude.sh) +``` + +## Recommended Documentation Update + +We should update README.md to show process substitution first: + +### Before (Current) +```bash +curl https://openrouter.ai/lab/spawn/sprite/claude.sh | bash +``` + +### After (Recommended) +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/sprite/claude.sh) +``` + +This gives users the best experience by default while still supporting non-interactive usage. + +## Technical Details + +### What is Process Substitution? + +In bash, `<(command)` is replaced with a file descriptor path like `/dev/fd/63`: + +```bash +# This: +bash <(curl -fsSL URL) + +# Becomes: +bash /dev/fd/63 +# where /dev/fd/63 contains the curl output +``` + +### stdin Flow + +**With piping:** +``` +Terminal stdin → curl (unused) +curl stdout → bash stdin (script) +bash needs input → NO STDIN AVAILABLE ❌ +``` + +**With process substitution:** +``` +Terminal stdin → bash stdin (available!) ✅ +curl stdout → /dev/fd/63 (script) +bash reads script from /dev/fd/63 +bash needs input → reads from stdin ✅ +``` + +## Security Considerations + +Both methods have similar security profiles: + +```bash +# Review before executing +curl -fsSL URL -o script.sh +cat script.sh # Review +bash script.sh + +# Or review inline +curl -fsSL URL | less # Review +bash <(curl -fsSL URL) # Execute +``` + +**Best practice:** Always review scripts from the internet before executing them. diff --git a/NON_INTERACTIVE_MODE.md b/NON_INTERACTIVE_MODE.md new file mode 100644 index 00000000..eec1c517 --- /dev/null +++ b/NON_INTERACTIVE_MODE.md @@ -0,0 +1,220 @@ +# Non-Interactive Mode + +## The Problem + +When running scripts via `curl URL | bash`, the script is piped directly to bash without access to a TTY (terminal). This causes failures when trying to read user input: + +```bash +$ curl https://raw.githubusercontent.com/.../claude.sh | bash +/dev/fd/63: line 42: /dev/tty: No such device or address +``` + +**Why this happens:** +- `curl | bash` pipes the script content to bash's stdin +- `/dev/tty` doesn't exist in this context +- `read` commands fail with "No such device or address" + +## The Solution + +We implemented a `safe_read()` function that: + +1. Checks if `/dev/tty` is available +2. Falls back to stdin if available +3. Returns a helpful error message if neither works +4. Suggests using environment variables for non-interactive usage + +### Environment Variable Support + +Scripts now support environment variables for non-interactive execution: + +```bash +# Required +SPRITE_NAME=dev-mk1 \ + curl URL | bash + +# Optional (skips OAuth) +SPRITE_NAME=dev-mk1 \ +OPENROUTER_API_KEY=sk-or-v1-xxxxx \ + curl URL | bash +``` + +## Implementation Details + +### safe_read() Function + +```bash +safe_read() { + local prompt="$1" + local result="" + + # Try to read from TTY if available + if [[ -c /dev/tty ]]; then + read -p "$prompt" result < /dev/tty + elif [[ -t 0 ]]; then + # stdin is a terminal + read -p "$prompt" result + else + # No interactive input available + log_error "Cannot read input: no TTY available" + return 1 + fi + + echo "$result" +} +``` + +### get_sprite_name() Function + +```bash +get_sprite_name() { + # Check if SPRITE_NAME is already set in environment + if [[ -n "$SPRITE_NAME" ]]; then + log_info "Using sprite name from environment: $SPRITE_NAME" + echo "$SPRITE_NAME" + return 0 + fi + + # Try to read interactively + local sprite_name=$(safe_read "Enter sprite name: ") + if [[ -z "$sprite_name" ]]; then + log_error "Sprite name is required" + log_warn "Set SPRITE_NAME environment variable for non-interactive usage:" + log_warn " SPRITE_NAME=dev-mk1 curl ... | bash" + return 1 + fi + + echo "$sprite_name" +} +``` + +## Testing + +### Test Interactive Mode + +```bash +# Will prompt for sprite name +curl https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sprite/claude.sh | bash +``` + +### Test Non-Interactive Mode + +```bash +# Provides all inputs via environment +SPRITE_NAME=test-sprite \ +OPENROUTER_API_KEY=sk-or-v1-xxxxx \ + curl https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sprite/claude.sh | bash +``` + +### Test Local Execution + +```bash +# Interactive +bash sprite/claude.sh + +# Non-interactive +SPRITE_NAME=test-sprite bash sprite/claude.sh +``` + +## Error Messages + +### Without SPRITE_NAME in non-interactive mode: + +``` +❌ Cannot read input: no TTY available +❌ Sprite name is required +⚠️ Set SPRITE_NAME environment variable for non-interactive usage: +⚠️ SPRITE_NAME=dev-mk1 curl ... | bash +``` + +### Without OAuth capability in non-interactive mode: + +``` +⚠️ netcat (nc) not found - OAuth server unavailable +⚠️ OAuth authentication failed or unavailable +⚠️ You can enter your API key manually instead +❌ Cannot prompt for manual entry in non-interactive mode +⚠️ Set OPENROUTER_API_KEY environment variable for non-interactive usage +``` + +## Use Cases + +### CI/CD Pipelines + +```yaml +# GitHub Actions example +- name: Setup Sprite + run: | + SPRITE_NAME=ci-test-${{ github.run_id }} \ + OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }} \ + curl -fsSL https://raw.githubusercontent.com/.../claude.sh | bash +``` + +### Automation Scripts + +```bash +#!/bin/bash +# setup-dev-env.sh + +export SPRITE_NAME="dev-$(whoami)" +export OPENROUTER_API_KEY="sk-or-v1-xxxxx" + +curl -fsSL https://raw.githubusercontent.com/.../claude.sh | bash +``` + +### Docker Containers + +```dockerfile +FROM ubuntu:latest + +ENV SPRITE_NAME=docker-dev +ENV OPENROUTER_API_KEY=sk-or-v1-xxxxx + +RUN curl -fsSL https://raw.githubusercontent.com/.../claude.sh | bash +``` + +## Comparison: Before vs After + +### Before (Broken in non-interactive mode) + +```bash +get_sprite_name() { + read -p "Enter sprite name: " SPRITE_NAME < /dev/tty + echo "$SPRITE_NAME" +} +# ERROR: /dev/tty: No such device or address +``` + +### After (Works in both modes) + +```bash +get_sprite_name() { + # Check environment first + if [[ -n "$SPRITE_NAME" ]]; then + echo "$SPRITE_NAME" + return 0 + fi + + # Try interactive input + local sprite_name=$(safe_read "Enter sprite name: ") + [[ -n "$sprite_name" ]] || return 1 + + echo "$sprite_name" +} +# SUCCESS: Uses env var or prompts interactively +``` + +## Related Files + +- `sprite/lib/common.sh` - Contains `safe_read()` and updated functions +- `sprite/openclaw.sh` - Updated to use `safe_read()` for model selection +- `sprite/claude.sh` - Already uses library functions (no changes needed) +- `README.md` - Documents non-interactive usage + +## Future Improvements + +Potential enhancements: +- [ ] Add `--non-interactive` flag for explicit mode control +- [ ] Support reading from config files (e.g., `.spawnrc`) +- [ ] Add `--help` flag to show all environment variables +- [ ] Validate environment variables before starting +- [ ] Add dry-run mode to show what would be executed diff --git a/README.md b/README.md index 7ab7536f..0f5dea5d 100644 --- a/README.md +++ b/README.md @@ -11,18 +11,37 @@ Conjure your agents! ## Usage -### Interactive Mode +### Interactive Mode (Recommended) -Run the scripts and provide input when prompted: +Use process substitution for full interactivity: ```bash -# Claude Code -curl https://openrouter.ai/lab/spawn/sprite/claude.sh | bash +# Claude Code - Prompts for sprite name and OAuth +bash <(curl -fsSL https://openrouter.ai/lab/spawn/sprite/claude.sh) + +# OpenClaw - Prompts for sprite name, OAuth, and model selection +bash <(curl -fsSL https://openrouter.ai/lab/spawn/sprite/openclaw.sh) +``` + +**Why `bash <(curl ...)` instead of `curl | bash`?** +- Process substitution keeps stdin available for interactive prompts +- No special TTY handling required +- Works like a normal bash script + +### Alternative: Piping (Requires env vars) + +If using the shorter `curl | bash` pattern (like the OpenRouter documentation shows), you must set environment variables: + +```bash +# Claude Code - As shown on openrouter.ai/lab/spawn +SPRITE_NAME=dev-mk1 curl https://openrouter.ai/lab/spawn/sprite/claude.sh | bash # OpenClaw -curl https://openrouter.ai/lab/spawn/sprite/openclaw.sh | bash +SPRITE_NAME=dev-mk1 curl https://openrouter.ai/lab/spawn/sprite/openclaw.sh | bash ``` +**Note:** The OpenRouter URLs (`openrouter.ai/lab/spawn/...`) may redirect or proxy to this repository. + ### Non-Interactive Mode For automation or CI/CD, set environment variables: