From 6310cbb7e67ddca8d1f82c3caf4b32e0e72007be Mon Sep 17 00:00:00 2001 From: jiminu Date: Mon, 14 Sep 2026 22:15:05 +0900 Subject: [PATCH 1/3] refactor: manage developer CLI tools with mise --- config/shared/mise.toml | 7 ++++++- config/shared/zsh/interactive.zsh | 6 ++---- docs/PROFILES.md | 15 ++++++++++----- profiles/developer.conf | 21 ++++++--------------- scripts/update-dependencies.sh | 17 ++++++++++++----- tests/dependency_updates_test.bash | 24 ++++++++++++++++++++++++ tests/profiles_test.bash | 25 +++++++++++++++++++++++-- 7 files changed, 83 insertions(+), 32 deletions(-) diff --git a/config/shared/mise.toml b/config/shared/mise.toml index 53cf4ddc..4a17e681 100644 --- a/config/shared/mise.toml +++ b/config/shared/mise.toml @@ -1,4 +1,10 @@ [tools] +fzf = "0.74.4" +zoxide = "0.10.0" +ripgrep = "15.2.0" +eza = "0.23.5" +bat = "0.26.1" +jq = "1.8.2" node = "24.18.0" python = "3.13.14" neovim = "0.12.5" @@ -9,4 +15,3 @@ gh = "2.100.0" [settings] not_found_auto_install = false - diff --git a/config/shared/zsh/interactive.zsh b/config/shared/zsh/interactive.zsh index ced75351..4049b9d6 100644 --- a/config/shared/zsh/interactive.zsh +++ b/config/shared/zsh/interactive.zsh @@ -81,10 +81,8 @@ unset _selfishell_zoxide_bin if _selfishell_fzf_bin="$(command -v fzf)"; then # Scheme 16 keeps fzf to the terminal's own colors, as the prompt does by - # naming colors. Spelled 16, not base16: that alias postdates the fzf Ubuntu - # 24.04 ships (0.44.1), which rejects an unknown scheme outright and would - # take every invocation down with it. The environment wins, so this is a - # default, not a policy, and it reaches only Ctrl-T and Ctrl-R. + # naming colors. The environment wins, so this is a default, not a policy, + # and it reaches only Ctrl-T and Ctrl-R. export FZF_DEFAULT_OPTS="${FZF_DEFAULT_OPTS:---color=16}" _selfishell_fzf_cache="$SELFISHELL_CACHE_DIR/fzf-init.zsh" diff --git a/docs/PROFILES.md b/docs/PROFILES.md index 53321d6c..a0808ebe 100644 --- a/docs/PROFILES.md +++ b/docs/PROFILES.md @@ -17,9 +17,12 @@ interactive Zsh. Selfishell keeps its defaults in `${XDG_CONFIG_HOME:-$HOME/.config}/selfishell/mise/selfishell.toml` (which is symlinked to `~/.config/mise/conf.d/selfishell.toml` so it is automatically loaded by `mise`); a project's `mise.toml` can select different tool versions. -Built-in mise tools use exact reviewed versions pinned in `config/shared/mise.toml`, -the single source of truth for these versions. Projects remain free to -override them in a local `mise.toml`. Updating these defaults requires a normal +Developer tools managed by mise use exact reviewed versions pinned in +`config/shared/mise.toml`, the single source of truth for these versions. This +includes FZF, Zoxide, Ripgrep, Eza, Bat, jq, Neovim, Tree-sitter CLI, Node.js, +Python, uv, and GitHub CLI on both macOS and Ubuntu. Eza and Bat remain optional; +failure to install either does not stop setup. Projects remain free to override +the defaults in a local `mise.toml`. Updating the defaults requires a normal Selfishell release and never happens during shell startup. Preview without changing the machine: @@ -36,8 +39,10 @@ selfishell install --profile minimal --yes The active profile is recorded in the XDG state directory. `selfishell update` uses that recorded profile to install missing Apt, Homebrew, and directly -managed tools before updating configuration. Apt and Homebrew retain -responsibility for versions of packages they already manage. +managed tools and synchronize mise tools before updating configuration. Apt and +Homebrew retain responsibility for packages still declared through them. +Copies of a tool left from an older profile release are not removed +automatically; after mise activation, its pinned tool version takes precedence. Profile package requirements have two failure policies: diff --git a/profiles/developer.conf b/profiles/developer.conf index 3e267c94..d6fd1a33 100644 --- a/profiles/developer.conf +++ b/profiles/developer.conf @@ -1,18 +1,12 @@ include minimal -package macos required formula fzf -package macos required formula zoxide -package macos required formula ripgrep -package macos optional formula eza -package macos optional formula bat - -package ubuntu required apt fzf -package ubuntu required apt zoxide -package ubuntu required apt ripgrep -package ubuntu optional apt eza -package ubuntu optional apt bat - package all required direct mise +package all required mise fzf +package all required mise zoxide +package all required mise ripgrep +package all optional mise eza +package all optional mise bat +package all required mise jq package all required mise neovim package all required mise tree-sitter package all required mise node @@ -20,7 +14,4 @@ package all required mise python package all required mise uv package all required mise gh -package macos required formula jq - -package ubuntu required apt jq package ubuntu required apt build-essential diff --git a/scripts/update-dependencies.sh b/scripts/update-dependencies.sh index 9304cc99..0bfb1159 100755 --- a/scripts/update-dependencies.sh +++ b/scripts/update-dependencies.sh @@ -107,18 +107,25 @@ discover_metadata() { done local tool repository candidate_tag - for tool in neovim tree-sitter uv gh; do + for tool in fzf zoxide ripgrep eza bat jq neovim tree-sitter uv gh; do repository="$(mise_tool_repository "$tool")" candidate_tag="$(github_latest_tag "$repository")" - printf 'mise-tool %s %s\n' "$tool" "${candidate_tag#v}" >>"$metadata" + candidate_tag="${candidate_tag#v}" + candidate_tag="${candidate_tag#jq-}" + printf 'mise-tool %s %s\n' "$tool" "$candidate_tag" >>"$metadata" done } -# Maps a bumpable mise tool to its upstream repository. node and python are -# deliberately absent -- they stay manual, and discover_metadata() never -# queries anything unlisted. +# Maps a bumpable mise tool to its upstream repository. node and python stay +# manual because their release lines need a policy choice, not a latest tag. mise_tool_repository() { case "$1" in + fzf) printf 'junegunn/fzf\n' ;; + zoxide) printf 'ajeetdsouza/zoxide\n' ;; + ripgrep) printf 'BurntSushi/ripgrep\n' ;; + eza) printf 'eza-community/eza\n' ;; + bat) printf 'sharkdp/bat\n' ;; + jq) printf 'jqlang/jq\n' ;; neovim) printf 'neovim/neovim\n' ;; tree-sitter) printf 'tree-sitter/tree-sitter\n' ;; uv) printf 'astral-sh/uv\n' ;; diff --git a/tests/dependency_updates_test.bash b/tests/dependency_updates_test.bash index 5ec4498d..923348ae 100755 --- a/tests/dependency_updates_test.bash +++ b/tests/dependency_updates_test.bash @@ -41,6 +41,12 @@ write_mise_toml_fixtures() { mkdir -p "$zsh_root/config/shared" cat >"$zsh_root/config/shared/mise.toml" <<'EOF' [tools] +fzf = "0.74.3" +zoxide = "0.9.8" +ripgrep = "15.1.0" +eza = "0.23.4" +bat = "0.26.0" +jq = "1.8.1" node = "24.18.0" python = "3.13.14" neovim = "0.12.4" @@ -325,6 +331,24 @@ test_mise_tool_update_bumps_pin_in_mise_toml_only() { fail "python was modified; this updater must never touch it" } +# Developer CLI tools use the same reviewed pin update path as the existing +# mise-managed runtimes. +test_mise_tool_update_bumps_moved_cli_pin() { + local manifest metadata zsh_root + + manifest="$TEST_ROOT/dependencies.conf" + metadata="$TEST_ROOT/metadata" + zsh_root="$TEST_ROOT/zsh-root" + write_mise_toml_fixtures "$zsh_root" + : >"$manifest" + printf 'mise-tool fzf 0.74.4\n' >"$metadata" + + run_dependency_update "$manifest" "$metadata" "$zsh_root" + + grep -Fqx 'fzf = "0.74.4"' "$zsh_root/config/shared/mise.toml" || + fail "The moved fzf CLI pin was not updated" +} + # uv's real upstream history jumps from a 0.5.x pin to 0.12.x: a naive # lexicographic comparison would treat "0.12.3" as smaller than "0.5.21" # and wrongly skip a legitimate upgrade. diff --git a/tests/profiles_test.bash b/tests/profiles_test.bash index 83ece8bd..7c492b08 100755 --- a/tests/profiles_test.bash +++ b/tests/profiles_test.bash @@ -65,8 +65,8 @@ test_minimal_excludes_developer_environment() { } test_developer_includes_development_tools() { - local output full_output - local expected_mise_tools actual_mise_tools + local output full_output macos_output apt_plan homebrew_plan + local expected_mise_tools actual_mise_tools required_mise_tools optional_mise_tools output="$(run_profile_dry_run developer)" full_output="$(bash "$ROOT_DIR/bin/selfishell" install --profile developer --dry-run)" @@ -80,7 +80,11 @@ test_developer_includes_development_tools() { in_tools && NF >= 3 { print $1 } ' "$ROOT_DIR/config/shared/mise.toml" | sort)" actual_mise_tools="$(printf '%s\n' "$output" | + sed -n 's/^Would sync \(required\|optional\) mise tools: //p' | tr ' ' '\n' | sort)" + required_mise_tools="$(printf '%s\n' "$output" | sed -n 's/^Would sync required mise tools: //p' | tr ' ' '\n' | sort)" + optional_mise_tools="$(printf '%s\n' "$output" | + sed -n 's/^Would sync optional mise tools: //p' | tr ' ' '\n' | sort)" assert_minimal_foundation "$output" [[ "$output" == *'direct package: mise'* ]] || @@ -88,6 +92,23 @@ test_developer_includes_development_tools() { [[ -n "$actual_mise_tools" ]] || fail "Developer profile did not report required mise tools" [[ "$actual_mise_tools" == "$expected_mise_tools" ]] || fail "Developer profile mise tools do not match config/shared/mise.toml (expected: $expected_mise_tools; got: $actual_mise_tools)" + [[ "$required_mise_tools" == $'fzf\ngh\njq\nneovim\nnode\npython\nripgrep\ntree-sitter\nuv\nzoxide' ]] || + fail "Developer profile required mise tools are incorrect: $required_mise_tools" + [[ "$optional_mise_tools" == $'bat\neza' ]] || + fail "Developer profile optional mise tools are incorrect: $optional_mise_tools" + apt_plan="$(printf '%s\n' "$output" | grep 'apt packages:' || true)" + for tool in fzf zoxide ripgrep eza bat jq; do + ! grep -Eq "(^|[[:space:]])$tool([[:space:]]|$)" <<<"$apt_plan" || + fail "Developer CLI tool remained in the Apt install plan: $tool" + done + + export SELFISHELL_TEST_SYSTEM_NAME=Darwin + macos_output="$(run_profile_dry_run developer)" + homebrew_plan="$(printf '%s\n' "$macos_output" | grep 'Homebrew formula' || true)" + for tool in fzf zoxide ripgrep eza bat jq; do + ! grep -Eq "(^|[[:space:]])$tool([[:space:]]|$)" <<<"$homebrew_plan" || + fail "Developer CLI tool remained in the Homebrew install plan: $tool" + done [[ "$full_output" == *'Neovim plugins'* ]] || fail "Developer profile is missing Neovim plugin setup" } From e6e7369f9c1bcb3fd668ead955afde817ba44da1 Mon Sep 17 00:00:00 2001 From: jiminu Date: Mon, 14 Sep 2026 23:05:22 +0900 Subject: [PATCH 2/3] refactor: unify setup and manage Starship with mise --- .github/workflows/ci.yml | 4 +- AGENTS.md | 25 +- README.md | 26 +- bin/selfishell | 4 +- config/shared/mise.toml | 2 +- config/shared/nvim/lua/plugins/ui.lua | 2 +- config/shared/zsh/aliases.zsh | 2 +- dependencies.conf | 4 - docs/COMPANY.md | 4 +- docs/{PROFILES.md => ENVIRONMENT.md} | 46 ++-- docs/INSTALLATION.md | 11 +- docs/PERFORMANCE.md | 10 +- docs/TROUBLESHOOTING.md | 2 +- docs/UPDATES.md | 16 +- install.sh | 14 +- lib/commands/doctor.sh | 38 ++- lib/commands/install.sh | 55 ++-- lib/commands/status.sh | 21 +- lib/commands/uninstall.sh | 2 +- lib/commands/update.sh | 18 +- lib/installers.sh | 4 +- lib/package_manifest.sh | 89 +++++++ lib/package_scan.sh | 21 ++ lib/packages.sh | 32 +-- lib/platform.sh | 4 +- lib/profile_scan.sh | 22 -- lib/profiles.sh | 116 -------- lib/tool_status.sh | 2 +- packages.conf | 29 ++ profiles/developer.conf | 17 -- profiles/minimal.conf | 14 - scripts/benchmark.sh | 69 +++-- scripts/build-release.sh | 2 +- scripts/macos-configuration-e2e.sh | 12 +- scripts/neovim-e2e.sh | 2 +- scripts/ubuntu-container-e2e.sh | 9 +- scripts/update-dependencies.sh | 22 +- tests/cli_test.bash | 12 +- tests/common_zsh_test.bash | 4 +- tests/dependency_updates_test.bash | 16 +- tests/installers_test.bash | 2 +- tests/lifecycle_e2e_test.bash | 4 +- tests/managed_install_test.bash | 252 ++++++++++-------- ...s_test.bash => package_manifest_test.bash} | 105 +++----- tests/release_bootstrap_test.bash | 6 +- tests/tool_status_test.bash | 2 +- tests/updates_test.bash | 10 +- 47 files changed, 550 insertions(+), 635 deletions(-) rename docs/{PROFILES.md => ENVIRONMENT.md} (63%) create mode 100644 lib/package_manifest.sh create mode 100644 lib/package_scan.sh delete mode 100644 lib/profile_scan.sh delete mode 100644 lib/profiles.sh create mode 100644 packages.conf delete mode 100644 profiles/developer.conf delete mode 100644 profiles/minimal.conf rename tests/{profiles_test.bash => package_manifest_test.bash} (51%) mode change 100755 => 100644 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e9d8f91..10746891 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,9 @@ jobs: needs: changes if: needs.changes.outputs.ubuntu_container_e2e == 'true' runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 25 + env: + GITHUB_TOKEN: ${{ github.token }} container: image: ubuntu:24.04 diff --git a/AGENTS.md b/AGENTS.md index 0ee62035..347fae18 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ and Ubuntu on WSL. Installation must be simple, maintenance predictable, and the user experience consistent across supported platforms. - The immutable `v` Git tag is the release version source of truth. -- `profiles/*.conf` defines built-in package profiles. +- `packages.conf` defines the environment's package membership. - `dependencies.conf` pins direct downloads and Git dependencies. - `config/shared/mise.toml` pins mise-managed developer tools. - `docs/RELEASING.md` is the release procedure. @@ -26,7 +26,7 @@ Supported commands are `help`, `version`, `doctor`, `install`, `status`, `update`, `rollback`, and `uninstall`. Keep their responsibilities narrow: - the bootstrap installs only the CLI unless `--setup` is explicit; -- `selfishell install` explicitly installs a profile and configuration; +- `selfishell install` explicitly installs the development environment and configuration; - `update --cli-only` and `update --tools-only` keep release and environment updates separable; - rollback uses a retained release without downloading it again; @@ -88,8 +88,8 @@ Preserve these lifecycle invariants: macOS Bash 3.2 unless the product explicitly installs another interpreter. - Keep Homebrew and Apt operations in `lib/package_managers/`; do not scatter platform branches through command implementations. -- Keep profile files declarative: only supported `include` and `package` - records, never executable shell code. +- Keep `packages.conf` declarative: only supported `package` records, never + executable shell code. - Make repeated setup safe and idempotent. - Download to a temporary location, verify it, and activate it atomically. - Never execute an unversioned remote release payload as the installer. @@ -103,15 +103,14 @@ Preserve these lifecycle invariants: - Ordinary shell startup must never install updates or block on the network. A cached release notice may refresh metadata in a non-blocking background job. -## Profiles and Dependencies +## Packages and Dependencies -`developer` is the default profile and includes `minimal` plus the larger -interactive tools, jq, build tools, and language/editor tooling. `minimal` is -the explicit lightweight choice. Ghostty is a separate saved macOS installation -choice. +Selfishell provides one development environment, without selectable profiles. +Ghostty is a separate saved macOS installation choice. The `configured` marker +in the state directory records completed setup, not a package selection. -The developer profile's mise-managed tool membership is declared in -`profiles/developer.conf`; exact versions for those tools are pinned in +Mise-managed tool membership is declared in +`packages.conf`; exact versions for those tools are pinned in `config/shared/mise.toml`, the source of truth for mise-managed tool versions. When the tools/configuration phase runs, `--skip-packages` must skip package @@ -162,7 +161,7 @@ instead. After publication, verify all four archives, `SHA256SUMS`, generated ## Verification Run the smallest relevant tests while iterating, then run the repository gate -for any shell, lifecycle, profile, dependency, or release change: +for any shell, lifecycle, package, dependency, or release change: ```sh bash scripts/check.sh @@ -190,7 +189,7 @@ it; the gate remains required for the change categories listed above. | --- | --- | | `bin/`, `lib/` | CLI commands, lifecycle, platform and package adapters | | `config/` | Managed shared, macOS, and Ubuntu shell/editor configuration | -| `profiles/`, `dependencies.conf` | Declarative profiles and approved dependencies | +| `packages.conf`, `dependencies.conf` | Declarative packages and approved dependencies | | `tests/` | Isolated unit and lifecycle coverage | | `scripts/` | Validation, benchmarks, dependency discovery, release builds | | `.github/` | CI, dependency automation, and release publication | diff --git a/README.md b/README.md index 4ec40fc0..bc124166 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Install the CLI: curl -fsSL https://raw.githubusercontent.com/jiminu/selfishell/main/install.sh | bash ``` -Then install the default `developer` environment and verify it: +Then install the development environment and verify it: ```bash selfishell install @@ -32,35 +32,27 @@ Ghostty, and platform notes. - A consistent Zsh environment across supported macOS and Ubuntu systems. - A Starship prompt, useful aliases, completions, and managed shell defaults. -- `minimal` and `developer` profiles, with Neovim and development tooling in - the developer profile. +- Neovim, mise-managed runtimes, CLI tools, and build tooling. - Checksum-verified release updates and a retained release for offline rollback. - User-owned shell files: personal aliases, exports, functions, and project tooling stay outside Selfishell-managed blocks. ![Selfishell Neovim workspace showing the file explorer, buffer tabs, shell-script syntax highlighting, and compact statusline](img/nvim.png) -## Profiles +## Development Environment -`developer` is the default profile; choose `minimal` explicitly for a lighter -shell setup. - -| Profile | Includes | -| --- | --- | -| `minimal` | Core Zsh, Git, Vim, Starship, Zinit, and shell configuration | -| `developer` | Everything in `minimal`, plus Neovim, mise-managed runtimes, CLI/editor tools, and build tooling | - -See [Profiles](docs/PROFILES.md) for the complete package list and profile -behavior. +`selfishell install` sets up one consistent environment: Zsh, Git, Vim, +Starship, Zinit, Neovim, mise-managed runtimes, CLI tools, and build tooling. +See [Environment](docs/ENVIRONMENT.md) for tool management and editor behavior. ## Everyday Commands | Command | Use it to | | --- | --- | -| `selfishell status` | Show the active profile, managed resources, and CLI/rollback version. | +| `selfishell status` | Show installed tools, managed resources, and CLI/rollback version. | | `selfishell doctor` | Diagnose the current installation. | | `selfishell version --available` | Check the latest published release. | -| `selfishell update` | Update the CLI, profile tools, and configuration. | +| `selfishell update` | Update the CLI, tools, and configuration. | | `selfishell rollback` | Return to the previous retained release offline. | | `selfishell uninstall --restore --dry-run` | Preview restoring backed-up configuration. | @@ -80,7 +72,7 @@ startup. - [Installation](docs/INSTALLATION.md) — setup, uninstallation, Ghostty, and platform notes. -- [Profiles](docs/PROFILES.md) — package choices and Neovim workflow. +- [Environment](docs/ENVIRONMENT.md) — managed tools and Neovim workflow. - [Python development](docs/PYTHON.md) — Python tooling and project setup. - [Updates and rollback](docs/UPDATES.md) — release updates and recovery. - [Troubleshooting](docs/TROUBLESHOOTING.md) — common installation and shell diff --git a/bin/selfishell b/bin/selfishell index 7ef2e7ca..acdc62b1 100755 --- a/bin/selfishell +++ b/bin/selfishell @@ -49,9 +49,9 @@ source "$SELFISHELL_ROOT/lib/platform.sh" source "$SELFISHELL_ROOT/lib/resources.sh" source "$SELFISHELL_ROOT/lib/dependencies.sh" source "$SELFISHELL_ROOT/lib/tool_status.sh" -source "$SELFISHELL_ROOT/lib/profile_scan.sh" +source "$SELFISHELL_ROOT/lib/package_scan.sh" source "$SELFISHELL_ROOT/lib/managed.sh" -source "$SELFISHELL_ROOT/lib/profiles.sh" +source "$SELFISHELL_ROOT/lib/package_manifest.sh" source "$SELFISHELL_ROOT/lib/package_managers/apt.sh" source "$SELFISHELL_ROOT/lib/package_managers/homebrew.sh" source "$SELFISHELL_ROOT/lib/installers.sh" diff --git a/config/shared/mise.toml b/config/shared/mise.toml index 34bd2924..6cf42348 100644 --- a/config/shared/mise.toml +++ b/config/shared/mise.toml @@ -1,4 +1,5 @@ [tools] +starship = "1.26.0" fzf = "0.74.4" zoxide = "0.10.0" ripgrep = "15.2.0" @@ -14,4 +15,3 @@ gh = "2.100.0" [settings] not_found_auto_install = false - diff --git a/config/shared/nvim/lua/plugins/ui.lua b/config/shared/nvim/lua/plugins/ui.lua index e7cadc53..daea7b1c 100644 --- a/config/shared/nvim/lua/plugins/ui.lua +++ b/config/shared/nvim/lua/plugins/ui.lua @@ -452,7 +452,7 @@ return { }, }, opts = { - -- The developer profile guarantees ripgrep, but not fd. + -- Selfishell installs ripgrep, but not fd. picker = { ui_select = false, sources = { diff --git a/config/shared/zsh/aliases.zsh b/config/shared/zsh/aliases.zsh index 6256a674..0ddfd723 100644 --- a/config/shared/zsh/aliases.zsh +++ b/config/shared/zsh/aliases.zsh @@ -16,7 +16,7 @@ if _selfishell_command_path nvim >/dev/null; then alias view='nvim -R' fi -# Git is a required dependency in every profile, so these aliases need no +# Git is a required dependency, so these aliases need no # _selfishell_command_path guard. alias gst='git status -s' alias gf='git fetch' diff --git a/dependencies.conf b/dependencies.conf index 327d236b..3f7cf75a 100644 --- a/dependencies.conf +++ b/dependencies.conf @@ -3,10 +3,6 @@ download mise 2026.9.7 linux amd64 https://github.com/jdx/mise/releases/download download mise 2026.9.7 linux arm64 https://github.com/jdx/mise/releases/download/v2026.9.7/mise-v2026.9.7-linux-arm64 1227565aeff505b0b0735d91b63109e1e1e44a237c06dc9aa274f2f3bf732af7 .local/bin/mise raw download mise 2026.9.7 macos amd64 https://github.com/jdx/mise/releases/download/v2026.9.7/mise-v2026.9.7-macos-x64 06f763e37615966f0d660f0fd7d4d23568d1b63d5bdb65e2f7a98af2fdc36075 .local/bin/mise raw download mise 2026.9.7 macos arm64 https://github.com/jdx/mise/releases/download/v2026.9.7/mise-v2026.9.7-macos-arm64 3c3f377e7123a466274a20f01502ddd8c58f76028907f471c9bc42fbf83846e1 .local/bin/mise raw -download starship 1.26.0 linux amd64 https://github.com/starship/starship/releases/download/v1.26.0/starship-x86_64-unknown-linux-gnu.tar.gz 321f0dd7af8340a5f2e6a8fec6538a04f617486f9ec70d878f91c09cd8deef22 .local/bin/starship starship -download starship 1.26.0 linux arm64 https://github.com/starship/starship/releases/download/v1.26.0/starship-aarch64-unknown-linux-musl.tar.gz dc30189378d2f2e287384e8a692d3f95ad1df64cf0e8c36aa9201516028aed6b .local/bin/starship starship -download starship 1.26.0 macos amd64 https://github.com/starship/starship/releases/download/v1.26.0/starship-x86_64-apple-darwin.tar.gz 5548f406a4b6f5695903bdea83f77ce47ec12c8c0e62dabd33122d8f133e4207 .local/bin/starship starship -download starship 1.26.0 macos arm64 https://github.com/starship/starship/releases/download/v1.26.0/starship-aarch64-apple-darwin.tar.gz c40b27b11f580411e068f2fa6c1be7830a387c0bc47a94d1d37f32b054c5361d .local/bin/starship starship git zinit v3.17.0 all all https://github.com/zdharma-continuum/zinit.git - .local/share/zinit/zinit.git zinit.zsh zsh-plugin zsh-users/zsh-completions de02bb84ab0af51e328c6ae85ab5555397c31277 all all https://github.com/zsh-users/zsh-completions.git - - - zsh-plugin Aloxaf/fzf-tab 24105b15714bfec37989ed5c5b6e60f572253019 all all https://github.com/Aloxaf/fzf-tab.git - - - diff --git a/docs/COMPANY.md b/docs/COMPANY.md index c6750499..e7a4d59c 100644 --- a/docs/COMPANY.md +++ b/docs/COMPANY.md @@ -15,5 +15,5 @@ Recommended deployment controls: 3. Provision Homebrew separately if executing its upstream bootstrap is not an acceptable trust decision. 4. Never place credentials, tokens, kubeconfigs, internal URLs, or certificate - private keys in a profile or this repository. -5. Validate the selected profile on a clean managed image before broad rollout. + private keys in package configuration or this repository. +5. Validate the environment on a clean managed image before broad rollout. diff --git a/docs/PROFILES.md b/docs/ENVIRONMENT.md similarity index 63% rename from docs/PROFILES.md rename to docs/ENVIRONMENT.md index 8c5669d2..73ce45d6 100644 --- a/docs/PROFILES.md +++ b/docs/ENVIRONMENT.md @@ -1,58 +1,44 @@ -# Profiles +# Development Environment -Profiles are cumulative. Choose the smallest profile that covers the machine's -role. +Selfishell installs one development environment. `packages.conf` declares +its packages: Zsh, Git, Vim, Starship, Zinit, Neovim, CLI tools, language +runtimes, compiler tooling, and optional macOS terminal fonts. -| Profile | Purpose | -| --- | --- | -| `minimal` | Core shell, Zinit, Vim, and macOS terminal fonts | -| `developer` | Minimal plus Neovim, Tree-sitter CLI, Node.js, Python, uv, GitHub CLI, FZF, Zoxide, Ripgrep, Eza, Bat, jq, and compiler tooling | - -`developer` is selected when `--profile` is omitted. Choose `minimal` -explicitly for a lightweight shell setup without the larger development -toolchain. - -The `developer` profile installs a pinned mise binary and activates it for +Selfishell installs a pinned mise binary and activates it for interactive Zsh. Selfishell keeps its defaults in `${XDG_CONFIG_HOME:-$HOME/.config}/selfishell/mise/selfishell.toml` (which is symlinked to `~/.config/mise/conf.d/selfishell.toml` so it is automatically loaded by `mise`); a project's `mise.toml` can select different tool versions. Developer tools managed by mise use exact reviewed versions pinned in `config/shared/mise.toml`, the single source of truth for these versions. This -includes FZF, Zoxide, Ripgrep, Eza, Bat, jq, Neovim, Tree-sitter CLI, Node.js, +includes Starship, FZF, Zoxide, Ripgrep, Eza, Bat, jq, Neovim, Tree-sitter CLI, Node.js, Python, uv, and GitHub CLI on both macOS and Ubuntu. Eza and Bat remain optional; failure to install either does not stop setup. Projects remain free to override the defaults in a local `mise.toml`. Updating the defaults requires a normal Selfishell release and never happens during shell startup. +Mise manages the Starship executable and its version; Selfishell still manages +`starship.toml` and prompt initialization. + Preview without changing the machine: ```sh selfishell install --dry-run ``` -Install or change the selected profile explicitly: +Install the environment: ```sh -selfishell install --profile minimal --yes +selfishell install --yes ``` -The active profile is recorded in the XDG state directory. `selfishell update` -uses that recorded profile to install missing Apt, Homebrew, and directly +`selfishell update` uses `packages.conf` to install missing Apt, Homebrew, and directly managed tools and synchronize mise tools before updating configuration. Apt and Homebrew retain responsibility for packages still declared through them. -Copies of a tool left from an older profile release are not removed +Copies of a tool left from an older release are not removed automatically; after mise activation, its pinned tool version takes precedence. -Changing from `developer` to `minimal` changes future tool synchronization; -it does not remove previously installed tools or configuration. Existing -Neovim and mise configuration links remain active, and `selfishell status` -continues to check all tracked paths, including retained developer resources. -To remove managed configuration before setting up a smaller profile, run -`selfishell uninstall --restore`, then `selfishell install --profile minimal`. -Uninstall leaves installed packages and user-created files in place. - -Profile package requirements have two failure policies: +Package requirements have two failure policies: - `required` packages must be available and install successfully; - `optional` packages are recommended and attempted automatically, but an @@ -67,7 +53,7 @@ choice is saved and reused by `selfishell update`. ## Neovim workflow -The `developer` profile includes a pinned Neovim configuration whose leader key +Selfishell includes a pinned Neovim configuration whose leader key is `Space`. In Normal mode, press `Space` and pause to open which-key. The popup shows actions available in the current context; continue typing to narrow the list. Every Selfishell mapping has a description, so which-key remains aligned @@ -91,5 +77,5 @@ applied. Bufferline shows open buffers across the top; use `[b` and `]b` to move between them, and `Space b d` to close the current buffer without closing its editor window. -In the `developer` profile, `vim` resolves to Neovim while `vi` remains the +When Neovim is available, `vim` resolves to Neovim while `vi` remains the system editor. diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index bc266f65..31d76418 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -9,8 +9,7 @@ curl -fsSL https://raw.githubusercontent.com/jiminu/selfishell/main/install.sh | selfishell install ``` -`selfishell install` selects the recommended `developer` profile. Use -`selfishell install --profile minimal` for a lightweight shell setup. +`selfishell install` sets up the complete development environment. ## Verification coverage @@ -41,9 +40,9 @@ The bootstrap installs only the CLI unless `--setup` is explicitly supplied. Version discovery prefers the latest stable release and otherwise uses the newest version tag only after its exact `VERSION` release asset is published. -### Install CLI and default profile together +### Install CLI and environment together -Install the CLI and default `developer` profile non-interactively: +Install the CLI and development environment non-interactively: ```sh curl -fsSL https://raw.githubusercontent.com/jiminu/selfishell/main/install.sh | @@ -60,7 +59,7 @@ Use an exact release in controlled environments: ```sh curl -fsSL https://raw.githubusercontent.com/jiminu/selfishell/main/install.sh | bash -s -- --version -selfishell install --profile developer --yes +selfishell install --yes ``` The archive is downloaded to a temporary directory, checked against the @@ -74,7 +73,7 @@ release for offline rollback and removes older inactive releases. For configuration-only installation after the CLI is provisioned: ```sh -selfishell install --profile developer --skip-packages --yes +selfishell install --skip-packages --yes ``` `--skip-packages` performs configuration-only installation without package or diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 05f3eb7a..a663c74d 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -35,12 +35,12 @@ cannot reintroduce host tools. A normal local run therefore does not read the developer's mise configuration or execute or modify the developer's plugin checkout. -### Full-profile mode +### Full-environment mode Full mode additionally provisions the pinned mise, Starship, and Zinit -- with its pinned Zsh plugins -- into the benchmark's own isolated `HOME`, via the same code path the real installer uses, so `interactive-cached` reflects a -real developer-profile startup rather than whatever happens to already be on +real full-environment startup rather than whatever happens to already be on the runner's `PATH`. It: - uses an isolated, temporary `HOME`; the real user `HOME` is never read or @@ -48,10 +48,10 @@ the runner's `PATH`. It: - uses that home as its working directory and gives mise an isolated global config; - installs the pinned mise, Starship, and Zinit (with its pinned plugins) - into that isolated `HOME`; + into that isolated `HOME`, with Starship installed through mise using the + release's exact pin; - measures fzf and zoxide only if they are already on `PATH` -- installing - packages is out of scope for this script, so provision them via the - platform package manager first; + those tools is out of scope for this script, so provision them first; - needs network access to provision those tools, so it is not part of the regular (network-free) unit test suite, or run in CI -- run it locally when needed. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 507b5b54..8565b169 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -15,7 +15,7 @@ selfishell doctor selfishell status ``` -`status` reports tools from the active profile as Selfishell-managed, Homebrew, +`status` reports tools from the environment as Selfishell-managed, Homebrew, apt, external, or missing. Package-manager versions are reported without an exact approved version because those repositories control resolution. It returns nonzero when required tools are missing or managed configuration is diff --git a/docs/UPDATES.md b/docs/UPDATES.md index 10641e75..bc7e534a 100644 --- a/docs/UPDATES.md +++ b/docs/UPDATES.md @@ -3,7 +3,7 @@ By default, `selfishell update` updates to the latest release and then synchronizes that release's managed environment. The CLI release is switched first; if it changed, the new CLI continues the same command so packages newly -added to that release's profile are included. If the target release is +added to that release's package list are included. If the target release is already installed, the command reports that and exits without changing anything. Use `--tools-only` to explicitly resynchronize the current release's tools and configuration regardless of whether a new release is available. @@ -31,10 +31,10 @@ selfishell rollback --yes ``` The tools/configuration phase synchronizes apt or Homebrew packages from the -installed profile, installs directly managed tools at the approved versions in +release's `packages.conf`, installs directly managed tools at the approved versions in `dependencies.conf`, synchronizes mise-managed developer tools, reapplies -managed configuration, and synchronizes Neovim plugins for the developer -profile; Tree-sitter parsers install lazily the first time their filetype is +managed configuration, and synchronizes Neovim plugins; +Tree-sitter parsers install lazily the first time their filetype is opened. Already installed operating-system packages remain managed by apt or Homebrew; this command does not perform a general package upgrade. A CLI-only update skips this phase, as does a default update @@ -46,7 +46,7 @@ configuration for the current release. skips package and tool installation and applies managed configuration only, the same contract `selfishell install` follows. -`status` reports local CLI, rollback, profile, and managed-resource state +`status` reports local CLI, rollback, tools, and managed-resource state only; it never checks the network. Use `selfishell version --available` to check the latest published release, or rely on the automatic update notice. @@ -76,14 +76,14 @@ That manifest is also the source of truth for exact Neovim plugin commits, so a repository `lazy-lock.json` is intentionally unnecessary. lazy.nvim may write a runtime lock under the Selfishell state directory, but updates cannot move a plugin beyond the commit approved in the release manifest. -`profiles/developer.conf` declares mise-managed developer tool membership; +`packages.conf` declares mise-managed developer tool membership; exact default versions are pinned only in `config/shared/mise.toml`, updated through the same review-and-release boundary. Individual project `mise.toml` files remain outside Selfishell's update lifecycle. Maintainers can run `scripts/update-dependencies.sh` to discover current -upstream releases, download platform artifacts, calculate Starship and mise -checksums, and bump mise-managed tool versions. The weekly `Dependency +upstream releases, download mise platform artifacts, calculate their +checksums, and bump mise-managed tool versions, including Starship. The weekly `Dependency updates` workflow uses the same script and opens or refreshes a review PR only when a tracked file changes. It never merges the PR or publishes a Selfishell release. Review diff --git a/install.sh b/install.sh index 38827dff..6f352680 100755 --- a/install.sh +++ b/install.sh @@ -45,14 +45,13 @@ bootstrap_help() { cat <<'EOF' Usage: install.sh [--version VERSION] [--prefix PATH] [--setup] [--yes] - [--profile NAME] [--skip-packages] + [--skip-packages] Options: --version VERSION Install an exact Selfishell release --prefix PATH Installation prefix (default: $HOME/.local) --setup Run 'selfishell install' after installing the CLI --yes Skip setup confirmation when used with --setup - --profile NAME Profile passed to setup (default: developer) --skip-packages Pass configuration-only mode to setup --help Show this help EOF @@ -277,7 +276,6 @@ main() { local prefix="${HOME}/.local" local setup=0 local assume_yes=0 - local profile=developer local skip_packages=0 local platform local architecture @@ -317,14 +315,6 @@ main() { ;; --setup) setup=1 ;; --yes) assume_yes=1 ;; - --profile) - shift - (("$#" > 0)) || { - bootstrap_error "--profile requires a value" - return 2 - } - profile="$1" - ;; --skip-packages) skip_packages=1 ;; help | --help | -h) bootstrap_help @@ -428,7 +418,7 @@ main() { fi if [[ "$setup" == "1" ]]; then - setup_args=(install --profile "$profile") + setup_args=(install) [[ "$skip_packages" == "1" ]] && setup_args+=(--skip-packages) [[ "$assume_yes" == "1" ]] && setup_args+=(--yes) "$bin_dir/selfishell" "${setup_args[@]}" diff --git a/lib/commands/doctor.sh b/lib/commands/doctor.sh index 9500773e..d4af758c 100644 --- a/lib/commands/doctor.sh +++ b/lib/commands/doctor.sh @@ -89,7 +89,7 @@ command_doctor() { local platform local architecture local package_manager - local profile profile_platform dependency_platform + local package_platform dependency_platform local result="$SELFISHELL_EXIT_OK" platform="$(detect_platform)" @@ -138,32 +138,28 @@ command_doctor() { fi selfishell_initialize_paths - if [[ -r "$SELFISHELL_STATE_DIR/profile" ]] && platform_is_supported "$platform"; then + if [[ -r "$SELFISHELL_STATE_DIR/configured" ]] && platform_is_supported "$platform"; then tool_status_reset_cache - profile="$(<"$SELFISHELL_STATE_DIR/profile")" - doctor_info "Installed profile: $profile" - if [[ "$profile" == developer ]]; then - doctor_info "Developer profile active: Neovim and mise-managed runtimes are enabled." - if have_command gcc; then - doctor_ok "C compiler: gcc ($(gcc --version | head -n 1))" - elif have_command clang; then - doctor_ok "C compiler: clang ($(clang --version | head -n 1))" + doctor_info "Selfishell configuration is installed." + if have_command gcc; then + doctor_ok "C compiler: gcc ($(gcc --version | head -n 1))" + elif have_command clang; then + doctor_ok "C compiler: clang ($(clang --version | head -n 1))" + else + doctor_error "C compiler: gcc or clang was not found (required for compiling Tree-sitter parsers)" + if [[ "$platform" == "macos" ]]; then + printf " Install Xcode Command Line Tools by running: %sxcode-select --install%s\n" \ + "$SELFISHELL_COLOR_BOLD" "$SELFISHELL_COLOR_RESET" else - doctor_error "C compiler: gcc or clang was not found (required for compiling Tree-sitter parsers)" - if [[ "$platform" == "macos" ]]; then - printf " Install Xcode Command Line Tools by running: %sxcode-select --install%s\n" \ - "$SELFISHELL_COLOR_BOLD" "$SELFISHELL_COLOR_RESET" - else - printf " Install build tools by running: %ssudo apt install build-essential%s\n" \ - "$SELFISHELL_COLOR_BOLD" "$SELFISHELL_COLOR_RESET" - fi - result="$SELFISHELL_EXIT_ERROR" + printf " Install build tools by running: %ssudo apt install build-essential%s\n" \ + "$SELFISHELL_COLOR_BOLD" "$SELFISHELL_COLOR_RESET" fi + result="$SELFISHELL_EXIT_ERROR" fi dependency_platform="$(platform_dependency_platform "$platform")" - profile_platform="$(platform_profile_platform "$platform")" + package_platform="$(platform_package_platform "$platform")" DOCTOR_RESULT="$result" - selfishell_scan_profile_packages "$profile" "$dependency_platform" "$architecture" doctor_report_package "$profile_platform" + selfishell_scan_packages "$dependency_platform" "$architecture" doctor_report_package "$package_platform" doctor_report_zinit_plugins result="$DOCTOR_RESULT" fi diff --git a/lib/commands/install.sh b/lib/commands/install.sh index 74c210b8..b043e443 100644 --- a/lib/commands/install.sh +++ b/lib/commands/install.sh @@ -3,10 +3,9 @@ print_install_help() { cat <<'EOF' Usage: - selfishell install [--profile NAME] [--skip-packages] [--dry-run] [--yes] + selfishell install [--skip-packages] [--dry-run] [--yes] Options: - --profile NAME Select minimal or developer (default: developer) --skip-packages Skip package and tool installation and apply managed configuration only --dry-run Show changes without modifying files --yes Skip interactive confirmation @@ -17,9 +16,8 @@ EOF install_managed_configuration() { local platform="$1" local dry_run="$2" - local profile="$3" - local ghostty_enabled="${4:-0}" - local assume_yes="${5:-0}" + local ghostty_enabled="${3:-0}" + local assume_yes="${4:-0}" local zsh_source local resource_kind resource_name resource_target resource_source @@ -35,9 +33,6 @@ install_managed_configuration() { while IFS=$'\t' read -r resource_kind resource_name resource_target resource_source; do case "$resource_kind" in file) - if [[ "$profile" != "developer" && "$resource_name" == nvim-* ]]; then - continue - fi if [[ "$resource_name" == "zshrc-config" ]]; then resource_source="$zsh_source" fi @@ -52,9 +47,6 @@ install_managed_configuration() { managed_install_file "$resource_name" "$resource_source" "$resource_target" "$dry_run" "$assume_yes" ;; link) - if [[ "$profile" != "developer" && ("$resource_name" == user-nvim || "$resource_name" == mise-config-link) ]]; then - continue - fi managed_install_link "$resource_name" "$resource_target" "$resource_source" "$dry_run" ;; block) @@ -195,7 +187,6 @@ install_mise_global_config() { command_install() { local assume_yes=0 local dry_run=0 - local profile=developer local skip_packages=0 local platform local ghostty_enabled=0 @@ -208,14 +199,6 @@ command_install() { --dry-run) dry_run=1 ;; --yes) assume_yes=1 ;; --skip-packages) skip_packages=1 ;; - --profile) - shift - if (("$#" == 0)); then - cli_error "--profile requires a value" - return "$SELFISHELL_EXIT_USAGE" - fi - profile="$1" - ;; help | --help | -h) print_install_help return @@ -228,6 +211,7 @@ command_install() { shift done + package_manifest_load || return platform="$(detect_platform)" if ! platform_is_supported "$platform"; then cli_error "Managed installation is unavailable on $(platform_label "$platform")." @@ -244,10 +228,7 @@ command_install() { ;; esac managed_preflight_block_target user-vimrc "$HOME/.vimrc" "$assume_yes" "$dry_run" || return - profile_load "$profile" - if [[ "$profile" == "developer" ]]; then - preflight_mise_global_config || return - fi + preflight_mise_global_config || return if [[ "$platform" == "macos" ]]; then if [[ -r "$SELFISHELL_STATE_DIR/ghostty" ]]; then @@ -270,36 +251,34 @@ command_install() { if [[ "$skip_packages" == "1" ]]; then printf '%sSkipping package and tool installation.%s\n' "$SELFISHELL_COLOR_CYAN" "$SELFISHELL_COLOR_RESET" else - packages_install_profile "$platform" "$dry_run" + packages_install "$platform" "$dry_run" if [[ "$platform" == "macos" && "$ghostty_enabled" == "1" ]]; then homebrew_install_packages optional cask "$dry_run" ghostty fi fi - install_managed_configuration "$platform" "$dry_run" "$profile" "$ghostty_enabled" "$assume_yes" - if [[ "$profile" == "developer" ]]; then - install_mise_global_config "$dry_run" || return - fi - if [[ "$skip_packages" == "0" && "$profile" == "developer" ]]; then + install_managed_configuration "$platform" "$dry_run" "$ghostty_enabled" "$assume_yes" + install_mise_global_config "$dry_run" || return + if [[ "$skip_packages" == "0" ]]; then install_neovim_plugins "$dry_run" || return fi install_default_shell "$dry_run" "$assume_yes" if [[ "$dry_run" == "0" ]]; then - local profile_state - local temporary_profile_state + local configured_state + local temporary_configured_state local ghostty_state local temporary_ghostty_state mkdir -p "$SELFISHELL_STATE_DIR" || return "$SELFISHELL_EXIT_ERROR" - profile_state="$SELFISHELL_STATE_DIR/profile" - temporary_profile_state="$(mktemp "${profile_state}.tmp.XXXXXX")" || return "$SELFISHELL_EXIT_ERROR" - printf '%s\n' "$profile" >"$temporary_profile_state" || { - rm -f "$temporary_profile_state" + configured_state="$SELFISHELL_STATE_DIR/configured" + temporary_configured_state="$(mktemp "${configured_state}.tmp.XXXXXX")" || return "$SELFISHELL_EXIT_ERROR" + printf '1\n' >"$temporary_configured_state" || { + rm -f "$temporary_configured_state" return "$SELFISHELL_EXIT_ERROR" } - mv "$temporary_profile_state" "$profile_state" || { - rm -f "$temporary_profile_state" + mv "$temporary_configured_state" "$configured_state" || { + rm -f "$temporary_configured_state" return "$SELFISHELL_EXIT_ERROR" } diff --git a/lib/commands/status.sh b/lib/commands/status.sh index 7054ff77..71268af3 100644 --- a/lib/commands/status.sh +++ b/lib/commands/status.sh @@ -114,8 +114,7 @@ command_status() { local verbose=0 local current_version="unknown" local rollback_version="none" - local platform profile_platform dependency_platform architecture - local profile="" + local platform package_platform dependency_platform architecture local resource while (("$#" > 0)); do @@ -149,23 +148,15 @@ command_status() { platform="$(detect_platform)" dependency_platform="$(platform_dependency_platform "$platform")" - profile_platform="$(platform_profile_platform "$platform")" + package_platform="$(platform_package_platform "$platform")" architecture="$(detect_architecture)" - if [[ -r "$SELFISHELL_STATE_DIR/profile" ]]; then - profile="$(<"$SELFISHELL_STATE_DIR/profile")" - printf '%s[INFO]%s Installed profile: %s\n' "$SELFISHELL_COLOR_CYAN" "$SELFISHELL_COLOR_RESET" "$profile" - selfishell_scan_profile_packages "$profile" "$dependency_platform" "$architecture" status_report_package "$profile_platform" + if [[ -r "$SELFISHELL_STATE_DIR/configured" ]]; then + printf '%s[INFO]%s Selfishell configuration is installed.\n' "$SELFISHELL_COLOR_CYAN" "$SELFISHELL_COLOR_RESET" + selfishell_scan_packages "$dependency_platform" "$architecture" status_report_package "$package_platform" fi - if [[ "$profile" == minimal ]] && - { managed_state_exists user-nvim || managed_state_exists mise-config-link; }; then - printf '%s[INFO]%s Previously installed developer configuration is retained and checked below.\n' \ - "$SELFISHELL_COLOR_CYAN" "$SELFISHELL_COLOR_RESET" - fi - - # The selected profile controls future installation, not ownership of paths - # retained from an earlier profile or platform. + # Check every tracked path, including resources retained from another platform. while IFS= read -r resource; do status_resource "$resource" done < <(selfishell_managed_resource_names) diff --git a/lib/commands/uninstall.sh b/lib/commands/uninstall.sh index 32656b23..4ed0e3d1 100644 --- a/lib/commands/uninstall.sh +++ b/lib/commands/uninstall.sh @@ -137,7 +137,7 @@ command_uninstall() { fi if [[ "$dry_run" == "0" ]]; then - rm -f "$SELFISHELL_STATE_DIR/profile" "$SELFISHELL_STATE_DIR/ghostty" + rm -f "$SELFISHELL_STATE_DIR/configured" "$SELFISHELL_STATE_DIR/ghostty" rmdir "$SELFISHELL_CONFIG_DIR/ghostty" 2>/dev/null || true rmdir "$SELFISHELL_CONFIG_DIR/nvim/after/lsp" 2>/dev/null || true rmdir "$SELFISHELL_CONFIG_DIR/nvim/after" 2>/dev/null || true diff --git a/lib/commands/update.sh b/lib/commands/update.sh index 552b2542..76105b3f 100644 --- a/lib/commands/update.sh +++ b/lib/commands/update.sh @@ -7,7 +7,7 @@ Usage: [--skip-packages] [--dry-run] [--yes] By default, update to the latest Selfishell release and synchronize that -release's profile packages, approved tools, and managed configuration. If the +release's packages, approved tools, and managed configuration. If the target release is already installed, no changes are made. Use --tools-only to explicitly resynchronize the current release's tools and configuration, or --cli-only to limit the scope to the CLI release itself. @@ -32,10 +32,10 @@ update_tools_and_configuration() { # 1 when command_update closes with the version transition instead, so this # phase leaves the final result to it. local defer_result="$5" - local profile platform ghostty_enabled=0 + local platform ghostty_enabled=0 selfishell_initialize_paths - if [[ ! -r "$SELFISHELL_STATE_DIR/profile" ]]; then + if [[ ! -r "$SELFISHELL_STATE_DIR/configured" ]]; then if [[ "$require_configuration" == 1 ]]; then cli_error "Selfishell configuration is not installed." return "$SELFISHELL_EXIT_ERROR" @@ -43,10 +43,10 @@ update_tools_and_configuration() { printf '%sSelfishell configuration is not installed; skipping tools and configuration.%s\n' "$SELFISHELL_COLOR_CYAN" "$SELFISHELL_COLOR_RESET" return fi - profile="$(<"$SELFISHELL_STATE_DIR/profile")" + package_manifest_load || return [[ ! -r "$SELFISHELL_STATE_DIR/ghostty" ]] || ghostty_enabled="$(<"$SELFISHELL_STATE_DIR/ghostty")" [[ "$ghostty_enabled" == "1" ]] || ghostty_enabled=0 - confirm_action "Synchronize $profile profile packages and configuration?" "$assume_yes" "$dry_run" || return + confirm_action "Synchronize Selfishell packages and configuration?" "$assume_yes" "$dry_run" || return platform="$(detect_platform)" managed_preflight_zsh_loader "$assume_yes" "$dry_run" || return managed_preflight_block_target user-zprofile "$HOME/.zprofile" "$assume_yes" "$dry_run" || return @@ -62,18 +62,16 @@ update_tools_and_configuration() { "${XDG_CONFIG_HOME:-$HOME/.config}/ghostty/config.ghostty" "$assume_yes" "$dry_run" || return fi - profile_load "$profile" - if [[ "$skip_packages" == "1" ]]; then printf '%sSkipping package and tool installation.%s\n' "$SELFISHELL_COLOR_CYAN" "$SELFISHELL_COLOR_RESET" else - packages_install_profile "$platform" "$dry_run" + packages_install "$platform" "$dry_run" if [[ "$platform" == "macos" && "$ghostty_enabled" == "1" ]]; then homebrew_install_packages optional cask "$dry_run" ghostty fi fi - install_managed_configuration "$platform" "$dry_run" "$profile" "$ghostty_enabled" "$assume_yes" - if [[ "$skip_packages" == "0" && "$profile" == "developer" ]]; then + install_managed_configuration "$platform" "$dry_run" "$ghostty_enabled" "$assume_yes" + if [[ "$skip_packages" == "0" ]]; then install_neovim_plugins "$dry_run" || return fi if [[ "$dry_run" == 1 ]]; then diff --git a/lib/installers.sh b/lib/installers.sh index fed71e0b..61382691 100644 --- a/lib/installers.sh +++ b/lib/installers.sh @@ -97,7 +97,7 @@ install_direct_package() { dependency_platform="$(platform_dependency_platform "$platform")" case "$package" in - starship | mise) + mise) dependency_install "$package" "$dependency_platform" "$architecture" ;; zinit) @@ -295,7 +295,7 @@ install_neovim_plugins() { fi if ! nvim_command="$(selfishell_nvim_command)"; then - cli_error "Could not locate Neovim after installing the developer profile." + cli_error "Could not locate Neovim after installing the development environment." return 1 fi diff --git a/lib/package_manifest.sh b/lib/package_manifest.sh new file mode 100644 index 00000000..0b7f1283 --- /dev/null +++ b/lib/package_manifest.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +package_manifest_reset() { + PACKAGE_PLATFORMS=() + PACKAGE_REQUIREMENTS=() + PACKAGE_MANAGERS=() + PACKAGE_NAMES=() +} + +package_manifest_add() { + local platform="$1" + local requirement="$2" + local manager="$3" + local package="$4" + local index + + for ((index = 0; index < ${#PACKAGE_NAMES[@]}; index++)); do + if [[ "${PACKAGE_PLATFORMS[$index]}" == "$platform" && + "${PACKAGE_REQUIREMENTS[$index]}" == "$requirement" && + "${PACKAGE_MANAGERS[$index]}" == "$manager" && + "${PACKAGE_NAMES[$index]}" == "$package" ]]; then + return + fi + done + + PACKAGE_PLATFORMS+=("$platform") + PACKAGE_REQUIREMENTS+=("$requirement") + PACKAGE_MANAGERS+=("$manager") + PACKAGE_NAMES+=("$package") +} + +package_manifest_read() { + local manifest_file="$1" + local record + local first + local second + local third + local fourth + local extra + + [[ -r "$manifest_file" ]] || { + cli_error "Package manifest not found: $manifest_file" + return "$SELFISHELL_EXIT_ERROR" + } + + while read -r record first second third fourth extra; do + [[ -z "$record" || "$record" == \#* ]] && continue + + case "$record" in + package) + if [[ -z "$first" || -z "$second" || -z "$third" || -z "$fourth" || -n "$extra" ]]; then + cli_error "Invalid package record in manifest: $manifest_file" + return "$SELFISHELL_EXIT_ERROR" + fi + case "$first" in macos | ubuntu | all) ;; *) + cli_error "Invalid package platform: $first" + return 1 + ;; + esac + case "$second" in required | optional) ;; *) + cli_error "Invalid package requirement: $second" + return 1 + ;; + esac + case "$third" in apt | formula | cask | direct | mise) ;; *) + cli_error "Invalid package manager: $third" + return 1 + ;; + esac + case "$fourth" in + -* | *[!A-Za-z0-9@+._/-]*) + cli_error "Invalid package name: $fourth" + return "$SELFISHELL_EXIT_USAGE" + ;; + esac + package_manifest_add "$first" "$second" "$third" "$fourth" + ;; + *) + cli_error "Unknown package manifest record: $record" + return "$SELFISHELL_EXIT_ERROR" + ;; + esac + done <"$manifest_file" +} + +package_manifest_load() { + package_manifest_reset + package_manifest_read "$SELFISHELL_ROOT/packages.conf" +} diff --git a/lib/package_scan.sh b/lib/package_scan.sh new file mode 100644 index 00000000..33d04908 --- /dev/null +++ b/lib/package_scan.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +selfishell_scan_packages() { + local dependency_platform="$1" + local architecture="$2" + local callback="$3" + local package_platform="$4" + local index package manager requirement key="" + + package_manifest_load + + for ((index = 0; index < ${#PACKAGE_NAMES[@]}; index++)); do + [[ "${PACKAGE_PLATFORMS[$index]}" == all || "${PACKAGE_PLATFORMS[$index]}" == "$package_platform" ]] || continue + package="${PACKAGE_NAMES[$index]}" + [[ "$key" != *"|$package|"* ]] || continue + key="${key}|${package}|" + manager="${PACKAGE_MANAGERS[$index]}" + requirement="${PACKAGE_REQUIREMENTS[$index]}" + "$callback" "$package" "$manager" "$requirement" "$dependency_platform" "$architecture" + done +} diff --git a/lib/packages.sh b/lib/packages.sh index b8c6521e..50544ea1 100644 --- a/lib/packages.sh +++ b/lib/packages.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash -packages_install_profile() { +packages_install() { local platform="$1" local dry_run="$2" local index - local profile_platform + local package_platform local architecture="" local required_apt=() local optional_apt=() @@ -30,24 +30,24 @@ packages_install_profile() { # shellcheck disable=SC2034 SELFISHELL_BREW_CASKS_READY=0 SELFISHELL_SKIPPED_OPTIONAL_PACKAGES=() - profile_platform="$(platform_profile_platform "$platform")" + package_platform="$(platform_package_platform "$platform")" - for ((index = 0; index < ${#PROFILE_PACKAGES[@]}; index++)); do - if [[ "${PROFILE_PLATFORMS[$index]}" != "all" && "${PROFILE_PLATFORMS[$index]}" != "$profile_platform" ]]; then + for ((index = 0; index < ${#PACKAGE_NAMES[@]}; index++)); do + if [[ "${PACKAGE_PLATFORMS[$index]}" != "all" && "${PACKAGE_PLATFORMS[$index]}" != "$package_platform" ]]; then continue fi - case "${PROFILE_REQUIREMENTS[$index]}:${PROFILE_MANAGERS[$index]}" in - required:apt) required_apt+=("${PROFILE_PACKAGES[$index]}") ;; - optional:apt) optional_apt+=("${PROFILE_PACKAGES[$index]}") ;; - required:formula) required_formula+=("${PROFILE_PACKAGES[$index]}") ;; - optional:formula) optional_formula+=("${PROFILE_PACKAGES[$index]}") ;; - required:cask) required_cask+=("${PROFILE_PACKAGES[$index]}") ;; - optional:cask) optional_cask+=("${PROFILE_PACKAGES[$index]}") ;; - required:direct) required_direct+=("${PROFILE_PACKAGES[$index]}") ;; - optional:direct) optional_direct+=("${PROFILE_PACKAGES[$index]}") ;; - required:mise) required_mise+=("${PROFILE_PACKAGES[$index]}") ;; - optional:mise) optional_mise+=("${PROFILE_PACKAGES[$index]}") ;; + case "${PACKAGE_REQUIREMENTS[$index]}:${PACKAGE_MANAGERS[$index]}" in + required:apt) required_apt+=("${PACKAGE_NAMES[$index]}") ;; + optional:apt) optional_apt+=("${PACKAGE_NAMES[$index]}") ;; + required:formula) required_formula+=("${PACKAGE_NAMES[$index]}") ;; + optional:formula) optional_formula+=("${PACKAGE_NAMES[$index]}") ;; + required:cask) required_cask+=("${PACKAGE_NAMES[$index]}") ;; + optional:cask) optional_cask+=("${PACKAGE_NAMES[$index]}") ;; + required:direct) required_direct+=("${PACKAGE_NAMES[$index]}") ;; + optional:direct) optional_direct+=("${PACKAGE_NAMES[$index]}") ;; + required:mise) required_mise+=("${PACKAGE_NAMES[$index]}") ;; + optional:mise) optional_mise+=("${PACKAGE_NAMES[$index]}") ;; esac done diff --git a/lib/platform.sh b/lib/platform.sh index 7cba5223..0365dba7 100644 --- a/lib/platform.sh +++ b/lib/platform.sh @@ -138,8 +138,8 @@ platform_dependency_platform() { esac } -# ubuntu-wsl has no profile-package entries of its own; it shares ubuntu's. -platform_profile_platform() { +# ubuntu-wsl has no package entries of its own; it shares ubuntu's. +platform_package_platform() { case "$1" in ubuntu | ubuntu-wsl) printf 'ubuntu\n' ;; *) printf '%s\n' "$1" ;; diff --git a/lib/profile_scan.sh b/lib/profile_scan.sh deleted file mode 100644 index b95100d5..00000000 --- a/lib/profile_scan.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash - -selfishell_scan_profile_packages() { - local profile="$1" - local dependency_platform="$2" - local architecture="$3" - local callback="$4" - local profile_platform="$5" - local index package manager requirement key="" - - profile_load "$profile" - - for ((index = 0; index < ${#PROFILE_PACKAGES[@]}; index++)); do - [[ "${PROFILE_PLATFORMS[$index]}" == all || "${PROFILE_PLATFORMS[$index]}" == "$profile_platform" ]] || continue - package="${PROFILE_PACKAGES[$index]}" - [[ "$key" != *"|$package|"* ]] || continue - key="${key}|${package}|" - manager="${PROFILE_MANAGERS[$index]}" - requirement="${PROFILE_REQUIREMENTS[$index]}" - "$callback" "$package" "$manager" "$requirement" "$dependency_platform" "$architecture" - done -} diff --git a/lib/profiles.sh b/lib/profiles.sh deleted file mode 100644 index a01d6d14..00000000 --- a/lib/profiles.sh +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env bash - -profile_reset() { - PROFILE_PLATFORMS=() - PROFILE_REQUIREMENTS=() - PROFILE_MANAGERS=() - PROFILE_PACKAGES=() -} - -profile_is_supported() { - case "$1" in - minimal | developer) return 0 ;; - *) return 1 ;; - esac -} - -profile_add_package() { - local platform="$1" - local requirement="$2" - local manager="$3" - local package="$4" - local index - - for ((index = 0; index < ${#PROFILE_PACKAGES[@]}; index++)); do - if [[ "${PROFILE_PLATFORMS[$index]}" == "$platform" && - "${PROFILE_REQUIREMENTS[$index]}" == "$requirement" && - "${PROFILE_MANAGERS[$index]}" == "$manager" && - "${PROFILE_PACKAGES[$index]}" == "$package" ]]; then - return - fi - done - - PROFILE_PLATFORMS+=("$platform") - PROFILE_REQUIREMENTS+=("$requirement") - PROFILE_MANAGERS+=("$manager") - PROFILE_PACKAGES+=("$package") -} - -profile_read_file() { - local profile_file="$1" - local record - local first - local second - local third - local fourth - local extra - - [[ -r "$profile_file" ]] || { - cli_error "Profile file not found: $profile_file" - return "$SELFISHELL_EXIT_ERROR" - } - - while read -r record first second third fourth extra; do - [[ -z "$record" || "$record" == \#* ]] && continue - - case "$record" in - include) - if [[ -z "$first" || -n "$second" ]]; then - cli_error "Invalid include in profile: $profile_file" - return "$SELFISHELL_EXIT_ERROR" - fi - profile_load_builtin "$first" - ;; - package) - if [[ -z "$first" || -z "$second" || -z "$third" || -z "$fourth" || -n "$extra" ]]; then - cli_error "Invalid package record in profile: $profile_file" - return "$SELFISHELL_EXIT_ERROR" - fi - case "$first" in macos | ubuntu | all) ;; *) - cli_error "Invalid package platform: $first" - return 1 - ;; - esac - case "$second" in required | optional) ;; *) - cli_error "Invalid package requirement: $second" - return 1 - ;; - esac - case "$third" in apt | formula | cask | direct | mise) ;; *) - cli_error "Invalid package manager: $third" - return 1 - ;; - esac - case "$fourth" in - -* | *[!A-Za-z0-9@+._/-]*) - cli_error "Invalid package name: $fourth" - return "$SELFISHELL_EXIT_USAGE" - ;; - esac - profile_add_package "$first" "$second" "$third" "$fourth" - ;; - *) - cli_error "Unknown profile record: $record" - return "$SELFISHELL_EXIT_ERROR" - ;; - esac - done <"$profile_file" -} - -profile_load_builtin() { - local profile="$1" - - if ! profile_is_supported "$profile"; then - cli_error "Unknown profile: $profile (expected: minimal or developer)" - return "$SELFISHELL_EXIT_USAGE" - fi - - profile_read_file "$SELFISHELL_ROOT/profiles/$profile.conf" -} - -profile_load() { - local profile="$1" - - profile_reset - profile_load_builtin "$profile" -} diff --git a/lib/tool_status.sh b/lib/tool_status.sh index a4327fbc..95b82809 100644 --- a/lib/tool_status.sh +++ b/lib/tool_status.sh @@ -95,7 +95,7 @@ tool_status_mise_version() { TOOL_STATUS_MISE_VERSION="" TOOL_STATUS_APPROVED="" if [[ "$TOOL_STATUS_MISE_READY" == 0 ]]; then - # mise.toml owns approved versions; profile records contain only tool names. + # mise.toml owns approved versions; package records contain only tool names. TOOL_STATUS_MISE_APPROVED_VERSIONS="$(awk ' /^\[/ { in_tools = ($0 == "[tools]"); next } in_tools && $2 == "=" { diff --git a/packages.conf b/packages.conf new file mode 100644 index 00000000..1058c2f2 --- /dev/null +++ b/packages.conf @@ -0,0 +1,29 @@ +# Selfishell environment: platform requirement manager package +package macos required formula git +package macos required formula vim +package macos required direct zinit +package macos optional cask font-meslo-lg-nerd-font +package macos optional cask font-noto-sans-cjk-kr +package ubuntu required apt zsh +package ubuntu required apt git +package ubuntu required apt curl +package ubuntu required apt ca-certificates +package ubuntu required apt vim +package ubuntu required direct zinit + +package all required direct mise +package all required mise starship +package all required mise fzf +package all required mise zoxide +package all required mise ripgrep +package all optional mise eza +package all optional mise bat +package all required mise jq +package all required mise neovim +package all required mise tree-sitter +package all required mise node +package all required mise python +package all required mise uv +package all required mise gh + +package ubuntu required apt build-essential diff --git a/profiles/developer.conf b/profiles/developer.conf deleted file mode 100644 index d6fd1a33..00000000 --- a/profiles/developer.conf +++ /dev/null @@ -1,17 +0,0 @@ -include minimal - -package all required direct mise -package all required mise fzf -package all required mise zoxide -package all required mise ripgrep -package all optional mise eza -package all optional mise bat -package all required mise jq -package all required mise neovim -package all required mise tree-sitter -package all required mise node -package all required mise python -package all required mise uv -package all required mise gh - -package ubuntu required apt build-essential diff --git a/profiles/minimal.conf b/profiles/minimal.conf deleted file mode 100644 index b0b9d5da..00000000 --- a/profiles/minimal.conf +++ /dev/null @@ -1,14 +0,0 @@ -# platform requirement manager package -package macos required formula git -package macos required formula starship -package macos required formula vim -package macos required direct zinit -package macos optional cask font-meslo-lg-nerd-font -package macos optional cask font-noto-sans-cjk-kr -package ubuntu required apt zsh -package ubuntu required apt git -package ubuntu required apt curl -package ubuntu required apt ca-certificates -package ubuntu required apt vim -package ubuntu required direct starship -package ubuntu required direct zinit diff --git a/scripts/benchmark.sh b/scripts/benchmark.sh index 3a7f9c44..0ae4f16e 100644 --- a/scripts/benchmark.sh +++ b/scripts/benchmark.sh @@ -18,10 +18,10 @@ Usage: scripts/benchmark.sh [--mode base|full] full Installs the pinned mise, starship, and zinit (with its pinned plugins) into an isolated HOME before measuring, so the - interactive-cached metric reflects a real developer-profile - startup. fzf and zoxide are measured if already on PATH (install - them via the platform package manager before running this mode); - this script does not invoke a package manager itself. + interactive-cached metric reflects a real full-environment + startup. Starship is installed through mise. fzf and zoxide are + measured if already on PATH; this script does not install them + or invoke Apt/Homebrew. SELFISHELL_BENCHMARK_PROFILE=base|full is equivalent to --mode. EOF @@ -63,6 +63,9 @@ esac TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/selfishell-benchmark.XXXXXX")" TEST_HOME="$TEST_ROOT/home" TEST_DATA_HOME="$TEST_HOME/.local/share" +export MISE_DATA_DIR="$TEST_DATA_HOME/mise" +export MISE_CACHE_DIR="$TEST_HOME/.cache/mise" +export MISE_STATE_DIR="$TEST_HOME/.local/state/mise" trap 'rm -rf "$TEST_ROOT"' EXIT mkdir -p "$TEST_HOME/.cache/selfishell" "$TEST_HOME/.config/mise" \ "$TEST_HOME/.config/selfishell/zsh" "$TEST_HOME/.local/bin" "$TEST_DATA_HOME" @@ -93,34 +96,37 @@ date +%s >"$TEST_HOME/.cache/selfishell/update-checked-at" } # Installs the pinned mise/starship/zinit into $TEST_HOME so "full" mode -# measures a real developer-profile startup, not the runner's PATH, reusing -# dependency_install rather than reimplementing it. fzf and zoxide are left to -# the package manager, so provision them separately before --mode full. -install_full_profile_integrations() { - local name status=0 - - for name in mise starship zinit; do - HOME="$TEST_HOME" XDG_DATA_HOME="$TEST_DATA_HOME" XDG_STATE_HOME="$TEST_HOME/.local/state" \ - XDG_CACHE_HOME="$TEST_HOME/.cache" \ - SELFISHELL_ROOT="$ROOT_DIR" \ - bash -c ' - source "$1/lib/common.sh" - source "$1/lib/paths.sh" - source "$1/lib/platform.sh" - source "$1/lib/dependencies.sh" - source "$1/lib/installers.sh" - install_direct_package required "$2" 0 "$(detect_platform)" "$(detect_architecture)" - ' _ "$ROOT_DIR" "$name" || status=1 +# measures a real full-environment startup, not the runner's PATH, reusing +# the production installers. fzf and zoxide are measured from the caller's PATH. +install_full_integrations() ( + local name + export HOME="$TEST_HOME" XDG_CONFIG_HOME="$TEST_HOME/.config" + export XDG_DATA_HOME="$TEST_DATA_HOME" XDG_STATE_HOME="$TEST_HOME/.local/state" + export XDG_CACHE_HOME="$TEST_HOME/.cache" SELFISHELL_ROOT="$ROOT_DIR" + cd "$TEST_HOME" + source "$ROOT_DIR/lib/common.sh" + source "$ROOT_DIR/lib/paths.sh" + source "$ROOT_DIR/lib/platform.sh" + source "$ROOT_DIR/lib/dependencies.sh" + source "$ROOT_DIR/lib/installers.sh" + + for name in mise zinit; do + install_direct_package required "$name" 0 "$(detect_platform)" "$(detect_architecture)" || return done - ((status == 0)) || { - printf 'Failed to provision one or more full-profile integrations (mise/starship/zinit)\n' >&2 - exit 1 - } -} + # Activate only Starship in this benchmark, using the release's exact pin. + # Do not let the runtime implicitly install the rest of the development tools. + awk ' + BEGIN { print "[tools]" } + /^\[/ { in_tools = ($0 == "[tools]"); next } + in_tools && $1 == "starship" { print; found = 1 } + END { print "\n[settings]\nnot_found_auto_install = false"; exit !found } + ' "$ROOT_DIR/config/shared/mise.toml" >"$TEST_HOME/.config/mise/config.toml" + PATH="$TEST_HOME/.local/bin:$PATH" install_mise_tools required 0 starship +) if [[ "$PROFILE_MODE" == full ]]; then - install_full_profile_integrations + install_full_integrations fi case "$PROFILE_MODE" in @@ -240,7 +246,12 @@ describe_integrations() { local summary="Interactive integrations:" for integration in starship fzf zoxide; do - if PATH="$INTERACTIVE_PATH" command -v "$integration" >/dev/null 2>&1; then + if [[ "$PROFILE_MODE" == full && "$integration" == starship ]] && + HOME="$TEST_HOME" XDG_CONFIG_HOME="$TEST_HOME/.config" \ + MISE_GLOBAL_CONFIG_FILE="$TEST_HOME/.config/mise/config.toml" \ + "$TEST_HOME/.local/bin/mise" which starship >/dev/null 2>&1; then + status=enabled + elif PATH="$INTERACTIVE_PATH" command -v "$integration" >/dev/null 2>&1; then status=enabled else status=absent diff --git a/scripts/build-release.sh b/scripts/build-release.sh index 43bfd1d7..3b57815d 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -54,7 +54,7 @@ mkdir -p "$payload_dir" "$output_dir" cp -R \ "$ROOT_DIR/bin" \ "$ROOT_DIR/lib" \ - "$ROOT_DIR/profiles" \ + "$ROOT_DIR/packages.conf" \ "$ROOT_DIR/config" \ "$payload_dir/" cp "$ROOT_DIR/dependencies.conf" "$payload_dir/" diff --git a/scripts/macos-configuration-e2e.sh b/scripts/macos-configuration-e2e.sh index 8d09d196..40aa8e2c 100644 --- a/scripts/macos-configuration-e2e.sh +++ b/scripts/macos-configuration-e2e.sh @@ -80,7 +80,7 @@ run_primary_lifecycle() { SELFISHELL_RELEASE_ROOT="file://$RELEASE_ROOT" \ bash "$ROOT_DIR/install.sh" --version "$INITIAL_VERSION" --prefix "$prefix" \ - --setup --yes --profile minimal --skip-packages + --setup --yes --skip-packages # --- clean install --- [[ -f "$HOME/.zshrc" && ! -L "$HOME/.zshrc" ]] || fail "install did not leave .zshrc as a regular, user-owned file" @@ -115,7 +115,7 @@ run_primary_lifecycle() { # --- idempotent reinstall --- SELFISHELL_RELEASE_ROOT="file://$RELEASE_ROOT" \ - "$prefix/bin/selfishell" install --profile minimal --skip-packages --yes >/dev/null + "$prefix/bin/selfishell" install --skip-packages --yes >/dev/null loader_count="$(grep -Fc '# >>> Selfishell initialize >>>' "$HOME/.zshrc")" [[ "$loader_count" == 1 ]] || fail "a second install duplicated the loader block (found $loader_count)" vimrc_block_count="$(grep -Fc '" >>> Selfishell vimrc >>>' "$HOME/.vimrc")" @@ -127,8 +127,8 @@ run_primary_lifecycle() { # --- status --- local status_output status_output="$("$prefix/bin/selfishell" status)" || true - printf '%s\n' "$status_output" | grep -Fq '[INFO] Installed profile: minimal' || - fail "status did not report the installed profile" + printf '%s\n' "$status_output" | grep -Fq '[INFO] Selfishell configuration is installed.' || + fail "status did not report the installed configuration" assert_managed_resources_clean "$prefix" "on a clean install" # --- configuration update --- @@ -173,7 +173,7 @@ run_ghostty_preflight_check() { SELFISHELL_RELEASE_ROOT="file://$RELEASE_ROOT" \ bash "$ROOT_DIR/install.sh" --version "$INITIAL_VERSION" --prefix "$prefix" \ - --setup --yes --profile minimal --skip-packages + --setup --yes --skip-packages [[ "$(<"$XDG_STATE_HOME/selfishell/ghostty")" == 1 ]] || fail "Ghostty did not default to enabled on a clean --yes install" @@ -200,7 +200,7 @@ run_purge_lifecycle() { SELFISHELL_RELEASE_ROOT="file://$RELEASE_ROOT" \ bash "$ROOT_DIR/install.sh" --version "$INITIAL_VERSION" --prefix "$prefix" \ - --setup --yes --profile minimal --skip-packages + --setup --yes --skip-packages [[ -e "$prefix/bin/selfishell" ]] || fail "bootstrap did not install the CLI link" diff --git a/scripts/neovim-e2e.sh b/scripts/neovim-e2e.sh index 62608d2e..dccda375 100644 --- a/scripts/neovim-e2e.sh +++ b/scripts/neovim-e2e.sh @@ -85,7 +85,7 @@ if zsh_path="$(command -v zsh 2>/dev/null)"; then export SHELL="$zsh_path" fi -bash "$ROOT_DIR/bin/selfishell" install --profile developer --skip-packages --yes >/dev/null +bash "$ROOT_DIR/bin/selfishell" install --skip-packages --yes >/dev/null verify_mise_global_config_ownership diff --git a/scripts/ubuntu-container-e2e.sh b/scripts/ubuntu-container-e2e.sh index f59c7e33..8f4f8308 100755 --- a/scripts/ubuntu-container-e2e.sh +++ b/scripts/ubuntu-container-e2e.sh @@ -6,7 +6,13 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/selfishell-container-e2e.XXXXXX")" INITIAL_VERSION=0.0.0-container.1 NEXT_VERSION=0.0.0-container.2 +export HOME="$TEST_ROOT/home" +export XDG_CONFIG_HOME="$HOME/.config" +export XDG_DATA_HOME="$HOME/.local/share" +export XDG_STATE_HOME="$HOME/.local/state" +export XDG_CACHE_HOME="$HOME/.cache" PREFIX="$HOME/.local" +export PATH="$PREFIX/bin:$PATH" RELEASE_ROOT="$TEST_ROOT/releases" cleanup() { @@ -32,6 +38,7 @@ trap cleanup EXIT HUP INT TERM [[ "$(id -u)" == 0 ]] || fail "the Ubuntu container test must run as root" command -v sudo >/dev/null 2>&1 && fail "the test image unexpectedly contains sudo" +mkdir -p "$HOME" publish_fixture "$INITIAL_VERSION" publish_fixture "$NEXT_VERSION" @@ -40,7 +47,7 @@ SELFISHELL_RELEASE_ROOT="file://$RELEASE_ROOT" \ bash "$ROOT_DIR/install.sh" --version "$INITIAL_VERSION" --prefix "$PREFIX" SELFISHELL_RELEASE_ROOT="file://$RELEASE_ROOT" \ - "$PREFIX/bin/selfishell" install --profile minimal --yes + "$PREFIX/bin/selfishell" install --yes "$PREFIX/bin/selfishell" status >/dev/null "$PREFIX/bin/selfishell" doctor >/dev/null diff --git a/scripts/update-dependencies.sh b/scripts/update-dependencies.sh index e9343df6..12a798ea 100755 --- a/scripts/update-dependencies.sh +++ b/scripts/update-dependencies.sh @@ -50,9 +50,9 @@ record_download() { } discover_metadata() { - local starship_tag starship_version mise_tag mise_version asset_architecture + local mise_tag mise_version asset_architecture local type name source - local repository tag commit asset + local repository tag commit command -v curl >/dev/null 2>&1 || { printf 'curl is required.\n' >&2 @@ -81,21 +81,6 @@ discover_metadata() { printf '%s %s %s\n' "$type" "$name" "$commit" >>"$metadata" done <"$manifest" - starship_tag="$(github_latest_tag starship/starship)" - starship_version="${starship_tag#v}" - for platform in linux macos; do - for architecture in amd64 arm64; do - case "$platform:$architecture" in - linux:amd64) asset=starship-x86_64-unknown-linux-gnu.tar.gz ;; - linux:arm64) asset=starship-aarch64-unknown-linux-musl.tar.gz ;; - macos:amd64) asset=starship-x86_64-apple-darwin.tar.gz ;; - macos:arm64) asset=starship-aarch64-apple-darwin.tar.gz ;; - esac - source="https://github.com/starship/starship/releases/download/$starship_tag/$asset" - record_download starship "$starship_version" "$platform" "$architecture" "$source" - done - done - mise_tag="$(github_latest_tag jdx/mise)" mise_version="${mise_tag#v}" for platform in linux macos; do @@ -107,7 +92,7 @@ discover_metadata() { done local tool repository candidate_tag - for tool in fzf zoxide ripgrep eza bat jq neovim tree-sitter uv gh; do + for tool in starship fzf zoxide ripgrep eza bat jq neovim tree-sitter uv gh; do repository="$(mise_tool_repository "$tool")" candidate_tag="$(github_latest_tag "$repository")" candidate_tag="${candidate_tag#v}" @@ -120,6 +105,7 @@ discover_metadata() { # manual because their release lines need a policy choice, not a latest tag. mise_tool_repository() { case "$1" in + starship) printf 'starship/starship\n' ;; fzf) printf 'junegunn/fzf\n' ;; zoxide) printf 'ajeetdsouza/zoxide\n' ;; ripgrep) printf 'BurntSushi/ripgrep\n' ;; diff --git a/tests/cli_test.bash b/tests/cli_test.bash index 5c83550f..182a2dd6 100755 --- a/tests/cli_test.bash +++ b/tests/cli_test.bash @@ -170,12 +170,12 @@ test_doctor_rejects_unsupported_platform() { fail "Doctor should provide an actionable platform message" } -test_doctor_does_not_require_compiler_for_minimal_profile() { +test_doctor_checks_compiler_for_installed_environment() { local output setup_test_home mkdir -p "$HOME/.local/state/selfishell" "$TEST_ROOT/bin" - printf 'minimal\n' >"$HOME/.local/state/selfishell/profile" + printf '1\n' >"$HOME/.local/state/selfishell/configured" printf 'ID=ubuntu\n' >"$TEST_ROOT/os-release" printf 'Linux version 6.8.0\n' >"$TEST_ROOT/proc-version" printf '#!/usr/bin/env bash\nexit 0\n' >"$TEST_ROOT/bin/apt" @@ -192,7 +192,7 @@ test_doctor_does_not_require_compiler_for_minimal_profile() { )" set -e - [[ "$output" != *'C compiler:'* ]] || fail "Minimal profile should not require a C compiler" + [[ "$output" == *'C compiler:'* ]] || fail "Installed environment should check for a C compiler" teardown_test_home } @@ -207,7 +207,7 @@ test_doctor_reports_unprovisioned_zsh_plugins() { plugins_dir="$HOME/.local/share/zinit/plugins" mkdir -p "$HOME/.local/state/selfishell" "$TEST_ROOT/bin" \ "$HOME/.local/share/zinit/zinit.git" - printf 'minimal\n' >"$HOME/.local/state/selfishell/profile" + printf '1\n' >"$HOME/.local/state/selfishell/configured" printf 'ID=ubuntu\n' >"$TEST_ROOT/os-release" printf 'Linux version 6.8.0\n' >"$TEST_ROOT/proc-version" printf '#!/usr/bin/env bash\nexit 0\n' >"$TEST_ROOT/bin/apt" @@ -267,7 +267,7 @@ test_doctor_reports_zinit_plugin_revision_drift() { plugin_dir="$plugins_dir/${repository//\//---}" mkdir -p "$HOME/.local/state/selfishell" "$TEST_ROOT/bin" \ "$HOME/.local/share/zinit/zinit.git" "$plugin_dir" - printf 'minimal\n' >"$HOME/.local/state/selfishell/profile" + printf '1\n' >"$HOME/.local/state/selfishell/configured" printf 'ID=ubuntu\n' >"$TEST_ROOT/os-release" printf 'Linux version 6.8.0\n' >"$TEST_ROOT/proc-version" printf '#!/usr/bin/env bash\nexit 0\n' >"$TEST_ROOT/bin/apt" @@ -316,7 +316,7 @@ test_doctor_reports_dirty_zinit_plugin_checkout() { plugin_dir="$plugins_dir/${repository//\//---}" mkdir -p "$HOME/.local/state/selfishell" "$TEST_ROOT/bin" \ "$HOME/.local/share/zinit/zinit.git" "$plugin_dir" - printf 'minimal\n' >"$HOME/.local/state/selfishell/profile" + printf '1\n' >"$HOME/.local/state/selfishell/configured" printf 'ID=ubuntu\n' >"$TEST_ROOT/os-release" printf 'Linux version 6.8.0\n' >"$TEST_ROOT/proc-version" printf '#!/usr/bin/env bash\nexit 0\n' >"$TEST_ROOT/bin/apt" diff --git a/tests/common_zsh_test.bash b/tests/common_zsh_test.bash index ae455719..82aaef98 100644 --- a/tests/common_zsh_test.bash +++ b/tests/common_zsh_test.bash @@ -5,7 +5,7 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" source "$ROOT_DIR/tests/test_helper.bash" -test_minimal_profile_initializes_git_completion_without_zinit() { +test_git_completion_initializes_without_zinit() { setup_test_home local output @@ -1210,7 +1210,7 @@ EOF teardown_test_home } -test_minimal_profile_keeps_system_vim() { +test_missing_neovim_keeps_system_vim() { local output setup_test_home diff --git a/tests/dependency_updates_test.bash b/tests/dependency_updates_test.bash index 923348ae..e5d9787e 100755 --- a/tests/dependency_updates_test.bash +++ b/tests/dependency_updates_test.bash @@ -41,6 +41,7 @@ write_mise_toml_fixtures() { mkdir -p "$zsh_root/config/shared" cat >"$zsh_root/config/shared/mise.toml" <<'EOF' [tools] +starship = "1.25.0" fzf = "0.74.3" zoxide = "0.9.8" ripgrep = "15.1.0" @@ -87,14 +88,14 @@ test_updates_only_matching_manifest_fields() { write_zsh_root_fixtures "$zsh_root" cat >"$manifest" <"$metadata" <"$manifest" - printf 'mise-tool fzf 0.74.4\n' >"$metadata" + printf 'mise-tool fzf 0.74.4\nmise-tool starship 1.26.0\n' >"$metadata" run_dependency_update "$manifest" "$metadata" "$zsh_root" grep -Fqx 'fzf = "0.74.4"' "$zsh_root/config/shared/mise.toml" || fail "The moved fzf CLI pin was not updated" + grep -Fqx 'starship = "1.26.0"' "$zsh_root/config/shared/mise.toml" || + fail "The moved Starship pin was not updated" + [[ ! -s "$manifest" ]] || fail "mise tool updates changed the direct dependency manifest" } # uv's real upstream history jumps from a 0.5.x pin to 0.12.x: a naive diff --git a/tests/installers_test.bash b/tests/installers_test.bash index 363a9a36..32e76d9e 100644 --- a/tests/installers_test.bash +++ b/tests/installers_test.bash @@ -544,7 +544,7 @@ test_fails_neovim_plugins_when_neovim_is_unavailable() { [[ -z "$GIT_ARGUMENTS" ]] || fail "Missing Neovim still cloned lazy.nvim" [[ ! -e "$lazy_path" ]] || fail "Missing Neovim still prepared lazy.nvim" - [[ "$output" == *'Could not locate Neovim after installing the developer profile.'* ]] || + [[ "$output" == *'Could not locate Neovim after installing the development environment.'* ]] || fail "Missing Neovim failure was not actionable: $output" } diff --git a/tests/lifecycle_e2e_test.bash b/tests/lifecycle_e2e_test.bash index 0978f02d..1bd8a108 100644 --- a/tests/lifecycle_e2e_test.bash +++ b/tests/lifecycle_e2e_test.bash @@ -20,7 +20,7 @@ test_complete_release_lifecycle() { setup_test_home trap teardown_test_home EXIT mkdir -p "$TEST_ROOT/bin" - for tool in starship fzf zoxide rg nvim tree-sitter gcc build-essential; do + for tool in starship mise fzf zoxide rg jq nvim tree-sitter node python uv gh gcc build-essential; do printf '#!/usr/bin/env bash\nexit 0\n' >"$TEST_ROOT/bin/$tool" chmod +x "$TEST_ROOT/bin/$tool" done @@ -59,7 +59,7 @@ test_complete_release_lifecycle() { done bash "$ROOT_DIR/install.sh" --version "$initial_version" --prefix "$prefix" \ - --setup --yes --profile minimal --skip-packages >/dev/null + --setup --yes --skip-packages >/dev/null "$prefix/bin/selfishell" doctor [[ "$("$prefix/bin/selfishell" version)" == "selfishell $initial_version" ]] || fail "Clean install failed" [[ -f "$HOME/.zshrc" && ! -L "$HOME/.zshrc" ]] || fail "Install did not preserve a user-owned .zshrc" diff --git a/tests/managed_install_test.bash b/tests/managed_install_test.bash index 87adcd7b..babcc690 100755 --- a/tests/managed_install_test.bash +++ b/tests/managed_install_test.bash @@ -88,7 +88,7 @@ test_managed_resource_names_are_the_whole_name_column() { test_install_copies_configuration_and_tracks_resources() { printf 'original zshrc' >"$HOME/.zshrc" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null [[ -f "$HOME/.zshrc" && ! -L "$HOME/.zshrc" ]] || fail "Zsh startup file is not user-owned" grep -Fqx '# >>> Selfishell initialize >>>' "$HOME/.zshrc" || fail "Zsh loader start marker is missing" @@ -119,7 +119,7 @@ test_install_copies_configuration_and_tracks_resources() { fail "Zsh loader state version was not recorded" [[ "$(sed -n '2p' "$XDG_STATE_HOME/selfishell/resources/user-zshrc.state")" == block ]] || fail "Zsh loader was not recorded as a managed block" - [[ ! -e "$XDG_CONFIG_HOME/mise/config.toml" ]] || fail "Minimal install created a developer mise config" + [[ -f "$XDG_CONFIG_HOME/mise/config.toml" ]] || fail "Install omitted the user mise config" } test_install_switches_login_shell_to_zsh() { @@ -144,18 +144,18 @@ EOF [[ "$chsh_arguments" == *zsh* ]] || fail "Install did not request a Zsh login shell" } -test_developer_install_includes_neovim_configuration() { +test_install_includes_neovim_configuration() { printf 'original zshrc' >"$HOME/.zshrc" - run_selfishell install --profile developer --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null assert_symlink_to "$XDG_CONFIG_HOME/selfishell/nvim" "$XDG_CONFIG_HOME/nvim" assert_symlink_to "$XDG_CONFIG_HOME/selfishell/mise/selfishell.toml" "$XDG_CONFIG_HOME/mise/conf.d/selfishell.toml" cmp -s "$ROOT_DIR/config/shared/nvim/init.lua" "$XDG_CONFIG_HOME/selfishell/nvim/init.lua" || - fail "Neovim init.lua was not installed for the developer profile" + fail "Neovim init.lua was not installed for the development environment" cmp -s "$ROOT_DIR/config/shared/nvim/lua/config/options.lua" "$XDG_CONFIG_HOME/selfishell/nvim/lua/config/options.lua" || - fail "Neovim options module was not installed for the developer profile" + fail "Neovim options module was not installed for the development environment" cmp -s "$ROOT_DIR/config/shared/nvim/lua/plugins/lsp.lua" "$XDG_CONFIG_HOME/selfishell/nvim/lua/plugins/lsp.lua" || - fail "Neovim lsp plugin was not installed for the developer profile" + fail "Neovim lsp plugin was not installed for the development environment" [[ -f "$XDG_CONFIG_HOME/mise/config.toml" && ! -L "$XDG_CONFIG_HOME/mise/config.toml" ]] || fail "Developer install did not create a user-owned mise config" ! grep -Fqx "$XDG_CONFIG_HOME/mise/config.toml" "$SELFISHELL_RESOURCE_STATE_DIR"/*.state || @@ -167,7 +167,7 @@ test_macos_install_includes_ghostty_configuration() { mkdir -p "$XDG_CONFIG_HOME/ghostty" printf 'font-size = 14\n' >"$XDG_CONFIG_HOME/ghostty/config.ghostty" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null [[ -f "$XDG_CONFIG_HOME/ghostty/config.ghostty" && ! -L "$XDG_CONFIG_HOME/ghostty/config.ghostty" ]] || fail "Ghostty config is not user-owned" @@ -190,7 +190,7 @@ test_ghostty_defaults_include_precedes_user_override_include() { local defaults_line local override_line - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null defaults_line="$(grep -nFx "config-file = $XDG_CONFIG_HOME/selfishell/ghostty/config.ghostty" "$target" | cut -d: -f1)" override_line="$(grep -nFx 'config-file = ?user.ghostty' "$target" | cut -d: -f1)" @@ -207,13 +207,13 @@ test_managed_block_state_identity_mismatch_is_rejected() { local before_content local status - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null before_content="$(<"$target")" # case A: state type is a different valid type (file), well-formed otherwise. printf '2\nfile\nactive\n%s\n-\n-\nbogus-checksum\n' "$target" >"$state_file" set +e - run_selfishell install --profile minimal --skip-packages --yes >/dev/null 2>"$TEST_ROOT/stderr" + run_selfishell install --skip-packages --yes >/dev/null 2>"$TEST_ROOT/stderr" status=$? set -e [[ "$status" -eq 1 ]] || fail "A state type mismatch should stop installation" @@ -225,7 +225,7 @@ test_managed_block_state_identity_mismatch_is_rejected() { # case B: state type matches (block) but the recorded target does not. printf '2\nblock\nactive\n%s.other\n-\n-\nbogus-checksum\n' "$target" >"$state_file" set +e - run_selfishell install --profile minimal --skip-packages --yes >/dev/null 2>"$TEST_ROOT/stderr" + run_selfishell install --skip-packages --yes >/dev/null 2>"$TEST_ROOT/stderr" status=$? set -e [[ "$status" -eq 1 ]] || fail "A state target mismatch should stop installation" @@ -243,7 +243,7 @@ test_outdated_ghostty_block_is_upgraded_without_changing_user_bytes() { local expected="$TEST_ROOT/expected-ghostty" local old_body_checksum - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null cat >"$target" <"$state_file" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null cmp -s "$expected" "$target" || fail "Untouched outdated Ghostty block was not upgraded in place" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null } test_outdated_zprofile_block_is_upgraded_without_changing_user_bytes() { @@ -280,7 +280,7 @@ test_outdated_zprofile_block_is_upgraded_without_changing_user_bytes() { local old_block="$TEST_ROOT/old-zprofile-block" local old_checksum - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null cat >"$old_block" <<'EOF' # >>> Selfishell mise shims >>> @@ -307,7 +307,7 @@ EOF printf '2\nblock\nactive\n%s\n-\n-\n%s\n' \ "$target" "$old_checksum" >"$state_file" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null cmp -s "$expected" "$target" || fail "Untouched outdated .zprofile block was not upgraded in place" @@ -319,7 +319,7 @@ test_modified_zprofile_block_can_be_backed_up_and_overwritten_once() { local modified="$TEST_ROOT/modified-zprofile" local backup - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null printf 'export AFTER=1\n' >>"$target" cp "$target" "$expected" sed 's/command mise activate zsh/command mise activate --user-edited zsh/' \ @@ -327,7 +327,7 @@ test_modified_zprofile_block_can_be_backed_up_and_overwritten_once() { cp "$modified" "$target" printf 'y\ny\n' | SELFISHELL_TEST_TTY=1 \ - run_selfishell install --profile minimal --skip-packages >"$TEST_ROOT/stdout" + run_selfishell install --skip-packages >"$TEST_ROOT/stdout" cmp -s "$expected" "$target" || fail "Accepted block overwrite changed user bytes outside the block" @@ -345,14 +345,14 @@ test_modified_zprofile_block_can_be_skipped_and_install_continues() { local saved_state="$TEST_ROOT/user-zprofile.state" local status=0 - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null sed 's/command mise activate zsh/command mise activate --user-edited zsh/' \ "$target" >"$expected" cp "$expected" "$target" cp "$state_file" "$saved_state" printf 'y\nn\n' | SELFISHELL_TEST_TTY=1 \ - run_selfishell install --profile minimal --skip-packages >"$TEST_ROOT/stdout" || status=$? + run_selfishell install --skip-packages >"$TEST_ROOT/stdout" || status=$? [[ "$status" -eq 0 ]] || fail "Skipping a modified block stopped installation" cmp -s "$expected" "$target" || fail "Skipped block was changed" @@ -370,13 +370,13 @@ test_modified_zprofile_block_is_preserved_with_yes_flag() { local saved_state="$TEST_ROOT/user-zprofile.state" local status=0 - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null sed 's/command mise activate zsh/command mise activate --user-edited zsh/' \ "$target" >"$expected" cp "$expected" "$target" cp "$state_file" "$saved_state" - run_selfishell install --profile minimal --skip-packages --yes \ + run_selfishell install --skip-packages --yes \ >/dev/null 2>"$TEST_ROOT/stderr" || status=$? [[ "$status" -ne 0 ]] || fail "--yes silently overwrote a modified block" @@ -394,7 +394,7 @@ test_modified_block_backup_failure_preserves_target_and_state() { local fake_bin="$TEST_ROOT/fakebin" local status=0 - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null sed 's/command mise activate zsh/command mise activate --user-edited zsh/' \ "$target" >"$expected" cp "$expected" "$target" @@ -410,7 +410,7 @@ EOF chmod +x "$fake_bin/cp" printf 'y\ny\n' | PATH="$fake_bin:/usr/bin:/bin" SELFISHELL_TEST_TTY=1 \ - run_selfishell install --profile minimal --skip-packages \ + run_selfishell install --skip-packages \ >"$TEST_ROOT/stdout" 2>"$TEST_ROOT/stderr" || status=$? [[ "$status" -ne 0 ]] || fail "A failed block backup was reported as success" @@ -428,7 +428,7 @@ test_modified_block_replace_failure_is_retryable() { local fake_bin="$TEST_ROOT/fakebin" local status=0 - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null cp "$target" "$clean" sed 's/command mise activate zsh/command mise activate --user-edited zsh/' \ "$target" >"$modified" @@ -444,7 +444,7 @@ EOF chmod +x "$fake_bin/mv" printf 'y\ny\n' | PATH="$fake_bin:/usr/bin:/bin" SELFISHELL_TEST_TTY=1 \ - run_selfishell install --profile minimal --skip-packages \ + run_selfishell install --skip-packages \ >"$TEST_ROOT/stdout" 2>"$TEST_ROOT/stderr" || status=$? [[ "$status" -ne 0 ]] || fail "A failed block replacement was reported as success" @@ -456,7 +456,7 @@ EOF ! grep -Fq 'Updated Selfishell block:' "$TEST_ROOT/stdout" || fail "A failed block replacement printed update success" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null cmp -s "$clean" "$target" || fail "Retry did not finish the block replacement" [[ "$(sed -n '3p' "$state_file")" == active ]] || fail "Retry did not activate block state" @@ -467,7 +467,7 @@ test_macos_install_reuses_declined_ghostty_choice() { mkdir -p "$XDG_STATE_HOME/selfishell" printf '0\n' >"$XDG_STATE_HOME/selfishell/ghostty" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null [[ ! -e "$XDG_CONFIG_HOME/selfishell/ghostty/config.ghostty" ]] || fail "A saved declined Ghostty choice was ignored" @@ -490,13 +490,13 @@ test_user_ghostty_changes_survive_reinstall_and_uninstall_exactly() { printf '\ncursor-style = bar\n' >"$suffix" cat "$prefix" "$target" "$suffix" >"$expected" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null cat "$prefix" "$target" >"$modified" mv "$modified" "$target" cat "$suffix" >>"$target" run_selfishell update --tools-only --dry-run >/dev/null - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null run_selfishell uninstall --yes >/dev/null cmp -s "$expected" "$target" || fail "Uninstall changed user Ghostty config bytes outside the block" @@ -514,7 +514,7 @@ test_ghostty_config_symlink_is_rejected_without_changes() { ln -s "$dotfiles_source" "$target" set +e - run_selfishell install --profile minimal --skip-packages --yes >/dev/null 2>"$TEST_ROOT/stderr" + run_selfishell install --skip-packages --yes >/dev/null 2>"$TEST_ROOT/stderr" status=$? set -e @@ -535,7 +535,7 @@ test_ghostty_config_directory_is_rejected_without_changes() { mkdir -p "$target/keep" set +e - run_selfishell install --profile minimal --skip-packages --yes >/dev/null 2>"$TEST_ROOT/stderr" + run_selfishell install --skip-packages --yes >/dev/null 2>"$TEST_ROOT/stderr" status=$? set -e @@ -551,7 +551,7 @@ test_user_ghostty_absent_does_not_block_install() { export SELFISHELL_TEST_SYSTEM_NAME=Darwin local user_override="$XDG_CONFIG_HOME/ghostty/user.ghostty" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null [[ -f "$XDG_CONFIG_HOME/ghostty/config.ghostty" ]] || fail "Ghostty config.ghostty was not created" [[ ! -e "$user_override" ]] || fail "Selfishell must not create user.ghostty" @@ -566,11 +566,11 @@ test_user_ghostty_regular_file_is_preserved_across_lifecycle() { printf 'theme = Catppuccin Mocha\nfont-size = 15\n' >"$user_override" checksum_before="$(fixture_sha256 "$user_override")" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null [[ "$(fixture_sha256 "$user_override")" == "$checksum_before" ]] || fail "Install changed user.ghostty" - setup_fake_macos_minimal_packages + setup_fake_macos_packages run_selfishell update --tools-only --yes >/dev/null [[ "$(fixture_sha256 "$user_override")" == "$checksum_before" ]] || fail "Update changed user.ghostty" @@ -590,10 +590,10 @@ test_user_ghostty_symlink_is_untouched_across_lifecycle() { printf 'cursor-style = bar\n' >"$dotfiles_source" ln -s "$dotfiles_source" "$user_override" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null assert_symlink_to "$dotfiles_source" "$user_override" - setup_fake_macos_minimal_packages + setup_fake_macos_packages run_selfishell update --tools-only --yes >/dev/null assert_symlink_to "$dotfiles_source" "$user_override" @@ -608,7 +608,7 @@ test_status_does_not_inspect_user_ghostty() { local before_output before_status local after_output after_status - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null set +e before_output="$(run_selfishell status)" @@ -648,8 +648,8 @@ test_mise_shims_zprofile_survives_full_lifecycle() { local output printf 'export USER_ZPROFILE=kept' >"$HOME/.zprofile" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null [[ "$(grep -Fc '# >>> Selfishell mise shims >>>' "$HOME/.zprofile")" -eq 1 ]] || fail "Install did not add exactly one mise shims block" @@ -673,7 +673,7 @@ EOF test_mise_shims_zprofile_uses_selfishell_mise_outside_path() { local output - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null mkdir -p "$HOME/.local/bin" cat >"$HOME/.local/bin/mise" <<'EOF' #!/usr/bin/env bash @@ -767,7 +767,7 @@ test_zshenv_user_content_survives_ubuntu_lifecycle() { # shellcheck disable=SC2016 # Literal for zsh to expand at its own startup, not now. printf '. "$HOME/.cargo/env"\n' >"$HOME/.zshenv" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null [[ -f "$HOME/.zshenv" && ! -L "$HOME/.zshenv" ]] || fail "Zsh env file is not user-owned" grep -Fqx '# >>> Selfishell zshenv >>>' "$HOME/.zshenv" || fail "Zshenv block start marker is missing" @@ -778,7 +778,7 @@ test_zshenv_user_content_survives_ubuntu_lifecycle() { fail "Zshenv block marker appears more than once" [[ "$(sed -n '2p' "$state_file")" == block ]] || fail "Zshenv resource was not recorded as a managed block" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null [[ "$(grep -Fc '# >>> Selfishell zshenv >>>' "$HOME/.zshenv")" -eq 1 ]] || fail "Reinstall duplicated the Zshenv block marker" @@ -821,7 +821,7 @@ test_macos_lifecycle_never_touches_existing_zshenv() { # shellcheck disable=SC2016 # Literal for zsh to expand at its own startup, not now. printf '. "$HOME/.cargo/env"\n' >"$HOME/.zshenv" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null run_selfishell update --tools-only --skip-packages --yes >/dev/null run_selfishell uninstall --yes >/dev/null @@ -836,7 +836,7 @@ test_vimrc_user_content_survives_lifecycle() { printf 'set background=dark\nset nocompatible\n' >"$HOME/.vimrc" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null [[ -f "$HOME/.vimrc" && ! -L "$HOME/.vimrc" ]] || fail "Vim startup file is not user-owned" grep -Fqx '" >>> Selfishell vimrc >>>' "$HOME/.vimrc" || fail "Vim block start marker is missing" @@ -846,7 +846,7 @@ test_vimrc_user_content_survives_lifecycle() { fail "Vim block marker appears more than once" [[ "$(sed -n '2p' "$state_file")" == block ]] || fail "Vim resource was not recorded as a managed block" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null [[ "$(grep -Fc '" >>> Selfishell vimrc >>>' "$HOME/.vimrc")" -eq 1 ]] || fail "Reinstall duplicated the Vim block marker" @@ -879,7 +879,7 @@ test_unrelated_vimrc_symlink_is_rejected_without_changes() { } test_malformed_managed_file_state_variants_are_rejected_without_changes() { - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null local target="$XDG_CONFIG_HOME/selfishell/vim/vimrc" local state_file="$XDG_STATE_HOME/selfishell/resources/vimrc.state" @@ -911,7 +911,7 @@ test_malformed_managed_file_state_variants_are_rejected_without_changes() { esac set +e - run_selfishell install --profile minimal --skip-packages --yes >/dev/null 2>"$TEST_ROOT/stderr" + run_selfishell install --skip-packages --yes >/dev/null 2>"$TEST_ROOT/stderr" status=$? set -e @@ -1103,19 +1103,19 @@ test_managed_file_replaced_by_same_content_symlink_is_preserved() { done } -test_minimal_profile_keeps_retained_developer_configuration_visible() { +test_status_checks_neovim_configuration_after_reinstall() { local target="$XDG_CONFIG_HOME/selfishell/nvim/init.lua" local output rc=0 - run_selfishell install --profile developer --skip-packages --yes >/dev/null - run_selfishell install --profile minimal --skip-packages --yes >/dev/null - assert_file_content minimal "$SELFISHELL_STATE_DIR/profile" + run_selfishell install --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null + assert_file_content 1 "$SELFISHELL_STATE_DIR/configured" assert_symlink_to "$XDG_CONFIG_HOME/selfishell/nvim" "$XDG_CONFIG_HOME/nvim" assert_symlink_to "$XDG_CONFIG_HOME/selfishell/mise/selfishell.toml" "$XDG_CONFIG_HOME/mise/conf.d/selfishell.toml" printf '\n-- personal edit\n' >>"$target" output="$(run_selfishell status 2>&1)" || rc=$? - ((rc != 0)) || fail "Status ignored modified retained developer configuration" + ((rc != 0)) || fail "Status ignored modified Neovim configuration" [[ "$output" == *"[CHANGED] $target"* ]] || fail "Status omitted retained Neovim configuration: $output" } @@ -1133,7 +1133,7 @@ test_status_uses_current_resource_list() { test_uninstall_restores_original_files() { printf 'original zshrc' >"$HOME/.zshrc" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null run_selfishell uninstall --restore --yes >/dev/null assert_file_content 'original zshrc' "$HOME/.zshrc" @@ -1198,7 +1198,7 @@ EOF fail "Failed managed file removal was not preserved" [[ -r "$XDG_STATE_HOME/selfishell/resources/zshrc-config.state" ]] || fail "Failed managed resource state was removed" - assert_file_content 'developer' "$XDG_STATE_HOME/selfishell/profile" + assert_file_content '1' "$XDG_STATE_HOME/selfishell/configured" } test_uninstall_removes_ghostty_block_before_ghostty_defaults() { @@ -1210,7 +1210,7 @@ test_uninstall_removes_ghostty_block_before_ghostty_defaults() { local defaults_state="$XDG_STATE_HOME/selfishell/resources/ghostty-config.state" local status - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null [[ -f "$defaults_target" ]] || fail "Ghostty defaults were not installed" grep -Fqx '# >>> Selfishell ghostty >>>' "$block_target" || fail "Ghostty block was not installed" @@ -1328,7 +1328,7 @@ test_install_does_not_depend_on_checkout() { local release_root="$TEST_ROOT/release" mkdir -p "$release_root" - cp -R "$ROOT_DIR/bin" "$ROOT_DIR/lib" "$ROOT_DIR/profiles" "$ROOT_DIR/config" "$release_root/" + cp -R "$ROOT_DIR/bin" "$ROOT_DIR/lib" "$ROOT_DIR/packages.conf" "$ROOT_DIR/config" "$release_root/" printf '0.0.0-test.1\n' >"$release_root/VERSION" cp "$ROOT_DIR/dependencies.conf" "$release_root/" @@ -1352,7 +1352,7 @@ test_mise_config_global_preserves_existing_types() { source "$ROOT_DIR/lib/common.sh" source "$ROOT_DIR/lib/commands/install.sh" - # Exercise the create-once boundary directly; the developer/minimal + # Exercise the create-once boundary directly; the configuration # installation tests above cover command wiring. mkdir -p "$XDG_CONFIG_HOME/mise" printf 'user_owned_data_content_bytes\n' >"$XDG_CONFIG_HOME/mise/config.toml" @@ -1391,11 +1391,11 @@ test_mise_config_global_idempotency_and_status() { mkdir -p "$XDG_CONFIG_HOME/mise" printf 'pre-existing user config\n' >"$XDG_CONFIG_HOME/mise/config.toml" - run_selfishell install --profile developer --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null assert_file_content 'pre-existing user config' "$XDG_CONFIG_HOME/mise/config.toml" cp "$XDG_CONFIG_HOME/selfishell/mise/selfishell.toml" "$TEST_ROOT/defaults.before" printf 'modified by user 123\n' >"$XDG_CONFIG_HOME/mise/config.toml" - run_selfishell install --profile developer --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null assert_file_content 'modified by user 123' "$XDG_CONFIG_HOME/mise/config.toml" local status_out @@ -1410,18 +1410,18 @@ test_mise_config_global_idempotency_and_status() { } test_mise_config_global_uninstall_preservation() { - run_selfishell install --profile developer --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null run_selfishell uninstall --restore --yes >/dev/null [[ -f "$XDG_CONFIG_HOME/mise/config.toml" ]] || fail "config.toml should remain after uninstall" } test_mise_config_global_dry_run_and_directory_error() { - run_selfishell install --profile developer --skip-packages --dry-run --yes >/dev/null + run_selfishell install --skip-packages --dry-run --yes >/dev/null [[ ! -e "$XDG_CONFIG_HOME/mise/config.toml" ]] || fail "dry-run created config.toml" mkdir -p "$XDG_CONFIG_HOME/mise/config.toml" local rc=0 - run_selfishell install --profile developer --skip-packages --yes >/dev/null 2>&1 || rc=$? + run_selfishell install --skip-packages --yes >/dev/null 2>&1 || rc=$? ((rc != 0)) || fail "install did not return error when config.toml is a directory" [[ -d "$XDG_CONFIG_HOME/mise/config.toml" ]] || fail "invalid existing directory was changed" @@ -1470,7 +1470,7 @@ EOF )" || fail "runtime created MISE_GLOBAL_CONFIG_FILE" } -# A real `update` reaches packages_install_profile, which must not touch the +# A real `update` reaches packages_install, which must not touch the # network or need root here. Faking apt-get/dpkg satisfies the apt check # without sudo, and pre-creating the direct dependency targets makes # dependency_install treat them as present. Works on either CI runner. @@ -1493,17 +1493,20 @@ zinit() { EOF cat >"$TEST_ROOT/bin/git" <"$TEST_ROOT/bin/apt-get" chmod +x "$TEST_ROOT/bin/apt-get" @@ -1516,9 +1519,33 @@ setup_fake_minimal_packages() { printf '#!/usr/bin/env bash\nexit 0\n' >"$HOME/.local/bin/starship" chmod +x "$HOME/.local/bin/starship" setup_fake_zinit + setup_fake_editor_tools } -setup_fake_macos_minimal_packages() { +setup_fake_editor_tools() { + local type repository revision source plugin_dir tool + mkdir -p "$HOME/.local/bin" "$TEST_ROOT/bin" + for tool in mise nvim; do + printf '#!/usr/bin/env bash\nexit 0\n' >"$HOME/.local/bin/$tool" + chmod +x "$HOME/.local/bin/$tool" + ln -s "$HOME/.local/bin/$tool" "$TEST_ROOT/bin/$tool" + done + # These lifecycle tests exercise managed files; model already-synced editors + # without downloading tools or plugin repositories. + while read -r type repository revision _ _ source _; do + [[ "$type" == nvim-plugin ]] || continue + if [[ "$repository" == folke/lazy.nvim ]]; then + plugin_dir="$HOME/.local/share/selfishell/nvim/lazy/lazy.nvim" + else + source="${source##*/}" + plugin_dir="$HOME/.local/share/nvim/lazy/${source%.git}" + fi + mkdir -p "$plugin_dir/.git" + printf '%s\n' "$revision" >"$plugin_dir/.git/selfishell-approved-revision" + done <"$ROOT_DIR/dependencies.conf" +} + +setup_fake_macos_packages() { mkdir -p "$TEST_ROOT/bin" cat >"$TEST_ROOT/bin/brew" <<'EOF' #!/usr/bin/env bash @@ -1532,6 +1559,7 @@ EOF chmod +x "$TEST_ROOT/bin/brew" setup_fake_zinit + setup_fake_editor_tools } # Use a private checkout to simulate release changes without modifying the test source. @@ -1539,7 +1567,7 @@ build_release_copy() { local release_root="$1" mkdir -p "$release_root" - cp -R "$ROOT_DIR/bin" "$ROOT_DIR/lib" "$ROOT_DIR/profiles" "$ROOT_DIR/config" "$release_root/" + cp -R "$ROOT_DIR/bin" "$ROOT_DIR/lib" "$ROOT_DIR/packages.conf" "$ROOT_DIR/config" "$release_root/" printf '0.0.0-test.1\n' >"$release_root/VERSION" cp "$ROOT_DIR/dependencies.conf" "$release_root/dependencies.conf" } @@ -1550,7 +1578,7 @@ test_reinstall_preserves_tool_caches_until_generator_configuration_changes() { local scenario tool build_release_copy "$release_root" - bash "$release_root/bin/selfishell" install --profile minimal --skip-packages --yes >/dev/null + bash "$release_root/bin/selfishell" install --skip-packages --yes >/dev/null mkdir -p "$cache_dir" for tool in zoxide fzf starship; do printf '# cached %s init\n' "$tool" >"$cache_dir/$tool-init.zsh" @@ -1562,9 +1590,9 @@ test_reinstall_preserves_tool_caches_until_generator_configuration_changes() { dry-run) printf '\n# updated generator\n' >>"$release_root/config/shared/zsh/interactive.zsh" ;; esac if [[ "$scenario" == dry-run ]]; then - bash "$release_root/bin/selfishell" install --profile minimal --skip-packages --yes --dry-run >/dev/null + bash "$release_root/bin/selfishell" install --skip-packages --yes --dry-run >/dev/null else - bash "$release_root/bin/selfishell" install --profile minimal --skip-packages --yes >/dev/null + bash "$release_root/bin/selfishell" install --skip-packages --yes >/dev/null fi for tool in zoxide fzf starship; do if [[ "$scenario" == changed ]]; then @@ -1578,7 +1606,7 @@ test_reinstall_preserves_tool_caches_until_generator_configuration_changes() { } test_managed_file_interactive_overwrite_yes() { - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null local target_file="$XDG_CONFIG_HOME/selfishell/vim/vimrc" printf 'user_modified_data\n' >"$target_file" @@ -1587,7 +1615,7 @@ test_managed_file_interactive_overwrite_yes() { # resource loop remaps it; the second answers the conflict prompt, read from # FD 3, the copy of stdin taken before that remap. local stdout - stdout="$(printf 'y\ny\n' | SELFISHELL_TEST_TTY=1 run_selfishell install --profile minimal --skip-packages)" + stdout="$(printf 'y\ny\n' | SELFISHELL_TEST_TTY=1 run_selfishell install --skip-packages)" cmp -s "$ROOT_DIR/config/shared/vimrc" "$target_file" || fail "Modified managed file was not overwritten with the default" @@ -1604,7 +1632,7 @@ test_managed_file_interactive_overwrite_yes() { } test_managed_file_interactive_skip_preserves_state_and_continues() { - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null local completion_target="$XDG_CONFIG_HOME/selfishell/zsh/completion.zsh" local completion_state="$XDG_STATE_HOME/selfishell/resources/zsh-completion.state" @@ -1614,7 +1642,7 @@ test_managed_file_interactive_skip_preserves_state_and_continues() { cp "$completion_state" "$saved_state" local rc=0 - printf 'y\nn\n' | SELFISHELL_TEST_TTY=1 run_selfishell install --profile minimal --skip-packages >/dev/null || rc=$? + printf 'y\nn\n' | SELFISHELL_TEST_TTY=1 run_selfishell install --skip-packages >/dev/null || rc=$? ((rc == 0)) || fail "Install failed after skipping a modified managed file (exit code $rc)" assert_file_content 'user_modified_completion' "$completion_target" @@ -1626,7 +1654,7 @@ test_managed_file_interactive_skip_preserves_state_and_continues() { } test_managed_file_yes_flag_preserves_modification() { - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null local target_file="$XDG_CONFIG_HOME/selfishell/vim/vimrc" local state_file="$XDG_STATE_HOME/selfishell/resources/vimrc.state" @@ -1636,7 +1664,7 @@ test_managed_file_yes_flag_preserves_modification() { cp "$state_file" "$saved_state" local rc=0 - run_selfishell install --profile minimal --skip-packages --yes >/dev/null 2>"$TEST_ROOT/stderr" || rc=$? + run_selfishell install --skip-packages --yes >/dev/null 2>"$TEST_ROOT/stderr" || rc=$? ((rc != 0)) || fail "--yes must not silently overwrite a modified managed file" assert_file_content 'user_modified_data' "$target_file" @@ -1648,7 +1676,7 @@ test_managed_file_yes_flag_preserves_modification() { } test_managed_link_conflict_still_aborts() { - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null local link_path="$XDG_CONFIG_HOME/starship.toml" local state_file="$XDG_STATE_HOME/selfishell/resources/user-starship.state" @@ -1660,7 +1688,7 @@ test_managed_link_conflict_still_aborts() { printf 'replaced_by_user\n' >"$link_path" local rc=0 - run_selfishell install --profile minimal --skip-packages --yes >/dev/null 2>"$TEST_ROOT/stderr" || rc=$? + run_selfishell install --skip-packages --yes >/dev/null 2>"$TEST_ROOT/stderr" || rc=$? ((rc != 0)) || fail "A replaced managed link must still abort installation" assert_file_content 'replaced_by_user' "$link_path" @@ -1670,7 +1698,7 @@ test_managed_link_conflict_still_aborts() { } test_managed_link_creation_failure_does_not_report_success_and_is_retryable() { - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null local link_path="$XDG_CONFIG_HOME/starship.toml" local state_file="$XDG_STATE_HOME/selfishell/resources/user-starship.state" @@ -1680,7 +1708,7 @@ test_managed_link_creation_failure_does_not_report_success_and_is_retryable() { rm "$link_path" chmod 0555 "$XDG_CONFIG_HOME" - run_selfishell install --profile minimal --skip-packages --yes >"$TEST_ROOT/stdout" 2>"$TEST_ROOT/stderr" || rc=$? + run_selfishell install --skip-packages --yes >"$TEST_ROOT/stdout" 2>"$TEST_ROOT/stderr" || rc=$? chmod 0755 "$XDG_CONFIG_HOME" ((rc != 0)) || fail "A symlink creation failure must not be reported as success" @@ -1689,7 +1717,7 @@ test_managed_link_creation_failure_does_not_report_success_and_is_retryable() { [[ "$(sed -n '3p' "$state_file")" == pending ]] || fail "A failed link creation must not be recorded as active" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null assert_symlink_to "$XDG_CONFIG_HOME/selfishell/starship.toml" "$link_path" [[ "$(sed -n '3p' "$state_file")" == active ]] || fail "Retrying after a fixed permission error did not recover" @@ -1801,7 +1829,7 @@ EOF } test_managed_file_overwrite_conflict_atomic_copy_failure_preserves_backup_and_state() { - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null local target_file="$XDG_CONFIG_HOME/selfishell/vim/vimrc" local state_file="$XDG_STATE_HOME/selfishell/resources/vimrc.state" @@ -1826,7 +1854,7 @@ EOF chmod +x "$fake_bin/cp" set +e - printf 'y\ny\n' | PATH="$fake_bin:/usr/bin:/bin" SELFISHELL_TEST_TTY=1 run_selfishell install --profile minimal --skip-packages >/dev/null 2>"$TEST_ROOT/stderr" + printf 'y\ny\n' | PATH="$fake_bin:/usr/bin:/bin" SELFISHELL_TEST_TTY=1 run_selfishell install --skip-packages >/dev/null 2>"$TEST_ROOT/stderr" status=$? set -e @@ -1842,7 +1870,7 @@ EOF ! grep -Fq 'Installed managed file' "$TEST_ROOT/stderr" || fail "A failed overwrite must not report success" - printf 'y\ny\n' | SELFISHELL_TEST_TTY=1 run_selfishell install --profile minimal --skip-packages >/dev/null + printf 'y\ny\n' | SELFISHELL_TEST_TTY=1 run_selfishell install --skip-packages >/dev/null cmp -s "$ROOT_DIR/config/shared/vimrc" "$target_file" || fail "Retrying after removing the forced failure did not recover" } @@ -1870,7 +1898,7 @@ EOF chmod +x "$fake_bin/ln" set +e - PATH="$fake_bin:/usr/bin:/bin" run_selfishell install --profile minimal --skip-packages --yes >"$TEST_ROOT/stdout" 2>"$TEST_ROOT/stderr" + PATH="$fake_bin:/usr/bin:/bin" run_selfishell install --skip-packages --yes >"$TEST_ROOT/stdout" 2>"$TEST_ROOT/stderr" status=$? set -e @@ -1883,7 +1911,7 @@ EOF ! grep -Fq "Linked: $link_path" "$TEST_ROOT/stdout" || fail "A failed link creation printed a success message" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null assert_symlink_to "$XDG_CONFIG_HOME/selfishell/starship.toml" "$link_path" } @@ -1898,7 +1926,7 @@ test_ghostty_preflight_stops_before_other_resources_install() { ln -s "$dotfiles_source" "$target" set +e - run_selfishell install --profile minimal --skip-packages --yes >"$TEST_ROOT/stdout" 2>"$TEST_ROOT/stderr" + run_selfishell install --skip-packages --yes >"$TEST_ROOT/stdout" 2>"$TEST_ROOT/stderr" status=$? set -e @@ -1921,7 +1949,7 @@ test_update_ghostty_preflight_stops_before_other_resources_change() { local before_vimrc_state local status - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null mkdir -p "$(dirname "$dotfiles_source")" printf 'font-size = 14\n' >"$dotfiles_source" @@ -1954,7 +1982,7 @@ test_update_zshenv_preflight_stops_before_other_resources_change() { local before_vimrc_state local status - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null rm -f "$HOME/.zshenv" ln -s "$TEST_ROOT/dotfiles-zshenv" "$HOME/.zshenv" @@ -1983,7 +2011,7 @@ test_update_ghostty_preflight_rejects_directory_before_other_resources_change() local before_zshrc local status - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null rm -f "$target" mkdir -p "$target/keep" @@ -2205,31 +2233,31 @@ test_block_remove_truncation_failure_preserves_block_and_state() { test_install_final_state_write_failure_does_not_report_success() { local rc=0 - local before_profile + local before_configured local before_ghostty local tmp_count - run_selfishell install --profile minimal --skip-packages --yes >/dev/null - before_profile="$(<"$XDG_STATE_HOME/selfishell/profile")" + run_selfishell install --skip-packages --yes >/dev/null + before_configured="$(<"$XDG_STATE_HOME/selfishell/configured")" before_ghostty="$(<"$XDG_STATE_HOME/selfishell/ghostty")" chmod 0555 "$XDG_STATE_HOME/selfishell" - run_selfishell install --profile minimal --skip-packages --yes >"$TEST_ROOT/stdout" 2>"$TEST_ROOT/stderr" || rc=$? + run_selfishell install --skip-packages --yes >"$TEST_ROOT/stdout" 2>"$TEST_ROOT/stderr" || rc=$? chmod 0755 "$XDG_STATE_HOME/selfishell" ((rc != 0)) || fail "A final state write failure must not be reported as success" ! grep -Fq 'Selfishell configuration installed.' "$TEST_ROOT/stdout" || fail "A failed install printed the success message" - tmp_count="$(find "$XDG_STATE_HOME/selfishell" -maxdepth 1 \( -name 'profile.tmp.*' -o -name 'ghostty.tmp.*' \) | wc -l)" + tmp_count="$(find "$XDG_STATE_HOME/selfishell" -maxdepth 1 \( -name 'configured.tmp.*' -o -name 'ghostty.tmp.*' \) | wc -l)" [[ "$tmp_count" -eq 0 ]] || fail "A failed final state write left a temporary file behind" - [[ "$(<"$XDG_STATE_HOME/selfishell/profile")" == "$before_profile" ]] || - fail "A failed final state write must not corrupt the existing profile state" + [[ "$(<"$XDG_STATE_HOME/selfishell/configured")" == "$before_configured" ]] || + fail "A failed final state write must not corrupt the existing configuration marker" [[ "$(<"$XDG_STATE_HOME/selfishell/ghostty")" == "$before_ghostty" ]] || fail "A failed final state write must not corrupt the existing ghostty state" } test_managed_file_dry_run_conflict_changes_nothing() { - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null local target_file="$XDG_CONFIG_HOME/selfishell/vim/vimrc" local state_file="$XDG_STATE_HOME/selfishell/resources/vimrc.state" @@ -2259,7 +2287,7 @@ test_original_backup_survives_overwrite_and_uninstall_restore() { mkdir -p "$(dirname "$target_file")" printf 'original-before-install\n' >"$target_file" - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null local state_file="$XDG_STATE_HOME/selfishell/resources/vimrc.state" local original_backup @@ -2268,7 +2296,7 @@ test_original_backup_survives_overwrite_and_uninstall_restore() { assert_file_content 'original-before-install' "$original_backup" printf 'user-modification-after-install\n' >"$target_file" - printf 'y\ny\n' | SELFISHELL_TEST_TTY=1 run_selfishell install --profile minimal --skip-packages >/dev/null + printf 'y\ny\n' | SELFISHELL_TEST_TTY=1 run_selfishell install --skip-packages >/dev/null [[ "$(sed -n '6p' "$state_file")" == "$original_backup" ]] || fail "Overwriting a conflict must keep the original installation backup" @@ -2287,9 +2315,9 @@ test_update_tools_only_overwrites_modified_managed_file() { local release_root="$TEST_ROOT/release" build_release_copy "$release_root" - setup_fake_minimal_packages + setup_fake_packages - bash "$release_root/bin/selfishell" install --profile minimal --skip-packages --yes >/dev/null + bash "$release_root/bin/selfishell" install --skip-packages --yes >/dev/null local target_file="$XDG_CONFIG_HOME/selfishell/vim/vimrc" printf 'user_modified_vimrc\n' >"$target_file" @@ -2316,9 +2344,9 @@ test_update_tools_only_skips_modified_managed_file_and_continues() { local release_root="$TEST_ROOT/release" build_release_copy "$release_root" - setup_fake_minimal_packages + setup_fake_packages - bash "$release_root/bin/selfishell" install --profile minimal --skip-packages --yes >/dev/null + bash "$release_root/bin/selfishell" install --skip-packages --yes >/dev/null local completion_target="$XDG_CONFIG_HOME/selfishell/zsh/completion.zsh" local completion_state="$XDG_STATE_HOME/selfishell/resources/zsh-completion.state" @@ -2339,8 +2367,8 @@ test_update_tools_only_skips_modified_managed_file_and_continues() { } test_update_tools_only_yes_preserves_modified_file() { - setup_fake_minimal_packages - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + setup_fake_packages + run_selfishell install --skip-packages --yes >/dev/null local target_file="$XDG_CONFIG_HOME/selfishell/vim/vimrc" local state_file="$XDG_STATE_HOME/selfishell/resources/vimrc.state" @@ -2362,7 +2390,7 @@ test_update_tools_only_yes_preserves_modified_file() { } test_tools_only_update_reports_its_own_result_without_a_version_transition() { - run_selfishell install --profile minimal --skip-packages --yes >/dev/null + run_selfishell install --skip-packages --yes >/dev/null run_selfishell update --tools-only --skip-packages --yes >"$TEST_ROOT/stdout" 2>&1 diff --git a/tests/profiles_test.bash b/tests/package_manifest_test.bash old mode 100755 new mode 100644 similarity index 51% rename from tests/profiles_test.bash rename to tests/package_manifest_test.bash index 40530e0e..ed61fd04 --- a/tests/profiles_test.bash +++ b/tests/package_manifest_test.bash @@ -6,7 +6,7 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" source "$ROOT_DIR/tests/test_helper.bash" -setup_profile_home() { +setup_package_home() { setup_test_home export XDG_CONFIG_HOME="$HOME/.config" export XDG_STATE_HOME="$HOME/.local/state" @@ -18,136 +18,117 @@ setup_profile_home() { printf 'Linux version 6.8.0\n' >"$SELFISHELL_TEST_PROC_VERSION_FILE" } -teardown_profile_home() { +teardown_package_home() { unset XDG_CONFIG_HOME XDG_STATE_HOME unset SELFISHELL_TEST_SYSTEM_NAME SELFISHELL_TEST_MACHINE_ARCH unset SELFISHELL_TEST_OS_RELEASE_FILE SELFISHELL_TEST_PROC_VERSION_FILE teardown_test_home } -run_profile_dry_run() { +run_package_dry_run() { local output - output="$(bash "$ROOT_DIR/bin/selfishell" install --profile "$1" --dry-run)" + output="$(bash "$ROOT_DIR/bin/selfishell" install --dry-run)" printf '%s\n' "$output" | awk '/^Would (install .* (apt packages|Homebrew)|sync .* (direct package|mise tools))/' } -assert_minimal_foundation() { +assert_core_tools() { local output="$1" printf '%s\n' "$output" | grep -Eq '(^|[[:space:]])zsh([[:space:]]|$)' || - fail "Profile omitted the core Zsh environment" + fail "Environment omitted the core Zsh environment" printf '%s\n' "$output" | grep -Eq '(^|[[:space:]])vim([[:space:]]|$)' || - fail "Profile omitted Vim" - [[ "$output" == *'direct package: starship'* ]] || fail "Profile omitted Starship" - [[ "$output" == *'direct package: zinit'* ]] || fail "Profile omitted Zinit" + fail "Environment omitted Vim" + printf '%s\n' "$output" | grep -Eq '^Would sync required mise tools:.* starship([[:space:]]|$)' || + fail "Environment omitted mise-managed Starship" + [[ "$output" != *'direct package: starship'* ]] || fail "Starship still uses the direct installer" + [[ "$output" == *'direct package: zinit'* ]] || fail "Environment omitted Zinit" } -test_default_profile_is_developer() { +test_install_includes_mise_and_neovim() { local output output="$(bash "$ROOT_DIR/bin/selfishell" install --dry-run)" [[ "$output" == *'direct package: mise'* && "$output" == *'required mise tools:'* ]] || - fail "Default install did not select the developer profile" + fail "Install omitted mise-managed tools" [[ "$output" == *'Neovim plugins'* ]] || - fail "Default install omitted the developer Neovim setup" -} - -test_minimal_excludes_developer_environment() { - local output full_output - output="$(run_profile_dry_run minimal)" - full_output="$(bash "$ROOT_DIR/bin/selfishell" install --profile minimal --dry-run)" - - assert_minimal_foundation "$output" - [[ "$output" != *'direct package: mise'* ]] || fail "Minimal profile included developer runtimes" - [[ "$output" != *'required mise tools:'* ]] || fail "Minimal profile included mise-managed tools" - [[ "$output" != *'build-essential'* ]] || fail "Minimal profile included compiler tooling" - [[ "$full_output" != *'Neovim plugins'* ]] || fail "Minimal profile included Neovim plugin setup" + fail "Install omitted Neovim setup" } -test_developer_includes_development_tools() { +test_manifest_includes_development_tools() { local output full_output macos_output apt_plan homebrew_plan local expected_mise_tools actual_mise_tools required_mise_tools optional_mise_tools - output="$(run_profile_dry_run developer)" - full_output="$(bash "$ROOT_DIR/bin/selfishell" install --profile developer --dry-run)" + output="$(run_package_dry_run)" + full_output="$(bash "$ROOT_DIR/bin/selfishell" install --dry-run)" # Compare membership against the independently parsed mise.toml; order is irrelevant. expected_mise_tools="$(awk ' /^\[/ { in_tools = ($0 == "[tools]"); next } in_tools && NF >= 3 { print $1 } ' "$ROOT_DIR/config/shared/mise.toml" | sort)" actual_mise_tools="$(printf '%s\n' "$output" | - sed -n 's/^Would sync \(required\|optional\) mise tools: //p' | tr ' ' '\n' | sort)" + sed -E -n 's/^Would sync (required|optional) mise tools: //p' | tr ' ' '\n' | sort)" required_mise_tools="$(printf '%s\n' "$output" | sed -n 's/^Would sync required mise tools: //p' | tr ' ' '\n' | sort)" optional_mise_tools="$(printf '%s\n' "$output" | sed -n 's/^Would sync optional mise tools: //p' | tr ' ' '\n' | sort)" - assert_minimal_foundation "$output" + assert_core_tools "$output" [[ "$output" == *'direct package: mise'* ]] || - fail "Developer profile is missing development tools" - [[ -n "$actual_mise_tools" ]] || fail "Developer profile did not report required mise tools" + fail "Environment is missing development tools" + [[ -n "$actual_mise_tools" ]] || fail "Environment did not report required mise tools" [[ "$actual_mise_tools" == "$expected_mise_tools" ]] || - fail "Developer profile mise tools do not match config/shared/mise.toml (expected: $expected_mise_tools; got: $actual_mise_tools)" - [[ "$required_mise_tools" == $'fzf\ngh\njq\nneovim\nnode\npython\nripgrep\ntree-sitter\nuv\nzoxide' ]] || - fail "Developer profile required mise tools are incorrect: $required_mise_tools" + fail "Environment mise tools do not match config/shared/mise.toml (expected: $expected_mise_tools; got: $actual_mise_tools)" + [[ "$required_mise_tools" == $'fzf\ngh\njq\nneovim\nnode\npython\nripgrep\nstarship\ntree-sitter\nuv\nzoxide' ]] || + fail "Environment required mise tools are incorrect: $required_mise_tools" [[ "$optional_mise_tools" == $'bat\neza' ]] || - fail "Developer profile optional mise tools are incorrect: $optional_mise_tools" + fail "Environment optional mise tools are incorrect: $optional_mise_tools" apt_plan="$(printf '%s\n' "$output" | grep 'apt packages:' || true)" - for tool in fzf zoxide ripgrep eza bat jq; do + for tool in starship fzf zoxide ripgrep eza bat jq; do ! grep -Eq "(^|[[:space:]])$tool([[:space:]]|$)" <<<"$apt_plan" || fail "Developer CLI tool remained in the Apt install plan: $tool" done export SELFISHELL_TEST_SYSTEM_NAME=Darwin - macos_output="$(run_profile_dry_run developer)" + macos_output="$(run_package_dry_run)" + printf '%s\n' "$macos_output" | grep -Eq '^Would sync required mise tools:.* starship([[:space:]]|$)' || + fail "macOS install omitted mise-managed Starship" homebrew_plan="$(printf '%s\n' "$macos_output" | grep 'Homebrew formula' || true)" - for tool in fzf zoxide ripgrep eza bat jq; do + for tool in starship fzf zoxide ripgrep eza bat jq; do ! grep -Eq "(^|[[:space:]])$tool([[:space:]]|$)" <<<"$homebrew_plan" || fail "Developer CLI tool remained in the Homebrew install plan: $tool" done - [[ "$full_output" == *'Neovim plugins'* ]] || fail "Developer profile is missing Neovim plugin setup" + [[ "$full_output" == *'Neovim plugins'* ]] || fail "Environment is missing Neovim plugin setup" } -test_minimal_macos_includes_fonts_and_opt_in_ghostty() { +test_macos_includes_fonts_and_opt_in_ghostty() { local output export SELFISHELL_TEST_SYSTEM_NAME=Darwin - output="$(bash "$ROOT_DIR/bin/selfishell" install --profile minimal --dry-run)" + output="$(bash "$ROOT_DIR/bin/selfishell" install --dry-run)" [[ "$output" == *'optional Homebrew cask:'* && "$output" == *'font-'* ]] || - fail "Minimal macOS profile omitted optional fonts" + fail "macOS environment omitted optional fonts" [[ "$output" == *'optional Homebrew cask: ghostty'* ]] || - fail "Minimal macOS profile omitted opt-in Ghostty" -} - -test_unknown_profile_returns_usage_error() { - local status - - set +e - bash "$ROOT_DIR/bin/selfishell" install --profile unknown --dry-run >/dev/null 2>&1 - status=$? - set -e - - [[ "$status" -eq 2 ]] || fail "Unknown profile should return exit code 2" + fail "macOS environment omitted opt-in Ghostty" } -test_profile_read_file_rejects_option_like_package() { - local profile_file="$TEST_ROOT/profile-record.conf" +test_package_manifest_read_rejects_option_like_package() { + local manifest_file="$TEST_ROOT/package-record.conf" local status - # profile_read_file() is the shared parser built-in profiles use too; + # package_manifest_read() validates the declarative package list; # exercise it directly rather than through a CLI entry point. - printf 'package ubuntu required apt --allow-unauthenticated\n' >"$profile_file" + printf 'package ubuntu required apt --allow-unauthenticated\n' >"$manifest_file" set +e bash -c ' source "$1/lib/common.sh" - source "$1/lib/profiles.sh" - profile_read_file "$2" - ' _ "$ROOT_DIR" "$profile_file" >/dev/null 2>&1 + source "$1/lib/package_manifest.sh" + package_manifest_read "$2" + ' _ "$ROOT_DIR" "$manifest_file" >/dev/null 2>&1 status=$? set -e [[ "$status" -eq 2 ]] || fail "Option-like package name should be rejected" } -run_discovered_tests setup_profile_home teardown_profile_home +run_discovered_tests setup_package_home teardown_package_home diff --git a/tests/release_bootstrap_test.bash b/tests/release_bootstrap_test.bash index 4012241a..026647f7 100755 --- a/tests/release_bootstrap_test.bash +++ b/tests/release_bootstrap_test.bash @@ -263,7 +263,7 @@ test_status_does_not_use_network() { version="$RELEASE_FIXTURE_VERSION" run_bootstrap --version "$version" >/dev/null "$TEST_ROOT/prefix/bin/selfishell" \ - install --profile minimal --skip-packages --yes >/dev/null + install --skip-packages --yes >/dev/null mkdir -p "$fake_bin" cat >"$fake_bin/curl" <<'EOF' #!/usr/bin/env bash @@ -705,7 +705,7 @@ test_default_update_reports_up_to_date_without_synchronizing() { fail "Default update at the latest release did not report up to date: $output" [[ "$output" != *'already at'* ]] || fail "Default update printed the CLI-only wording as well as the up-to-date message: $output" - # No profile is installed here, so update_tools_and_configuration would have + # No configuration is installed here, so update_tools_and_configuration would have # printed this had it run: its absence proves the phase was skipped, not just # that the summary line was suppressed. [[ "$output" != *'skipping tools and configuration'* ]] || @@ -951,7 +951,7 @@ test_setup_is_explicit_and_can_skip_packages() { [[ -f "$HOME/.zshrc" && ! -L "$HOME/.zshrc" ]] || fail "Setup did not create a user-owned .zshrc" grep -Fqx '# >>> Selfishell initialize >>>' "$HOME/.zshrc" || fail "Setup did not add the Zsh loader" - assert_file_content 'developer' "$XDG_STATE_HOME/selfishell/profile" + assert_file_content '1' "$XDG_STATE_HOME/selfishell/configured" } test_missing_bin_path_prints_actionable_message() { diff --git a/tests/tool_status_test.bash b/tests/tool_status_test.bash index 0ed585dd..d2897ee7 100644 --- a/tests/tool_status_test.bash +++ b/tests/tool_status_test.bash @@ -172,7 +172,7 @@ EOF printf 'node 24.18.0\npython 3.13.14 3.12.0\n' >"$HOME/mise-inventory" } -# Approved mise versions come from mise.toml, not the profile's bare tool names. +# Approved mise versions come from mise.toml, not the manifest's bare tool names. test_detects_mise_tool_version() { setup_mise_inventory diff --git a/tests/updates_test.bash b/tests/updates_test.bash index 7916c25a..ad3f245b 100644 --- a/tests/updates_test.bash +++ b/tests/updates_test.bash @@ -37,24 +37,24 @@ run_dependency_install() { ' _ "$ROOT_DIR" "$dependency" } -test_tools_update_synchronizes_profile_packages() { +test_tools_update_synchronizes_packages() { local output export XDG_CONFIG_HOME="$HOME/.config" mkdir -p "$XDG_STATE_HOME/selfishell" - printf 'minimal\n' >"$XDG_STATE_HOME/selfishell/profile" + printf '1\n' >"$XDG_STATE_HOME/selfishell/configured" output="$(bash "$ROOT_DIR/bin/selfishell" update --tools-only --dry-run)" [[ "$output" == *'Would install required apt packages:'* ]] || fail "Tools update did not synchronize package-manager packages" - [[ "$output" == *'git'* ]] || fail "Tools update did not include the current profile packages" - [[ "$output" != *'Neovim plugins'* ]] || fail "Minimal tools update included Neovim plugin setup" + [[ "$output" == *'git'* ]] || fail "Tools update did not include the environment packages" + [[ "$output" == *'Neovim plugins'* ]] || fail "Tools update omitted Neovim plugin setup" } test_tools_update_skip_packages_avoids_package_operations() { local output export XDG_CONFIG_HOME="$HOME/.config" mkdir -p "$XDG_STATE_HOME/selfishell" - printf 'minimal\n' >"$XDG_STATE_HOME/selfishell/profile" + printf '1\n' >"$XDG_STATE_HOME/selfishell/configured" output="$(bash "$ROOT_DIR/bin/selfishell" update --tools-only --skip-packages --dry-run)" [[ "$output" == *'Skipping package and tool installation.'* ]] || From c183fd0d9cfa117ae21ef816bb7d711596b1ef5f Mon Sep 17 00:00:00 2001 From: jiminu Date: Mon, 14 Sep 2026 23:24:50 +0900 Subject: [PATCH 3/3] fix: verify installed mise tools and isolate managed operations --- AGENTS.md | 4 +++ docs/PERFORMANCE.md | 9 +++---- lib/installers.sh | 10 +++++--- lib/tool_status.sh | 25 +++++++++++++------ scripts/benchmark.sh | 44 ++++++++++++++++++++++++--------- scripts/ubuntu-container-e2e.sh | 31 +++++++++++++++++++++++ tests/installers_test.bash | 13 ++++++++++ tests/tool_status_test.bash | 26 ++++++++++++++++--- 8 files changed, 129 insertions(+), 33 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 347fae18..5fa6ffc4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -112,6 +112,10 @@ in the state directory records completed setup, not a package selection. Mise-managed tool membership is declared in `packages.conf`; exact versions for those tools are pinned in `config/shared/mise.toml`, the source of truth for mise-managed tool versions. +Installer-owned mise operations run from the release's `config/shared` +directory so a caller's project cannot override approved tool versions. +Status and doctor query installed versions, not merely configured requests; +an orphaned mise shim does not count as an external tool installation. When the tools/configuration phase runs, `--skip-packages` must skip package and tool installation and apply managed configuration only. A default update diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index a663c74d..9e1750cf 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -37,7 +37,7 @@ checkout. ### Full-environment mode -Full mode additionally provisions the pinned mise, Starship, and Zinit -- with +Full mode additionally provisions the pinned mise, Starship, fzf, zoxide, and Zinit -- with its pinned Zsh plugins -- into the benchmark's own isolated `HOME`, via the same code path the real installer uses, so `interactive-cached` reflects a real full-environment startup rather than whatever happens to already be on @@ -47,11 +47,8 @@ the runner's `PATH`. It: changed; - uses that home as its working directory and gives mise an isolated global config; -- installs the pinned mise, Starship, and Zinit (with its pinned plugins) - into that isolated `HOME`, with Starship installed through mise using the - release's exact pin; -- measures fzf and zoxide only if they are already on `PATH` -- installing - those tools is out of scope for this script, so provision them first; +- installs the pinned mise and Zinit (with its pinned plugins), plus Starship, + fzf, and zoxide through mise using the release's exact pins; - needs network access to provision those tools, so it is not part of the regular (network-free) unit test suite, or run in CI -- run it locally when needed. diff --git a/lib/installers.sh b/lib/installers.sh index 61382691..e0efa88d 100644 --- a/lib/installers.sh +++ b/lib/installers.sh @@ -138,12 +138,14 @@ install_mise_tools() { selfishell_mise_trust + # The release config must win over any mise.toml in the caller's project. if MISE_GLOBAL_CONFIG_FILE="$SELFISHELL_ROOT/config/shared/mise.toml" \ - "$mise_command" -q install --dry-run-code "$@" >/dev/null 2>&1; then + "$mise_command" -C "$SELFISHELL_ROOT/config/shared" -q install --dry-run-code "$@" >/dev/null 2>&1; then return 0 fi - if ! MISE_GLOBAL_CONFIG_FILE="$SELFISHELL_ROOT/config/shared/mise.toml" "$mise_command" install "$@"; then + if ! MISE_GLOBAL_CONFIG_FILE="$SELFISHELL_ROOT/config/shared/mise.toml" \ + "$mise_command" -C "$SELFISHELL_ROOT/config/shared" install "$@"; then if [[ "$requirement" == "optional" ]]; then cli_warn "Could not install $requirement mise tools: $*" SELFISHELL_SKIPPED_OPTIONAL_PACKAGES+=("$@") @@ -326,7 +328,7 @@ selfishell_nvim_command() { if [[ -n "$mise_command" ]]; then resolved="$(MISE_GLOBAL_CONFIG_FILE="$SELFISHELL_ROOT/config/shared/mise.toml" \ - "$mise_command" which nvim 2>/dev/null)" || true + "$mise_command" -C "$SELFISHELL_ROOT/config/shared" which nvim 2>/dev/null)" || true if [[ -n "$resolved" && -x "$resolved" ]]; then printf '%s\n' "$resolved" return 0 @@ -354,7 +356,7 @@ selfishell_run_nvim() { mise_command="$(selfishell_mise_command)" || mise_command="" if [[ -n "$mise_command" ]]; then MISE_GLOBAL_CONFIG_FILE="$SELFISHELL_ROOT/config/shared/mise.toml" \ - "$mise_command" exec -- "$nvim_command" "$@" + "$mise_command" -C "$SELFISHELL_ROOT/config/shared" exec -- "$nvim_command" "$@" else "$nvim_command" "$@" fi diff --git a/lib/tool_status.sh b/lib/tool_status.sh index 95b82809..4caf1fc1 100644 --- a/lib/tool_status.sh +++ b/lib/tool_status.sh @@ -90,7 +90,7 @@ tool_status_executable() { tool_status_mise_version() { local tool="$1" - local mise_command="" name versions + local mise_command="" name versions extra TOOL_STATUS_MISE_VERSION="" TOOL_STATUS_APPROVED="" @@ -109,7 +109,10 @@ tool_status_mise_version() { mise_command="$HOME/.local/bin/mise" fi if [[ -n "$mise_command" ]]; then - TOOL_STATUS_MISE_VERSIONS="$(MISE_GLOBAL_CONFIG_FILE="${SELFISHELL_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/selfishell}/mise/selfishell.toml" "$mise_command" current 2>/dev/null)" || + # `current` also lists configured but uninstalled versions. Query one + # installed-only inventory, independent of the caller's project config. + TOOL_STATUS_MISE_VERSIONS="$(NO_COLOR=1 MISE_GLOBAL_CONFIG_FILE="$SELFISHELL_ROOT/config/shared/mise.toml" \ + "$mise_command" -C "$SELFISHELL_ROOT/config/shared" ls --current --installed --no-header --no-truncate 2>/dev/null)" || TOOL_STATUS_MISE_VERSIONS="" fi TOOL_STATUS_MISE_READY=1 @@ -121,13 +124,12 @@ tool_status_mise_version() { break fi done <<<"$TOOL_STATUS_MISE_APPROVED_VERSIONS" - while read -r name versions; do + while read -r name versions extra; do if [[ "$name" == "$tool" && -n "$versions" ]]; then - TOOL_STATUS_MISE_VERSION="$versions" - return + TOOL_STATUS_MISE_VERSION="${TOOL_STATUS_MISE_VERSION:+$TOOL_STATUS_MISE_VERSION }$versions" fi done <<<"$TOOL_STATUS_MISE_VERSIONS" - return 1 + [[ -n "$TOOL_STATUS_MISE_VERSION" ]] } tool_status_detect() { @@ -135,7 +137,7 @@ tool_status_detect() { local package="$2" local dependency_platform="$3" local architecture="$4" - local output state + local output state executable_path mise_shims TOOL_STATUS_INSTALLED="missing" TOOL_STATUS_SOURCE="none" @@ -202,7 +204,14 @@ tool_status_detect() { ;; esac - if have_command "$(tool_status_executable "$package")"; then + output="$(tool_status_executable "$package")" + if have_command "$output"; then + executable_path="$(command -v "$output")" + # Shims can exist after a tool was removed. They are not external installs. + mise_shims="${MISE_DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/mise}/shims" + if [[ "$manager" == mise && "$executable_path" == "$mise_shims/"* ]]; then + return 0 + fi TOOL_STATUS_INSTALLED="detected" TOOL_STATUS_SOURCE="external" fi diff --git a/scripts/benchmark.sh b/scripts/benchmark.sh index 0ae4f16e..1b129684 100644 --- a/scripts/benchmark.sh +++ b/scripts/benchmark.sh @@ -16,12 +16,11 @@ Usage: scripts/benchmark.sh [--mode base|full] base Selfishell's own startup cost, independent of external integrations (mise/starship/zinit/fzf/zoxide are excluded). This is the default. - full Installs the pinned mise, starship, and zinit (with its pinned + full Installs the pinned mise, starship, fzf, zoxide, and zinit (with its pinned plugins) into an isolated HOME before measuring, so the interactive-cached metric reflects a real full-environment - startup. Starship is installed through mise. fzf and zoxide are - measured if already on PATH; this script does not install them - or invoke Apt/Homebrew. + startup. Starship, fzf, and zoxide are installed through mise. + This script does not invoke Apt/Homebrew. SELFISHELL_BENCHMARK_PROFILE=base|full is equivalent to --mode. EOF @@ -95,9 +94,9 @@ date +%s >"$TEST_HOME/.cache/selfishell/update-checked-at" exit 1 } -# Installs the pinned mise/starship/zinit into $TEST_HOME so "full" mode +# Installs the pinned shell integrations into $TEST_HOME so "full" mode # measures a real full-environment startup, not the runner's PATH, reusing -# the production installers. fzf and zoxide are measured from the caller's PATH. +# the production installers. install_full_integrations() ( local name export HOME="$TEST_HOME" XDG_CONFIG_HOME="$TEST_HOME/.config" @@ -114,15 +113,15 @@ install_full_integrations() ( install_direct_package required "$name" 0 "$(detect_platform)" "$(detect_architecture)" || return done - # Activate only Starship in this benchmark, using the release's exact pin. + # Activate only shell integrations, using the release's exact pins. # Do not let the runtime implicitly install the rest of the development tools. awk ' BEGIN { print "[tools]" } /^\[/ { in_tools = ($0 == "[tools]"); next } - in_tools && $1 == "starship" { print; found = 1 } - END { print "\n[settings]\nnot_found_auto_install = false"; exit !found } + in_tools && ($1 == "starship" || $1 == "fzf" || $1 == "zoxide") { print; found++ } + END { print "\n[settings]\nnot_found_auto_install = false"; exit(found != 3) } ' "$ROOT_DIR/config/shared/mise.toml" >"$TEST_HOME/.config/mise/config.toml" - PATH="$TEST_HOME/.local/bin:$PATH" install_mise_tools required 0 starship + PATH="$TEST_HOME/.local/bin:$PATH" install_mise_tools required 0 starship fzf zoxide ) if [[ "$PROFILE_MODE" == full ]]; then @@ -227,6 +226,26 @@ run_interactive_zsh() { /bin/zsh -d -i -c exit >/dev/null 2>&1 } +verify_full_integrations() { + [[ "$PROFILE_MODE" == full ]] || return 0 + ( + cd "$TEST_HOME" + HOME="$TEST_HOME" ZDOTDIR="$TEST_HOME" XDG_CONFIG_HOME="$TEST_HOME/.config" \ + XDG_DATA_HOME="$TEST_DATA_HOME" XDG_CACHE_HOME="$TEST_HOME/.cache" \ + MISE_GLOBAL_CONFIG_FILE="$TEST_HOME/.config/mise/config.toml" MISE_SHELL='' \ + PATH="$INTERACTIVE_PATH" TERM=xterm-256color MISE_OFFLINE=1 \ + /bin/zsh -d -i -c ' + for tool in starship fzf zoxide; do + [[ "${commands[$tool]}" == "$MISE_DATA_DIR/installs/"* ]] || exit 1 + done + (( $+functions[prompt_starship_precmd] && $+functions[fzf-file-widget] && $+functions[__zoxide_z] )) + ' >/dev/null 2>&1 + ) || { + printf 'Pinned shell integrations did not initialize in the benchmark HOME.\n' >&2 + return 1 + } +} + profile_interactive_zsh() { local profile_status=0 @@ -246,10 +265,10 @@ describe_integrations() { local summary="Interactive integrations:" for integration in starship fzf zoxide; do - if [[ "$PROFILE_MODE" == full && "$integration" == starship ]] && + if [[ "$PROFILE_MODE" == full ]] && HOME="$TEST_HOME" XDG_CONFIG_HOME="$TEST_HOME/.config" \ MISE_GLOBAL_CONFIG_FILE="$TEST_HOME/.config/mise/config.toml" \ - "$TEST_HOME/.local/bin/mise" which starship >/dev/null 2>&1; then + "$TEST_HOME/.local/bin/mise" -C "$TEST_HOME" which "$integration" >/dev/null 2>&1; then status=enabled elif PATH="$INTERACTIVE_PATH" command -v "$integration" >/dev/null 2>&1; then status=enabled @@ -289,6 +308,7 @@ record_result "$common_result" # Warm the complete interactive configuration before measuring it. run_interactive_zsh +verify_full_integrations interactive_result="$(benchmark interactive-cached "$ITERATIONS" bash -c 'run_interactive_zsh')" record_result "$interactive_result" diff --git a/scripts/ubuntu-container-e2e.sh b/scripts/ubuntu-container-e2e.sh index 8f4f8308..7bc7f94c 100755 --- a/scripts/ubuntu-container-e2e.sh +++ b/scripts/ubuntu-container-e2e.sh @@ -46,11 +46,42 @@ publish_fixture "$NEXT_VERSION" SELFISHELL_RELEASE_ROOT="file://$RELEASE_ROOT" \ bash "$ROOT_DIR/install.sh" --version "$INITIAL_VERSION" --prefix "$PREFIX" +# Installer-owned operations must ignore conflicting caller project versions. +mkdir -p "$TEST_ROOT/project" +cat >"$TEST_ROOT/project/mise.toml" <<'EOF' +[tools] +node = "0.0.0" +neovim = "0.0.0" +starship = "0.0.0" +EOF +export MISE_TRUSTED_CONFIG_PATHS="$TEST_ROOT" +cd "$TEST_ROOT/project" + SELFISHELL_RELEASE_ROOT="file://$RELEASE_ROOT" \ "$PREFIX/bin/selfishell" install --yes "$PREFIX/bin/selfishell" status >/dev/null "$PREFIX/bin/selfishell" doctor >/dev/null +# Exercise the installed shell entrypoint, not just package inventory. +( + cd "$HOME" + MISE_OFFLINE=1 zsh -d -i -c ' + for tool in starship fzf zoxide; do + [[ "${commands[$tool]}" == "$XDG_DATA_HOME/mise/installs/"* ]] || exit 1 + done + (( $+functions[prompt_starship_precmd] && $+functions[fzf-file-widget] && $+functions[__zoxide_z] )) + ' +) || fail "Installed Zsh did not initialize mise-managed shell tools" + +# Config and shims survive a removed tool; doctor must still report it missing. +starship_install="$("$PREFIX/bin/mise" -C "$HOME" where starship)" +mv "$starship_install" "$TEST_ROOT/starship-not-installed" +doctor_status=0 +doctor_output="$(PATH="$XDG_DATA_HOME/mise/shims:$PATH" "$PREFIX/bin/selfishell" doctor 2>&1)" || doctor_status=$? +mv "$TEST_ROOT/starship-not-installed" "$starship_install" +[[ "$doctor_status" != 0 && "$doctor_output" == *'Tool: starship is missing (mise)'* ]] || + fail "Doctor accepted configured-but-uninstalled Starship" + vim --not-a-term -c 'if !&number || !&relativenumber | cquit 1 | endif' -c 'q' "$TEST_ROOT/bin/mise" <<'EOF' #!/usr/bin/env bash printf '%s\n' "$*" >>"$HOME/mise-calls" -[[ "$*" == current ]] || exit 1 -[[ "$MISE_GLOBAL_CONFIG_FILE" == "$SELFISHELL_CONFIG_DIR/mise/selfishell.toml" ]] || exit 1 +if [[ "$*" == current || "$*" == "-C $SELFISHELL_ROOT/config/shared current" ]]; then + printf 'gh 2.100.0\n' + exit 0 +elif [[ "$*" != "-C $SELFISHELL_ROOT/config/shared ls --current --installed --no-header --no-truncate" ]]; then + exit 1 +fi +[[ "$MISE_GLOBAL_CONFIG_FILE" == "$SELFISHELL_ROOT/config/shared/mise.toml" ]] || exit 1 cat "$HOME/mise-inventory" [[ ! -f "$HOME/mise-fail" ]] EOF @@ -169,7 +174,22 @@ gh = "2.100.0" [settings] node = "ignored" EOF - printf 'node 24.18.0\npython 3.13.14 3.12.0\n' >"$HOME/mise-inventory" + printf 'node 24.18.0 /config/mise.toml 24.18.0\npython 3.13.14 /config/mise.toml 3.13.14\npython 3.12.0 /config/mise.toml 3.12.0\n' >"$HOME/mise-inventory" +} + +test_configured_but_uninstalled_mise_tool_is_missing() { + setup_mise_inventory + # An existing mise shim does not prove the underlying tool is installed. + export MISE_DATA_DIR="$TEST_ROOT/mise-data" + mkdir -p "$MISE_DATA_DIR/shims" + ln -s "$TEST_ROOT/bin/mise" "$MISE_DATA_DIR/shims/gh" + export PATH="$MISE_DATA_DIR/shims:$PATH" + + tool_status_detect mise gh linux amd64 + [[ "$TOOL_STATUS_INSTALLED" == missing && "$TOOL_STATUS_SOURCE" == none ]] || + fail "Configured but uninstalled tool was reported present: $TOOL_STATUS_INSTALLED ($TOOL_STATUS_SOURCE)" + [[ "$TOOL_STATUS_APPROVED" == 2.100.0 ]] || fail "Missing tool lost its approved version" + unset MISE_DATA_DIR } # Approved mise versions come from mise.toml, not the manifest's bare tool names.