Skip to content

feat(system): add [system.defaults] for declarative macOS defaults#10363

Merged
jdx merged 2 commits into
mainfrom
feat/system-defaults
Jun 12, 2026
Merged

feat(system): add [system.defaults] for declarative macOS defaults#10363
jdx merged 2 commits into
mainfrom
feat/system-defaults

Conversation

@jdx

@jdx jdx commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

Extends the experimental [system] bootstrapping section with declarative macOS user defaults, following the same design as [system.packages] (#10326):

[system.defaults.NSGlobalDomain]
KeyRepeat = 2
ApplePressAndHoldEnabled = false

[system.defaults."com.apple.dock"]
autohide = true
tilesize = 48
orientation = "left"
  • Declarative and additive — (domain, key) pairs merge global → local across the config hierarchy; local overrides values, nothing is ever deleted.
  • OS-filtered — inert off-macOS: status/doctor list entries as skipped (nothing silently invisible), install ignores them, so cross-platform configs just work.
  • Manual application only — only mise system install writes anything, with the usual confirmation prompt; --dry-run prints the defaults write commands.
  • Typed values — TOML bool/int/float/string map to -bool/-int/-float/-string. Drift detection via defaults read-type + defaults read; types compare strictly (integer 1true). Unsupported plist shapes (arrays, dicts) warn and are skipped for forward compatibility.
  • No sudo ever (user defaults are per-user). No host-scoped (-currentHost) support.
  • mise system status gains a Domain/Key/Value/Current/State table, --json gains a defaults key, --missing exits 1 on drift; mise doctor reports out-of-sync counts.
  • Explicit package args and --manager scope mise system install to packages only; defaults apply in the bare converge-everything form.
  • After writing, mise reminds the user some apps need a relaunch (e.g. killall Dock) — it deliberately never kills apps itself.

Implementation

  • src/system/defaults.rs — new module (value typing, drift check, apply)
  • src/system/mod.rs[system.defaults] parsing + hierarchy merge; domain level stays raw TOML so malformed/newer shapes warn instead of failing the whole config
  • src/cli/system/{status,install,mod}.rs, src/cli/doctor/mod.rs — CLI/doctor wiring
  • schema/mise.json, docs page docs/system-packages/defaults.md, generated usage/completions

Tests

  • Unit: value conversion, defaults write arg rendering, strict type matching, [system.defaults] parsing (incl. forward-compat shapes)
  • E2E (e2e/cli/test_system_defaults): cross-platform behavior — skipped-not-error on Linux, --missing not triggered by unavailable platform, warnings for unsupported values/malformed domains

🤖 Generated with Claude Code


Note

Medium Risk
Writes real user preferences on macOS and changes mise system install/status/--missing behavior when defaults are configured; scope is limited to explicit install and per-user defaults (no sudo).

Overview
Adds [system.defaults] to mise.toml so macOS preferences can be declared like system packages and converged with mise system install (via defaults write), with mise system status / --missing and mise doctor reporting drift.

New src/system/defaults.rs handles typed bool/int/float/string values, strict drift checks (defaults read-type + read), and apply/dry-run. Config merges (domain, key) pairs across the hierarchy; unsupported TOML shapes warn and skip. Off macOS, entries show as skipped and install is a no-op.

mise system install applies defaults only on the bare converge run (no explicit manager:package args and no --manager). Schema, usage docs, and an e2e test cover cross-platform behavior and malformed config.

Reviewed by Cursor Bugbot for commit 63cfcb8. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Manage macOS system defaults via a new [system.defaults] config and apply them with mise system install.
    • mise system status and mise doctor now report defaults alongside system packages.
  • Documentation

    • Detailed docs and man/help text explaining macOS defaults behavior, usage, and restart tips.
  • Tests

    • Added unit and end-to-end tests covering parsing, status, dry-run, and apply behaviors for system defaults.

Extends the [system] bootstrapping section with declarative macOS user
defaults, following the same semantics as [system.packages]: additive
merge across the config hierarchy, OS-filtered (inert off-macOS), and
only ever applied by an explicit `mise system install`.

[system.defaults."com.apple.dock"]
autohide = true
tilesize = 48

Values map to typed `defaults write` flags (-bool/-int/-float/-string);
drift is detected with `defaults read-type`/`defaults read` and shown
by `mise system status` (set/differs/unset) and `mise doctor`. Types
compare strictly: integer 1 does not satisfy a configured `true`.
Unsupported plist shapes (arrays, dicts) parse fine for forward
compatibility but warn and are skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a4c771a-6fc0-40f1-9048-6b16e9005445

📥 Commits

Reviewing files that changed from the base of the PR and between 7e075ea and 63cfcb8.

📒 Files selected for processing (3)
  • schema/mise.json
  • src/cli/doctor/mod.rs
  • src/system/defaults.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • schema/mise.json
  • src/cli/doctor/mod.rs
  • src/system/defaults.rs

📝 Walkthrough

Walkthrough

This PR introduces macOS system defaults management to mise, allowing users to declare and apply macOS user preferences via a [system.defaults] configuration section. It extends the existing mise system install and mise system status commands to handle defaults alongside system packages, integrates defaults analysis into the doctor command, and provides comprehensive user documentation.

Changes

macOS System Defaults Support

Layer / File(s) Summary
Defaults Core Module
src/system/defaults.rs
Implements DefaultsRequest, DefaultsValue, DefaultsState, and DefaultsStatus types with side-effect-free status reading and apply writing using the defaults command-line tool. Includes availability checks for macOS and unit tests for type conversion and matching semantics.
Config Model and Aggregation
src/system/mod.rs, src/config/config_file/mise_toml.rs
Extends SystemTomlConfig with a defaults: IndexMap<String, toml::Value> field, implements defaults_from_config to aggregate defaults across config files with local-override semantics, and adds a unit test validating TOML parsing.
System Install Command
src/cli/system/install.rs, src/cli/system/mod.rs
Extends SystemInstall::run to collect and apply defaults after package installation via a new apply_defaults helper. Supports user prompts, --dry-run, and --yes flags, with logging of set/skipped/written outcomes.
System Status Command
src/cli/system/status.rs
Updates SystemStatus::run to compute and display both system packages and macOS defaults, rendering separate tables in human-readable mode and combined JSON with per-entry state (set/differs/unset), including cross-platform skipping logic.
Doctor Command Integration
src/cli/doctor/mod.rs
Adds system_defaults reporting to both JSON (doctor -J) and human-readable modes, showing availability, configured count, out-of-sync count, and warnings on check failures.
User Documentation and Help
docs/.vitepress/config.ts, docs/cli/system*, docs/system-packages/defaults.md, docs/system-packages/index.md, man/man1/mise.1, mise.usage.kdl, schema/mise.json, xtasks/fig/src/mise.ts
Comprehensive updates to sidebar navigation, command documentation, new dedicated defaults.md guide, JSON schema, KDL help text, man page entries, and shell completion spec describing [system.defaults] syntax, domain/key/value semantics, macOS-only behavior, and app restart reminders.
End-to-End Testing
e2e/cli/test_system_defaults
New bash test validating mise system status and install with system defaults, including JSON output, --dry-run, --missing, and cross-platform conditional behavior for unsupported or malformed configurations.

Sequence Diagram

sequenceDiagram
  participant User
  participant MiseInstall as mise system install
  participant PkgDriver as system driver
  participant ApplyDefaults as apply_defaults()
  participant DefaultsCmd as defaults write
  User->>MiseInstall: run command
  alt packages configured
    MiseInstall->>PkgDriver: install packages
    PkgDriver-->>MiseInstall: done
  end
  alt defaults configured & no --manager filter
    MiseInstall->>ApplyDefaults: apply_defaults(requests)
    ApplyDefaults->>ApplyDefaults: check current status
    alt user confirmation needed
      ApplyDefaults->>User: prompt for approval
      User-->>ApplyDefaults: yes/no
    end
    alt approved & not dry-run
      ApplyDefaults->>DefaultsCmd: defaults write domain key value
      DefaultsCmd-->>ApplyDefaults: success/error
    end
    ApplyDefaults-->>MiseInstall: log outcomes
  end
  MiseInstall-->>User: done
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • jdx/mise#10326: Both PRs modify the same SystemInstall::run and SystemStatus paths; the retrieved PR refactors package installation while this PR extends it with defaults handling.
  • jdx/mise#10346: Both PRs change src/cli/system/install.rs's SystemInstall::run; the retrieved PR delegates package installation to a driver while this PR adds defaults application on top.

Poem

🐰 I nibble keys and plist dreams,

defaults mapped in careful schemes,
A hop, a write — config gleams,
mise applies your macOS themes,
And the Dock wakes up from dreams.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(system): add [system.defaults] for declarative macOS defaults' clearly and concisely summarizes the main feature being added—support for declarative macOS defaults configuration via a new [system.defaults] section.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extends the experimental [system] bootstrapping section with declarative macOS user defaults under [system.defaults], following the exact same design as [system.packages]. Config entries merge global→local across the hierarchy, mise system install is the only writer (with confirmation and --dry-run), and the feature is fully inert on non-macOS platforms.

  • src/system/defaults.rs \u2014 new module handling value typing (bool/int/float/string), strict drift detection via defaults read-type + defaults read, shell-word\u2013quoted dry-run output, and proper error discrimination between "key not found" and real failures.
  • src/system/mod.rs / install.rs / status.rs / doctor/mod.rs \u2014 wires the new module into all CLI commands; defaults are skipped when explicit package args or --manager are given.
  • Docs, schema, e2e tests \u2014 comprehensive coverage including cross-platform skip behavior, unsupported value type warnings, and malformed domain warnings.

Confidence Score: 5/5

Safe to merge — the feature is opt-in, guarded behind the experimental flag, and only writes to user defaults when explicitly invoked with mise system install.

Previously-flagged bugs (shell-quoting, silent error swallowing) are fixed. Remaining observations are minor edge cases (float epsilon, TOCTOU) unlikely to surface with real macOS defaults values.

No files require special attention.

Important Files Changed

Filename Overview
src/system/defaults.rs New module for macOS defaults drift detection and application; clean design with correct type guards, shell-word quoting, and proper error discrimination on defaults stderr
src/system/mod.rs Adds defaults field to SystemTomlConfig as raw TOML for forward compat; defaults_from_config correctly reverses iteration so local config overrides global
src/cli/system/install.rs Defaults are correctly skipped when explicit packages or --manager are specified; driver::run errors propagate before apply_defaults is attempted
src/cli/system/status.rs Adds defaults table alongside packages table; any_missing correctly not set for unavailable (non-macOS) entries, preserving cross-platform --missing semantics
src/cli/doctor/mod.rs Shared check_system_defaults eliminates the duplication noted in earlier review; JSON and text paths each call it once from their own entry point
src/config/config_file/mise_toml.rs Unit test added for [system.defaults] parsing including forward-compat arrays; exercises all four scalar types
e2e/cli/test_system_defaults E2E covers cross-platform skip, dry-run, unsupported value type warning, malformed domain warning, and empty section — good coverage

Reviews (2): Last reviewed commit: "fix(system): address review feedback for..." | Re-trigger Greptile

Comment thread src/system/defaults.rs
Comment thread src/system/defaults.rs Outdated
Comment thread src/cli/doctor/mod.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cli/doctor/mod.rs`:
- Around line 456-477: When crate::system::defaults::status(&defaults).await
returns Err you currently drop the entire system_defaults section; instead keep
the section visible with available/requested context and still record the
warning: in the Err arm (and the analogous block at 496-513) push the existing
warning to self.warnings as you do now, but return Some(serde_json::json!({
"available": true, "requested": defaults.len(), "out_of_sync":
serde_json::Value::Null })) so callers can see configured count and availability
even when status() fails; locate the match on crate::system::defaults::status,
the Err(err) arm, and the defaults variable to implement this change.

In `@src/cli/system/install.rs`:
- Around line 99-107: The code calls defaults::apply even when stderr is
unattended, allowing writes without consent; before calling defaults::apply
ensure interactive consent or explicit --yes: if !self.dry_run && !self.yes then
require console::user_attended_stderr() and prompt::confirm(msg) — otherwise
return an error instructing the user to pass --yes or run interactively. Update
the block around prompt::confirm, console::user_attended_stderr, and the call to
defaults::apply so defaults::apply only runs when consent was obtained or --yes
was provided.

In `@src/system/defaults.rs`:
- Around line 211-213: The current code calls
String::from_utf8_lossy(&output.stdout).trim().to_string(), which removes
leading/trailing spaces; change this to preserve significant whitespace by
replacing .trim() with a newline-only trim such as
.trim_end_matches('\n').to_string() (or remove trimming entirely) so
String::from_utf8_lossy(&output.stdout).trim_end_matches('\n').to_string()
returns the value without stripping meaningful leading/trailing spaces.
- Line 1: Set a default Rust toolchain (e.g., run `rustup default stable`) and
then rerun `cargo build --all-features`, `mise run lint`, and `mise run
lint-fix` and commit changes; this fixes the CI/build failure. In
src/system/defaults.rs change the output handling in the defaults_cmd function
to avoid stripping intentional whitespace: replace the use of
String::from_utf8_lossy(&output.stdout).trim() with a variant that only removes
trailing newlines (e.g., trimming '\n' or using rtrim_end_matches) so
DefaultsValue::matches (specifically the DefaultsValue::Str branch comparing raw
== s) sees the exact stored value and avoids false Differs/rewrites.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c9da0354-4c49-4cac-89b0-3eef68cbe160

📥 Commits

Reviewing files that changed from the base of the PR and between ac042eb and 7e075ea.

📒 Files selected for processing (18)
  • docs/.vitepress/config.ts
  • docs/cli/system.md
  • docs/cli/system/install.md
  • docs/cli/system/status.md
  • docs/system-packages/defaults.md
  • docs/system-packages/index.md
  • e2e/cli/test_system_defaults
  • man/man1/mise.1
  • mise.usage.kdl
  • schema/mise.json
  • src/cli/doctor/mod.rs
  • src/cli/system/install.rs
  • src/cli/system/mod.rs
  • src/cli/system/status.rs
  • src/config/config_file/mise_toml.rs
  • src/system/defaults.rs
  • src/system/mod.rs
  • xtasks/fig/src/mise.ts

Comment thread src/cli/doctor/mod.rs
Comment thread src/cli/system/install.rs
Comment on lines +99 to +107
if !self.dry_run && !self.yes && console::user_attended_stderr() {
let msg = format!("defaults: write {}?", list.join(", "));
if !crate::ui::prompt::confirm(msg)? {
info!("defaults: skipped");
return Ok(());
}
}
defaults::apply(&targets, self.dry_run).await?;
if !self.dry_run {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Require explicit consent before writing defaults in unattended sessions.

The current guard skips the prompt when stderr is not attended, but still executes defaults::apply, so defaults can be written without confirmation. In non-interactive contexts, require --yes (or skip) to preserve the “write with confirmation” behavior.

Suggested fix
-        if !self.dry_run && !self.yes && console::user_attended_stderr() {
-            let msg = format!("defaults: write {}?", list.join(", "));
-            if !crate::ui::prompt::confirm(msg)? {
-                info!("defaults: skipped");
-                return Ok(());
-            }
-        }
+        if !self.dry_run && !self.yes {
+            if !console::user_attended_stderr() {
+                info!("defaults: skipped (non-interactive; rerun with --yes to apply)");
+                return Ok(());
+            }
+            let msg = format!("defaults: write {}?", list.join(", "));
+            if !crate::ui::prompt::confirm(msg)? {
+                info!("defaults: skipped");
+                return Ok(());
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/system/install.rs` around lines 99 - 107, The code calls
defaults::apply even when stderr is unattended, allowing writes without consent;
before calling defaults::apply ensure interactive consent or explicit --yes: if
!self.dry_run && !self.yes then require console::user_attended_stderr() and
prompt::confirm(msg) — otherwise return an error instructing the user to pass
--yes or run interactively. Update the block around prompt::confirm,
console::user_attended_stderr, and the call to defaults::apply so
defaults::apply only runs when consent was obtained or --yes was provided.

Comment thread src/system/defaults.rs
Comment thread src/system/defaults.rs Outdated
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown

Hyperfine Performance

mise x -- echo

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.6.5 x -- echo 25.4 ± 2.4 20.3 42.3 1.00
mise x -- echo 26.9 ± 2.9 20.2 53.4 1.06 ± 0.15

mise env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.6.5 env 24.8 ± 2.2 19.6 32.1 1.00
mise env 24.9 ± 2.4 20.0 35.8 1.00 ± 0.13

mise hook-env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.6.5 hook-env 23.9 ± 1.9 19.0 30.5 1.00
mise hook-env 25.9 ± 2.6 21.3 36.1 1.08 ± 0.14

mise ls

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.6.5 ls 20.4 ± 2.1 15.7 27.2 1.00
mise ls 21.9 ± 2.4 15.5 32.8 1.07 ± 0.16

xtasks/test/perf

Command mise-2026.6.5 mise Variance
install (cached) 160ms 163ms -1%
ls (cached) 88ms 92ms -4%
bin-paths (cached) 88ms 89ms -1%
task-ls (cached) 174ms 172ms +1%

- shell-quote dry-run/debug command output so printed `defaults write`
  commands are copy-pasteable when string values contain spaces
- distinguish missing keys ("does not exist") from real `defaults`
  failures (cfprefsd unavailable, managed domains) instead of treating
  every non-zero exit as Unset
- preserve leading/trailing spaces in string values read back from
  `defaults read` (strip only the trailing newline)
- doctor: factor the defaults check into one helper shared by the text
  and JSON paths, and keep the system_defaults section visible (with the
  error) when the status check fails instead of dropping it
- normalize schema/mise.json formatting from `mise run render` (CI lint)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

jdx commented Jun 12, 2026

Copy link
Copy Markdown
Owner Author

Addressed review feedback in 63cfcb8:

  • Dry-run output not copy-pasteable with spaces in values (greptile P1): now shell-quoted via shell_words::join.
  • All non-zero exits treated as Unset (greptile P2): defaults read-type/read failures are now only treated as missing when stderr says "does not exist"; anything else (cfprefsd unavailable, managed domains) errors out instead of masquerading as an unset key.
  • trim() stripping significant whitespace (coderabbit): only the trailing newline is stripped now, so string values like " left " compare correctly.
  • Doctor duplication / section dropped on error (greptile P2 + coderabbit): the check is factored into a single helper shared by the text and JSON paths, and a failed status check now keeps system_defaults visible with the error instead of dropping the section.
  • CI lint: mise run render schema formatting committed.

Declined: requiring --yes in unattended sessions before writing defaults (coderabbit). This intentionally matches the [system.packages] driver behavior (!dry_run && !yes && user_attended_stderr()) — non-interactive runs (containers/CI) proceed without a prompt, which the system-packages docs document as the expected CI flow. Diverging for defaults only would make the two halves of mise system install behave inconsistently; if that guard should change, it should change for both in a separate PR.

This comment was generated by an AI coding assistant.

@jdx jdx merged commit 4628160 into main Jun 12, 2026
35 checks passed
@jdx jdx deleted the feat/system-defaults branch June 12, 2026 23:28
jdx added a commit that referenced this pull request Jun 12, 2026
After rebasing onto main's [system.defaults] (#10363):

- mise bootstrap now also writes macOS defaults in step 1, matching
  what mise system install does (apply_defaults refactored into a
  shared function; inert off-macOS)
- mise system install converges packages -> files -> defaults in the
  full apply-everything form
- mise system status shows all three sections; --missing covers them
- regenerated usage/man/fig/docs for the merged state

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jdx added a commit that referenced this pull request Jun 13, 2026
After rebasing onto main's [system.defaults] (#10363):

- mise bootstrap now also writes macOS defaults in step 1, matching
  what mise system install does (apply_defaults refactored into a
  shared function; inert off-macOS)
- mise system install converges packages -> files -> defaults in the
  full apply-everything form
- mise system status shows all three sections; --missing covers them
- regenerated usage/man/fig/docs for the merged state

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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