Status:
accepted · ADR-30 · Filed 2026-04-29Decision
Install authority lives incli/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
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)
- Detect Node; install via OS-native package manager (
brewon Mac,wingeton Windows,apt/dnf/native on Linux). Unrecognized OS → clear error pointing at https://nodejs.org and the log file. - Prompt for Projects root with OS-convention defaults (Mac/Linux:
~/Projects/, Windows:%USERPROFILE%\Projects\). User can override to anywhere. The repo subdirectory is always namedPrism(not customizable). git cloneinto<projects-root>/Prism.cdinto it; execnpm install --prefix cli(the existingpreparehook builds viatsc); thennode cli/dist/index.js install— handing off to the Node CLI.
~/.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/aptfrom Node viachild_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’sstatusLine.commandto point at the resolved bash path; preserve other top-level keys;refreshInterval=1. - Install launchers; register
coderANDprismon PATH (managed~/.zshrc/~/.bashrcblock 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-onlyin$PRISM_ROOT(refuses on conflict; surfaces clear path forward).- Re-run editor MCP config writes (idempotent — writes only on drift).
- Refresh
coder/prismPATH entries if$PRISM_ROOTmoved. - Re-resolve
~/.claude/settings.json’sstatusLine.commandpath. docker compose pullfor 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 executebin/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 commit0590bb4 (“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 Pythoninstall/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.
