Skip to main content
Status: accepted · ADR-30 · Filed 2026-04-29

Decision

Install authority lives in cli/src/index.ts. No Python install code. No repo-internal shell that contains install business logic. The only shell allowed is a thin consumer bootstrap (one per OS family) at scripts/install.{sh,ps1} whose sole purpose is solving the chicken-and-egg of “Node not installed, repo not cloned.” Quality bar across all three artifacts (scripts/install.sh, scripts/install.ps1, cli/src/index.ts): production-grade — status output, error handling, on-disk log file at ~/.prism/install.log, professional copy. prism update is the canonical refresh verb after first install — same authority, same quality bar; semantically install --refresh.

Consumer install flow — one command per OS

Mac / Linux:
  curl -fsSL https://github.com/FrankTewksbury/Prism/raw/main/scripts/install.sh | bash

Windows (PowerShell):
  irm https://github.com/FrankTewksbury/Prism/raw/main/scripts/install.ps1 | iex
URL form is github.com/<user>/<repo>/raw/<branch>/<path> — visible, recognizable github.com domain (no raw.githubusercontent.com surprise). GitHub redirects to raw content automatically.

scripts/install.{sh,ps1} responsibilities (and only these)

  1. Detect Node; install via OS-native package manager (brew on Mac, winget on Windows, apt/dnf/native on Linux). Unrecognized OS → clear error pointing at https://nodejs.org and the log file.
  2. Prompt for Projects root with OS-convention defaults (Mac/Linux: ~/Projects/, Windows: %USERPROFILE%\Projects\). User can override to anywhere. The repo subdirectory is always named Prism (not customizable).
  3. git clone into <projects-root>/Prism.
  4. cd into it; exec npm install --prefix cli (the existing prepare hook builds via tsc); then node cli/dist/index.js install — handing off to the Node CLI.
Quality bar: status at every step; every external command checked, failure prints command + stderr + non-zero exit; ~/.prism/install.log with timestamps + full command output (append, not overwrite — preserve history across re-runs); idempotent (Node already installed → skip; repo already cloned → skip); calm professional copy (no emoji chatter, no welcome wizards, no jokes).

prism install (Node CLI) responsibilities — what the consumer never types directly

  • Detect missing system prereqs (uv, python 3.11, docker, docker-compose, gh).
  • Single consent prompt: “Install missing prereqs? [Y/n]” — then drive brew/winget/apt from Node via child_process. Never ask the user to run a second command.
  • Configuration prompts (backend mode: local/lan/cloud, API URL, API key).
  • Write env vars + JSON/TOML configs into every detected editor (Claude Desktop, Claude Code, Cursor, Codex).
  • Wire ~/.claude/statusline-command.sh$PRISM_ROOT/bin/statusline-claude-code.sh (symlink); JSON-merge ~/.claude/settings.json’s statusLine.command to point at the resolved bash path; preserve other top-level keys; refreshInterval=1.
  • Install launchers; register coder AND prism on PATH (managed ~/.zshrc/~/.bashrc block on Mac/Linux; [Environment]::SetEnvironmentVariable('Path', ..., 'User') on Windows — same registry mechanism already in cli/src/index.ts).
  • mkdir -p ~/.prism (cache dir, defensive).
  • Run smoke.
  • Same quality bar as bootstrap: status, error handling, append to ~/.prism/install.log, professional copy.

prism update — lifecycle refresh verb

After first install, prism is globally on PATH. Operator runs prism update from anywhere:
  • git pull --ff-only in $PRISM_ROOT (refuses on conflict; surfaces clear path forward).
  • Re-run editor MCP config writes (idempotent — writes only on drift).
  • Refresh coder/prism PATH entries if $PRISM_ROOT moved.
  • Re-resolve ~/.claude/settings.json’s statusLine.command path.
  • docker compose pull for in-scope stack profiles — refresh container images so backend/dashboard/SM updates land.
  • Detect missing-or-stale system prereqs introduced by recent commits and prompt-with-consent to install.
  • Run install-smoke.
  • Append to ~/.prism/install.log.
  • Report what changed: editor configs rewritten, images pulled, PATH entries refreshed, smokes passed.
prism install and prism update are the same code paths under the hood — verb names distinguish intent (first install vs ongoing maintenance) for operator clarity, not for code-path divergence. Idempotent: re-running prism update produces no second-time changes.

Known limitation surfaced post-install

Windows operators using pure PowerShell (no Git Bash) can’t execute bin/statusline-claude-code.sh. prism install/prism update post-run output flags this as a one-line note until a PowerShell port lands.

Rationale

Why this ADR exists at all. On 2026-04-15 commit 0590bb4 (“Port installer to TypeScript npm CLI”) deleted install.py with the explicit message “single source of truth for the CLI.” Three days later, commit d86f9ea (“Plan #5: prism_clone/create/destroy/migrate + two-stage install”) re-introduced install/*.py and demoted cli/ to “deprecated in README.” The reversal was framed as a forward redesign, not flagged as a mandate reversal. Between 2026-04-18 and 2026-04-28 four feature commits landed on the Python path while the Node path got parallel features. Two installers, ten days of drift. Cost surfaced via Candi’s Windows upgrade-path failure 2026-04-28: the upgrade-scenario gap was structurally unfixable without first reconciling which installer was canonical. Why an ADR not a memory or doc. Memory loads only for the agent whose memory it is — the d86f9ea author had no signal to check the prior commit’s mandate. Docs rot and get ignored. ADRs are checked-in, surfaced by semantic_recall / prism_decide, and carry “we already decided this” weight. A future agent solving “two-stage install” or “first-install bootstrap” hits this ADR before reaching for Python or repo-internal shell. Why one shell bootstrap is allowed when the broader rule forbids shell. The failure mode d86f9ea introduced was repo-internal shell that contained install business logic and quietly took over the canonical path. A hosted bootstrap with zero business logic is a different animal — it’s a reachability primitive (chicken-and-egg solver), not an installer. The sharp distinction: any line of install business logic in scripts/install.{sh,ps1} is a regression to be reverted. Why consumer-grade UX is product-imperative, not nice-to-have. Prism is for users we don’t have machine access to. Friction in install is a conversion-killer. The minimum is one command per OS — same pattern Homebrew, Rust, Bun, Deno, uv, pnpm all use because it works. Anything more (multiple commands, manual prereq installs, “now run X then Y”) fails the consumer mandate. Why prism update is a first-class verb. Operators need a single-command refresh path that handles git pull + editor configs + PATH + statusline + container images + smoke. Without it, “upgrade” devolves into a multi-step manual sequence each time, which is exactly what the consumer mandate forbids.

Alternatives Considered

Rejected: keep Python install/install.py as Stage 2 under bash bootstrap. This is the current state. Cost: two parallel installers, perpetual drift, install.py keeps accreting features that have to be mirrored in the Node CLI manually. The Candi failure mode repeats every few weeks until cutover. Rejected: keep a repo-internal bin/install.sh that wraps git pull + npm + prism install. Looks innocuous; recreates the failure mode. Once that file exists, future contributors will reach for it as a “convenient place to put just one more thing,” and install business logic will accrete there. The ADR forbids this category. Rejected: distribute as a self-contained npm package (npm install -g @prism/cli). Doesn’t solve the chicken-and-egg — operator still needs Node installed first, plus npm-global has known cross-platform PATH issues on Windows. The hosted bootstrap pattern is strictly more accessible. Rejected: ship a Windows .msi / Mac .pkg / Linux .deb. Highest UX polish, but requires a build pipeline + signing infrastructure + per-OS testing matrix that doesn’t exist today. Right answer eventually; wrong answer for the cutover. The curl|sh / irm|iex pattern reaches the same ergonomic floor with zero pipeline overhead. Rejected: separate prism upgrade and prism update verbs. Different names suggest different semantics; same code path under the hood means operators just guess which one to type. One verb (prism update) following the brew/apt idiom (update = refresh local artifacts) is cleaner.
Last modified on May 3, 2026