feat(system): add [system.defaults] for declarative macOS defaults#10363
Conversation
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>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThis PR introduces macOS system defaults management to mise, allowing users to declare and apply macOS user preferences via a ChangesmacOS System Defaults Support
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
Greptile SummaryThis PR extends the experimental
Confidence Score: 5/5Safe to merge — the feature is opt-in, guarded behind the experimental flag, and only writes to user defaults when explicitly invoked with 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
Reviews (2): Last reviewed commit: "fix(system): address review feedback for..." | Re-trigger Greptile |
There was a problem hiding this comment.
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
📒 Files selected for processing (18)
docs/.vitepress/config.tsdocs/cli/system.mddocs/cli/system/install.mddocs/cli/system/status.mddocs/system-packages/defaults.mddocs/system-packages/index.mde2e/cli/test_system_defaultsman/man1/mise.1mise.usage.kdlschema/mise.jsonsrc/cli/doctor/mod.rssrc/cli/system/install.rssrc/cli/system/mod.rssrc/cli/system/status.rssrc/config/config_file/mise_toml.rssrc/system/defaults.rssrc/system/mod.rsxtasks/fig/src/mise.ts
| 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 { |
There was a problem hiding this comment.
🎯 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.
Hyperfine Performance
|
| 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>
|
Addressed review feedback in 63cfcb8:
Declined: requiring This comment was generated by an AI coding assistant. |
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>
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>
Summary
Extends the experimental
[system]bootstrapping section with declarative macOS user defaults, following the same design as[system.packages](#10326):status/doctorlist entries as skipped (nothing silently invisible),installignores them, so cross-platform configs just work.mise system installwrites anything, with the usual confirmation prompt;--dry-runprints thedefaults writecommands.-bool/-int/-float/-string. Drift detection viadefaults read-type+defaults read; types compare strictly (integer1≠true). Unsupported plist shapes (arrays, dicts) warn and are skipped for forward compatibility.-currentHost) support.mise system statusgains a Domain/Key/Value/Current/State table,--jsongains adefaultskey,--missingexits 1 on drift;mise doctorreports out-of-sync counts.--managerscopemise system installto packages only; defaults apply in the bare converge-everything form.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 configsrc/cli/system/{status,install,mod}.rs,src/cli/doctor/mod.rs— CLI/doctor wiringschema/mise.json, docs pagedocs/system-packages/defaults.md, generated usage/completionsTests
defaults writearg rendering, strict type matching,[system.defaults]parsing (incl. forward-compat shapes)e2e/cli/test_system_defaults): cross-platform behavior — skipped-not-error on Linux,--missingnot 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/--missingbehavior when defaults are configured; scope is limited to explicit install and per-userdefaults(no sudo).Overview
Adds
[system.defaults]tomise.tomlso macOS preferences can be declared like system packages and converged withmise system install(viadefaults write), withmise system status/--missingandmise doctorreporting drift.New
src/system/defaults.rshandles 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 installapplies defaults only on the bare converge run (no explicitmanager:packageargs 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
Documentation
Tests