Part of #72. Depends on #76 (M2, the selector and mackas projects) and #77 (M3, pinned configs). Natural home alongside #79 (M5), which already owns the generated env files.
Why
During the transition there is a window where some projects are pinned and the old flat work/ layout is still in use. In that window . ~/oe/env.sh is the one command that says "I am about to build", and it is exactly when the tool knows least: cwd may be ~, or a legacy checkout, or a workspace whose pin was created months ago and forgotten. Today it resolves silently or not at all, and the first sign of a wrong answer is a cold build against volumes the user did not expect.
This adds an interactive selector when derivation cannot resolve a project, and a clear error when it cannot even offer a choice.
What ships
When env.sh is sourced:
MACKAS_PROJECT_SELECT already set → do nothing. An explicit value always wins, following the same only-if-unset discipline _mackas_derive_project() already uses.
- No pinned configs at all → silent, byte-identical to today. See "Not an error" below.
- cwd resolves to exactly one pinned workspace → select it, print the one-line note.
- Pins exist, cwd matched none (or matched several), shell is interactive → numbered selector, listing each pinned project with its three resolved volume names. Empty input selects nothing and stays on the legacy path.
- Same, but non-interactive or opted out → bold red message naming what was found and what to do, then
return 1. Never prompt, never block.
Re-derivation on cwd change
A selection made at source time is a tier-2 export, and tier 2 beats cwd derivation at tier 3. Left alone, a choice made once would silently outrank the workspace the user is standing in an hour later — the "stale correct guess" risk #72 names, made stickier by being an explicit export.
Decision: when cwd moves into a different pinned workspace, the selection is re-derived.
This requires distinguishing a value we set from one the user set, or re-derivation would trample an explicit choice:
- When the selector (or cwd derivation) sets
MACKAS_PROJECT_SELECT, it also records _MACKAS_PROJECT_PROVISIONAL=<name>.
- Re-derivation acts only when
MACKAS_PROJECT_SELECT is non-empty and equals _MACKAS_PROJECT_PROVISIONAL. If the user set the variable by hand — marker absent, or values diverged — it is never touched.
- Moving into a different pinned workspace updates both, and prints a one-line note naming the new project and its volumes. Moving somewhere unpinned changes nothing: the current selection persists rather than being cleared, because "I cd'd to
~ for a moment" must not silently drop the build environment.
Mechanism: re-derive at use, not on every cd. The kas-container() shell function env.sh already defines runs on every hand-typed invocation and can re-derive immediately before dispatch. This costs nothing when idle, needs no shell hooks, and behaves identically under bash and zsh. A chpwd/PROMPT_COMMAND hook is a possible later addition purely to surface the note at cd time, and should not be a dependency — hooks are per-shell, intrusive, and differ between bash and zsh.
Constraints that shape the implementation
env.sh is sourced, and by zsh on a stock Mac (see the generator comment in mackas). No exit, no die — either kills the user's terminal; only return. No bashisms, and specifically not select, whose prompt behaviour differs between bash 3.2 and zsh. A numbered list plus read is portable.
- Never block a non-interactive shell.
README.md tells people to source this in every shell, so it lands in .zshrc: a prompt would fire on every new terminal, tmux pane and split, and would hang outright where there is no TTY (git hooks, scp, cron). Gate on case $- in *i* and [ -t 0 ], plus a MACKAS_ENV_NO_PROMPT=1 opt-out.
- Colour reuses the existing gate (
NO_COLOR, non-tty, TERM=dumb) rather than hardcoding escapes, or red bytes end up in logs and pipes.
- Candidate configs are grepped, never sourced, consistent with
mackas projects.
Not an error: no pinned projects
The red message fires only when pinned configs exist and none matched. With zero pins there is nothing to select and nothing is wrong — that user is on the fully supported "do nothing" path, and erroring would put a red error in every shell of every existing user for correctly using the tool. Zero pins stays silent and byte-identical to today.
Sketch
_mackas_select_project() {
[ -n "${MACKAS_PROJECT_SELECT:-}" ] && return 0 # explicit always wins
local d="$HOME/.config/mackas/projects" pins
pins=$(ls "$d"/*.conf 2>/dev/null) || return 0 # no pins: silent, legacy
[ -n "$pins" ] || return 0
_mackas_derive_from_pwd && return 0 # cwd resolved it
case $- in *i*) ;; *) _mackas_env_warn; return 1 ;; esac
{ [ -t 0 ] && [ -t 2 ]; } || { _mackas_env_warn; return 1; }
[ "${MACKAS_ENV_NO_PROMPT:-0}" = 1 ] && { _mackas_env_warn; return 1; }
# numbered list + read; empty input selects nothing
}
Tests
- Zero pinned configs: sourcing is byte-identical to today, no prompt, no output, no non-zero return. Mutation-test this — it is the assertion protecting every existing user.
- Pins exist, cwd inside one: selected without prompting, note printed once.
- Pins exist, cwd matches none, non-interactive: no prompt, message on stderr,
return 1, shell survives (assert the shell is still alive afterwards — a stray exit is the failure mode).
MACKAS_ENV_NO_PROMPT=1 suppresses the prompt in an interactive shell.
- A pre-set
MACKAS_PROJECT_SELECT survives sourcing untouched.
- Re-derivation: provisional value updates on moving into a different pinned workspace; a hand-set value does not; moving somewhere unpinned leaves the selection intact.
- Sourced under both bash 3.2 and zsh.
- With
NO_COLOR=1 and with stderr redirected to a file, no escape sequences appear.
Size: M. Risk: medium — it runs in the user's own interactive shell, where the failure modes are hangs and killed terminals rather than a bad build.
Part of #72. Depends on #76 (M2, the selector and
mackas projects) and #77 (M3, pinned configs). Natural home alongside #79 (M5), which already owns the generated env files.Why
During the transition there is a window where some projects are pinned and the old flat
work/layout is still in use. In that window. ~/oe/env.shis the one command that says "I am about to build", and it is exactly when the tool knows least: cwd may be~, or a legacy checkout, or a workspace whose pin was created months ago and forgotten. Today it resolves silently or not at all, and the first sign of a wrong answer is a cold build against volumes the user did not expect.This adds an interactive selector when derivation cannot resolve a project, and a clear error when it cannot even offer a choice.
What ships
When
env.shis sourced:MACKAS_PROJECT_SELECTalready set → do nothing. An explicit value always wins, following the same only-if-unset discipline_mackas_derive_project()already uses.return 1. Never prompt, never block.Re-derivation on cwd change
A selection made at source time is a tier-2 export, and tier 2 beats cwd derivation at tier 3. Left alone, a choice made once would silently outrank the workspace the user is standing in an hour later — the "stale correct guess" risk #72 names, made stickier by being an explicit export.
Decision: when cwd moves into a different pinned workspace, the selection is re-derived.
This requires distinguishing a value we set from one the user set, or re-derivation would trample an explicit choice:
MACKAS_PROJECT_SELECT, it also records_MACKAS_PROJECT_PROVISIONAL=<name>.MACKAS_PROJECT_SELECTis non-empty and equals_MACKAS_PROJECT_PROVISIONAL. If the user set the variable by hand — marker absent, or values diverged — it is never touched.~for a moment" must not silently drop the build environment.Mechanism: re-derive at use, not on every
cd. Thekas-container()shell functionenv.shalready defines runs on every hand-typed invocation and can re-derive immediately before dispatch. This costs nothing when idle, needs no shell hooks, and behaves identically under bash and zsh. Achpwd/PROMPT_COMMANDhook is a possible later addition purely to surface the note atcdtime, and should not be a dependency — hooks are per-shell, intrusive, and differ between bash and zsh.Constraints that shape the implementation
env.shis sourced, and by zsh on a stock Mac (see the generator comment inmackas). Noexit, nodie— either kills the user's terminal; onlyreturn. No bashisms, and specifically notselect, whose prompt behaviour differs between bash 3.2 and zsh. A numbered list plusreadis portable.README.mdtells people to source this in every shell, so it lands in.zshrc: a prompt would fire on every new terminal, tmux pane and split, and would hang outright where there is no TTY (git hooks,scp, cron). Gate oncase $- in *i*and[ -t 0 ], plus aMACKAS_ENV_NO_PROMPT=1opt-out.NO_COLOR, non-tty,TERM=dumb) rather than hardcoding escapes, or red bytes end up in logs and pipes.mackas projects.Not an error: no pinned projects
The red message fires only when pinned configs exist and none matched. With zero pins there is nothing to select and nothing is wrong — that user is on the fully supported "do nothing" path, and erroring would put a red error in every shell of every existing user for correctly using the tool. Zero pins stays silent and byte-identical to today.
Sketch
Tests
return 1, shell survives (assert the shell is still alive afterwards — a strayexitis the failure mode).MACKAS_ENV_NO_PROMPT=1suppresses the prompt in an interactive shell.MACKAS_PROJECT_SELECTsurvives sourcing untouched.NO_COLOR=1and with stderr redirected to a file, no escape sequences appear.Size: M. Risk: medium — it runs in the user's own interactive shell, where the failure modes are hangs and killed terminals rather than a bad build.