Skip to main content

max / alloy

7.3 KB · 71 lines History Blame Raw
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