Skip to content

feat(mise): opt-in mise module, per-module tool declarations, nodejs provider=mise - #48

Merged
garygentry merged 6 commits into
mainfrom
feat/mise-module
Sep 27, 2026
Merged

garygentry merged 6 commits into
mainfrom
feat/mise-module

Conversation

@garygentry

@garygentry garygentry commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Summary

Stacked on #47. Review and merge #47 first. This PR's diff is the commits after #47's head.

An opt-in mise module: pinned, checksum-verified CLI tools and language runtimes in user space. Also nodejs.provider: mise. With no content overlay, or without selecting the module, nothing changes: the engine ships no tool list and no versions.

mise module

  • Binary. ~/.local/bin/mise, sudo-free, pinned with modules.mise.version, SHA-256-verified against the release SHASUMS256.txt (installing unverified is refused), atomically replaced, no self-update. A mise from another channel (Homebrew, a distro package) is used as-is and never replaced.
  • Any module can declare tools. Give the module a tools: setting (tool to exact version) and call mise_sync_tools "$DOTFILES_MODULE_NAME" (a new helper). Each module's tools go to ~/.config/mise/conf.d/<module>.toml, and exactly those are installed. A tool set therefore follows whichever profile selects its module. The mise module declares its own modules.mise.tools the same way. When a module is uninstalled or pruned, the mise module removes its file, because prune only undoes files the engine deployed itself.
  • Lockfile (optional). modules.mise.lockfile points at an overlay mise.lock (from mise lock --global --platform …). The engine then owns ~/.config/mise/mise.lock, and every declared set installs with --locked, using checksummed URLs and no GitHub API calls. A version bumped without re-locking fails loudly. Installs are limited to the declared tools, so a user's own mise use -g tools can't break them.
  • Shims for every shell. Managed blocks in ~/.zshenv, the login profile and ~/.bashrc cover ssh host cmd, bash -lc and agent tool shells. Interactive zsh also runs mise activate (skipped for zsh -i -c; turn it off with activate: false). The zsh PATH block puts shims first regardless of which block appears first in the file: modules run in dependency order, so priority alone can't fix block order.
  • settings render to conf.d/dotfiles-settings.toml. Your own ~/.config/mise/config.toml is never touched.

nodejs provider: mise

Declares node@<modules.nodejs.version> (exact) through the helper and checks that mise resolves exactly that version. min_version is still enforced, and npm's global prefix stays ~/.local, so global packages survive a Node switch. verify.sh checks that a login shell's node is mise's. The default is tarball, which is unchanged.

Engine fixes this needed

  • render_template passed the module directory as --module (it expects a name), so .Module was always empty in script-rendered templates. This bug dates from the first commit; nothing used it until now.
  • List and map settings reach scripts in a usable form: one item per line, or sorted key=value lines, instead of Go's map[a:b] formatting. Unit-tested. Nothing consumed the old format.

Measured (Ubuntu 22.04 container)

  • zsh -i -c exit about 45 ms
  • full interactive start about 80–100 ms with mise activate, about 65 ms with activation off
  • mise hook-env adds about 20 ms per prompt on a loaded 2-vCPU VM

Test plan

  • make test-integration-ubuntu: 69 passed, 0 failed. The fixture overlay declares tools from 3 modules (mise, nodejs, and an overlay module) and runs with no GitHub token. It checks:
    • the pinned mise, per-module fragments, and settings rendered as a TOML array
    • the managed lock
    • shims first in non-interactive zsh
    • zsh -c resolving node, fzf and rg at the pinned versions, and sh -lc resolving node
    • the npm prefix, a no-op second run, and a single .zshenv block
    • an uninstalled module's fragment removed while the others stay
  • Drift check by hand: bumping a tool without re-locking fails with No lockfile URL found … (--locked mode).
  • go vet ./... and go test ./... pass, including a new TestSettingToStr_NonScalars.
  • dotfiles validate passes for the engine and the fixture overlay. shellcheck is clean.
  • CI: the Arch integration run.

🤖 Generated with Claude Code

https://claude.ai/code/session_01S29GMkRB43pGsAUBUXwLdT

Review round 2 (adversarial, 2026-09-26): all addressed

  • Bug: switching nodejs.provider from mise back to tarball left mise owning node. The tarball provider now removes conf.d/nodejs.toml, and mise_sync_tools with nothing declared removes the fragment without needing mise.
  • Opt-in invariant: zsh put the shims first, and ran mise activate, whenever a mise shims dir existed. It now depends on a marker the mise module deploys through files:, which uninstall and prune remove. A user's own mise is left alone.
  • macOS: mise created ~/.bash_profile, which hid ~/.profile. The block now goes in ~/.profile always, plus ~/.bash_profile or ~/.bash_login only when one exists.
  • ~/.bashrc block was below distro early-returns. It now goes at the top (new top position in upsert_managed_block).
  • settings.toml.tmpl produced invalid TOML for maps and nulls. Maps now render as inline tables, and nulls are omitted. Validated through mise settings get.
  • Unquoted YAML versions (for example 1.10 becoming 1.1) are documented: versions must be quoted.
  • From the overlay review:
    • A locked tool with no entry for the current platform is skipped with a warning (eza has no macOS build), instead of failing the whole set.
    • Tool specs may carry mise backend options (github:atuinsh/atuin[matching=musl]), and tool=version is split at the last =. The container end-to-end test caught this.

Integration: 76 passed, 0 failed. The fixture now includes an options spec (atuin musl, asserted to run), a switch back from provider mise, .bashrc top placement and the marker.

@garygentry
garygentry changed the base branch from feat/zsh-startup-correctness to main September 27, 2026 16:16
garygentry and others added 5 commits September 27, 2026 16:17
- modules/mise: pinned, SHA-256-verified mise binary at ~/.local/bin/mise (no self-update;
  a mise from another channel is used as-is, never replaced); tools/settings rendered
  from modules.mise.* to ~/.config/mise/conf.d/dotfiles.toml (user config.toml untouched);
  optional overlay lockfile -> --locked install scoped to declared tools (no GitHub API);
  optional github_token_command for this run only; shims on PATH via managed blocks in
  ~/.zshenv, the login profile and ~/.bashrc; engine ships an empty tool list
- nodejs: provider tarball (default, unchanged) | mise (asserts mise provides node,
  enforces min_version, keeps npm prefix ~/.local; verify checks PATH provenance)
- zsh: shims first in the .zshenv PATH block and .zshrc (order-independent of block
  position); mise activate for interactive zsh (not -c; DOTFILES_MISE_ACTIVATE=0 opts out)
- fix render_template: --module got the module DIR, so .Module was always empty
- integration: fixture overlay + 13 assertions (64 passed)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S29GMkRB43pGsAUBUXwLdT
…h scripts

- settingToStr: lists -> one item per line, maps -> sorted key=value lines (was Go's
  map[a:b] formatting, unusable from shell); unit-tested
- lib/helpers.sh: mise_bin + mise_sync_tools NAME [tool@ver...]: declares a module's
  tools in conf.d/<module>.toml and installs exactly those (--locked when the mise module
  manages a lockfile); exact versions only
- mise module: own tools via the helper; settings -> conf.d/dotfiles-settings.toml;
  removes fragments whose owning module is no longer installed (prune only undoes
  engine-deployed files)
- nodejs provider=mise declares node@modules.nodejs.version itself and checks mise
  resolves exactly that version
- integration: overlay fixture module declaring its own tools + fragment GC (69 passed)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S29GMkRB43pGsAUBUXwLdT
…n profiles, TOML

- zsh puts mise shims first / runs mise activate ONLY when the mise module is installed
  (a marker it deploys via files:, removed on uninstall/prune), not whenever a shims dir
  exists — a user's own mise is left alone (opt-in invariant)
- nodejs provider tarball removes conf.d/nodejs.toml: switching back from mise no longer
  leaves mise owning node (two owners); mise_sync_tools with nothing declared removes the
  fragment and needs no mise
- login shells: shims block in ~/.profile always, plus ~/.bash_profile or ~/.bash_login
  when one exists; ~/.bash_profile is never created (it would hide ~/.profile)
- ~/.bashrc block goes at the TOP (upsert_managed_block gains a 'top' position): distro
  .bashrc files return early for non-interactive shells
- settings.toml.tmpl: YAML maps -> TOML inline tables, null -> omitted (was invalid TOML)
- with a lockfile, tools lacking an entry for this platform are skipped with a warning
  (eza has no macOS build) instead of failing the whole set
- docs: tool versions must be quoted (unquoted 1.10 is a YAML float -> 1.1)
- integration: provider switch-back, .bashrc top placement, marker deployed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S29GMkRB43pGsAUBUXwLdT
…musl])

Lets a module pick a release asset, e.g. "github:atuinsh/atuin[matching=musl]" (the gnu
build of atuin 18.22 needs glibc 2.39; Ubuntu 22.04 has 2.35). The lockfile keys tools
without their options, so the per-platform lookup strips them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S29GMkRB43pGsAUBUXwLdT
"github:atuinsh/atuin[matching=musl]" arrived as tool 'github:atuinsh/atuin[matching',
version 'musl]=18.22.0' and was rejected. Versions never contain '=', so split at the last
one. Integration fixture now declares that spec (re-locked) and asserts the binary runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S29GMkRB43pGsAUBUXwLdT
…l Arch)

cmp ships in diffutils, which Arch base doesn't install; verify.sh failed
with 'cmp: command not found' and reported the lock as drifted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S29GMkRB43pGsAUBUXwLdT
@garygentry
garygentry merged commit 6fe65d1 into main Sep 27, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant