Non-interactive install story GitHub issue

vibecode
{"vibecode": {
    "doc": "non_interactive_install_story",
    "role": "narrative walkthrough — Caspian install in environments without a TTY: CI runners, Docker builds, cron, systemd, ssh -T",
    "audience": "automation engineer setting up CI / Dockerfile / provisioning script",
    "detection": "wrapper attempts to open /dev/tty; if it fails, runs install.casp with --non-interactive",
    "override_mechanism": "environment variables",
    "defaults_when_non_interactive":
        {"install_location": "$HOME/caspian (or $CASPIAN_INSTALL_PREFIX)",
        "scope": "per_user",
        "examples": "part of install tree at $HOME/caspian/examples",
        "shell_rc": "updated if writable and CASPIAN_SKIP_RC_EDIT not set"},
    "status": "brainstorm — describes what V1.0 install should feel like"
}}

Some installs don't have a user to prompt: CI pipelines, Docker image builds, cron jobs, systemd-managed setup scripts, ssh -T host curl ... | sh. In all of these, /dev/tty isn't openable. The Caspian installer detects this and falls back to a silent default install — same code path as the interactive flow, just with every prompt answered with its default.

This story walks through what that looks like in three common environments.

For the interactive shell-user story, see linux.md.


How the installer detects non-interactive GitHub issue

The shell wrapper that curl | sh invokes attempts to open /dev/tty:

sh
if exec 3</dev/tty 2>/dev/null; then
    # Interactive — hand off to the Caspian installer normally
    exec "$tmpdir/bin/caspian" "$tmpdir/install.casp"
else
    # No TTY — pass --non-interactive so install.casp skips prompts
    exec "$tmpdir/bin/caspian" "$tmpdir/install.casp" --non-interactive
fi

The Caspian installer (install.casp) checks for the --non-interactive flag at startup. If present, it skips every prompt and uses the corresponding default value. Output is one line per significant step, no decorative chrome.


Defaults applied silently GitHub issue

Decision Default Override
Install location $HOME/caspian CASPIAN_INSTALL_PREFIX=/path
Scope per-user CASPIAN_INSTALL_SYSTEM=1 (requires sudo / root)
Edit shell rc yes if rc file exists and is writable CASPIAN_SKIP_RC_EDIT=1

Examples are part of the install tree ($HOME/caspian/examples/) and don't need a separate copy step — they live alongside the engine and are user-editable by virtue of being in the user's home.

Every override is an environment variable, settable before the curl call. No flags need to thread through the | sh pipe.


Scenario: GitHub Actions GitHub issue

A workflow that needs Caspian available before a test step:

yaml
- name: Install Caspian
  run: curl -fsSL https://puck.uno/install | sh

- name: Run tests
  run: caspian tests/run.casp

What happens:

yaml
- name: Install Caspian
  run: |
    curl -fsSL https://puck.uno/install | sh
    echo "$HOME/caspian/bin" >> $GITHUB_PATH

($GITHUB_PATH is GitHub Actions' way of making a PATH addition persist across steps.)

Output in the runner log:

caspian-install: detected non-interactive (no /dev/tty)
caspian-install: target = /home/runner/.caspian
caspian-install: extracted bundle (847 KB)
caspian-install: shell rc updated: /home/runner/.bashrc
caspian-install: done

Scenario: Docker image GitHub issue

A Dockerfile that bakes Caspian into a base image:

dockerfile
FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
        curl ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# Install Caspian system-wide so every user in the container can use it.
RUN curl -fsSL https://puck.uno/install \
    | CASPIAN_INSTALL_PREFIX=/usr/local sh

CMD ["caspian", "--version"]

What happens:

Resulting image size adds ~850 KB on top of the base, no other modifications. Caspian fits comfortably in even minimal base images without bloating the layer.


Scenario: Cloud-init / Ansible / shell provisioning GitHub issue

A provisioning script for a fresh VM:

bash
#!/bin/sh
# bootstrap.sh — run on a fresh VM via cloud-init, Ansible, etc.

set -e

# Caspian, system-wide, no shell rc edits (server has no real users).
export CASPIAN_INSTALL_PREFIX=/usr/local
export CASPIAN_SKIP_RC_EDIT=1
curl -fsSL https://puck.uno/install | sh

# Verify
caspian --version

What happens:


Error handling in non-interactive mode GitHub issue

When prompts can't help recover from a problem, the installer has to fail cleanly. Concrete cases:

Failure Exit code What's printed
Download failure (curl error, 404) 1 "could not download bundle: "
Checksum mismatch 1 "bundle checksum mismatch: refusing to install"
Target dir already exists, non-empty 1 "directory already exists: (set CASPIAN_OVERWRITE=1 to replace)"
Permission denied (system install without sudo) 1 "permission denied at : re-run with sudo or set CASPIANINSTALLPREFIX to a writable location"
Unsupported platform 1 "no pre-built bundle for <uname-output>: see from-source docs"

Anything that interactive-mode would have asked the user about becomes a hard failure with a clear error message and an env-var hint for how to retry.


One installer, two paths GitHub issue

The interactive flow and the non-interactive flow share one install.casp. The only difference is whether --non-interactive was passed. Inside the script:

caspian
if %argv.includes?('--non-interactive')
    $location = %env['CASPIAN_INSTALL_PREFIX'] || '~/caspian'
else
    $location = prompt('Install location', default: '~/caspian')
end

…and the same pattern for each decision point. Defaults are identical in both paths. The interactive flow shows the defaults in prompts and lets the user override; the non-interactive flow applies them silently and lets env vars override. Same destinations, same files, same shell rc behavior.

This keeps maintenance honest: any new install option added to the interactive flow gets its corresponding env var defined in the non-interactive flow at the same time.


Open questions GitHub issue

vibecode
{"vibecode": {"open_questions":
["whether_to_emit_machine_readable_json_output_when_non_interactive",
"how_loud_or_quiet_default_non_interactive_output_should_be",
"opt_in_telemetry_about_install_environment_or_strictly_no_phone_home",
"behavior_when_curl_pipe_to_sudo_sh_with_user_owned_home_dir"]}}

© 2026 Puck.uno