| 1 |
# Shell |
| 2 |
|
| 3 |
Nushell as the login shell, bash unchanged as `/bin/sh` and `/bin/bash`. Companion to [STACK.md](STACK.md) and [CONSOLE.md](CONSOLE.md). |
| 4 |
|
| 5 |
## Thesis |
| 6 |
|
| 7 |
Alloy's audience writes their own scripts and reaches for the shell as a tool-glue surface, not just an interactive prompt. That reframes the shell pick away from "safe universal default" and toward "which shell makes daily tool-glue actually good." Nushell wins that on structural grounds (structured pipelines with typed values instead of stringly-typed byte streams), and every daily interaction compounds the advantage. |
| 8 |
|
| 9 |
The pick is coherent with the rest of Alloy: `alloy console` treats configs as typed data, `alloy_tui` treats widgets as functions over state, and now the shell treats commands as functions over typed pipelines. Same principle at three layers. |
| 10 |
|
| 11 |
## Architecture: nu is login-only |
| 12 |
|
| 13 |
The pick that keeps the ecosystem tax bounded: |
| 14 |
|
| 15 |
- **Login shell:** `nu`. What the user sees when they open shop, when they SSH in, when they open a scratch pane. |
| 16 |
- **`/bin/sh`:** bash, unchanged. Every `curl | sh` invokes this. Untouched. |
| 17 |
- **`/bin/bash`:** bash, unchanged. Every `#!/bin/bash` script runs here. Untouched. |
| 18 |
- **`/etc/shells`:** contains both nu and bash. `chsh -s /bin/bash` reverts a user to bash cleanly and reversibly. |
| 19 |
|
| 20 |
**Consequence: `curl -fsSL https://example.com/install.sh | sh` works identically to any other distro.** The login shell is not in the invocation path. This was the biggest concern about picking a non-POSIX shell, and it turns out to be a misread of how `curl | sh` actually resolves. |
| 21 |
|
| 22 |
**Consequence: every `#!/bin/bash` or `#!/usr/bin/env bash` script the user writes or downloads runs in bash.** Their own tool-glue scripts can be nu (`#!/usr/bin/env nu`), which is where the structured-pipeline advantage lives, but nothing forces them to. |
| 23 |
|
| 24 |
## What Alloy ships |
| 25 |
|
| 26 |
`etc/skel/.config/nushell/` in the Alloy repo: |
| 27 |
|
| 28 |
- `config.nu`: main nu configuration. Table style, history (sqlite backend, shared live across panes rather than isolated per session, because several terminals at once is the normal case on a tiling desktop), fuzzy completions, Alloy color palette applied to nu's own output. Tool integrations are deliberately *not* here: `source` is a parse-time keyword in nu, so it cannot be gated behind a runtime check without failing the parse and costing the whole file. The image writes them to `/usr/share/nushell/vendor/autoload/` instead, which nushell reads before `config.nu` runs. |
| 29 |
- `env.nu`: environment. `EDITOR=hx`, `VISUAL=hx`, PATH additions for `~/.local/bin` and `~/.cargo/bin`, cache directory prep for integration inits. |
| 30 |
- `aliases.nu`: a small curated set (`ll`, `la`, `..`, `...`, git shortcuts). No plugin framework. |
| 31 |
|
| 32 |
The Alloy image installs these into `~/.config/nushell/` on first boot (or symlinks; packaging decision at v0). |
| 33 |
|
| 34 |
## Interoperability |
| 35 |
|
| 36 |
Where daily-use friction shows up, and how Alloy handles it: |
| 37 |
|
| 38 |
- **Tool init lines.** `eval "$(starship init bash)"` and equivalents. Starship, zoxide, atuin, and carapace all emit a nu init directly, and the image generates those at build time into `/usr/share/nushell/vendor/autoload/`. **direnv does not.** As of 2.35.0 its `hook` and `export` targets are bash, zsh, fish, elvish, tcsh, murex, pwsh, vim, systemd, gha, json, and gzenv, with no nu among them. Alloy therefore writes that one by hand, against `direnv export json`, and ships it into the same autoload directory: [`usr/share/nushell/vendor/autoload/direnv.nu`](../usr/share/nushell/vendor/autoload/direnv.nu). Generated or authored, every integration lands in one place. The rest of the long tail is handled the same way, added on demand rather than preemptively. |
| 39 |
|
| 40 |
Two things that directory's ordering settles. Nushell reads it *after* `config.nu`, which is what lets an autoload file register a hook: `config.nu` assigns `$env.config` wholesale and would discard anything registered earlier. And `$env.PATH` is a list in nu while direnv reports PATH colon-joined, so the hook converts that one variable rather than loading the diff verbatim. |
| 41 |
- **Interactive one-liners.** `FOO=bar cmd && cmd2` becomes `FOO=bar; cmd; cmd2` (or `try / catch` for the conditional flavor). Muscle-memory tax; not a real barrier. |
| 42 |
- **`export`.** `$env.FOO = "bar"` in nu, `export FOO=bar` in bash. Both work in their respective contexts. |
| 43 |
- **Piped stdin.** Third-party CLIs that expect line-oriented text stdin: nu's default output is a table, but piping to an external command auto-flattens to text. Explicit control via `| to text` when needed. |
| 44 |
- **Command-substitution.** `` `foo` `` becomes `(foo)` in nu. Small learn. |
| 45 |
|
| 46 |
## Accepted costs |
| 47 |
|
| 48 |
- **Pre-1.0 velocity.** Nushell is currently in the 0.100.x range. Breaking changes across releases have slowed dramatically since 0.75+ (era of "every release rewrites your config" is over), but a couple of key renames per year still happen. Alloy accepts drift maintenance for `config.nu` the same way it does for `userChrome.css`. |
| 49 |
- **Documentation tax.** Google results for "how do I X on the command line" assume bash. Fish translations are usually a nearby SO answer; nu translations often require a trip to nushell.sh's book. Real cost, absorbed by the "audience that reads docs deliberately" targeting. |
| 50 |
- **Command name collisions.** Nu's built-in `ls`, `cd`, `cp`, `ps` behave differently from GNU equivalents (structured output, different flags). Users learn the mapping. Strictly nicer for interactive use once learned. |
| 51 |
- **Ecosystem depth.** Fewer third-party CLIs ship `--init nu` than `--init fish` or `--init zsh`. Alloy backfills as needed via `etc/skel/.config/nushell/integrations/`. |
| 52 |
|
| 53 |
## Prompt |
| 54 |
|
| 55 |
**starship** (Rust, cross-shell). Boring, works, ships `starship init nu` first-class. Alloy configures starship's own palette to match the tokens. |
| 56 |
|
| 57 |
A custom nu prompt as a small design-system exercise is a legitimate future project (nushell's prompt hook is rich enough to author an Alloy prompt directly), but not v0 scope. Deferred. |
| 58 |
|
| 59 |
## Rejected |
| 60 |
|
| 61 |
- **fish.** Excellent shell, safer pick. Rejected because the audience is script-writers, not paste-and-forget users, so nu's structural advantages earn their cost. If nu turns out to be the wrong call in practice, fish is the fallback and the switch is small (login-shell only, `chsh -s /usr/bin/fish`). |
| 62 |
- **zsh.** Mostly-POSIX and powerful, but raw zsh is unpleasant and requires a config framework (oh-my-zsh, zinit) to be tolerable. Adopting a framework means importing someone else's opinions, which Alloy avoids everywhere else. |
| 63 |
- **bash.** No opinion, works. Best UX-out-of-box is worst-in-class. Alloy is opinionated everywhere; being un-opinionated at the shell layer specifically feels like leaving value on the table. |
| 64 |
- **elvish, xonsh, oil.** Interesting, small communities. Adopting them would put Alloy in a "maintaining half the ecosystem" position. |
| 65 |
|
| 66 |
## Open questions |
| 67 |
|
| 68 |
- [x] **Root login shell: bash.** Recovery is the wrong time to hit a shell surprise. Nu is in `/etc/shells` and reachable via `chsh -s /usr/bin/nu` for users who want consistency. |
| 69 |
- [x] **History file backend: sqlite.** Better interactive search; atuin (if adopted later) reads sqlite natively, so no migration on the future path. |
| 70 |
- [x] **atuin default: defer.** Encrypted history sync is a real feature but sits adjacent to CONTINUITY.md's Tailscale/Syncthing story and wants its own design pass. Users install atuin themselves in the meantime. |
| 71 |
|