feat: add [system.edits] for editing files mise doesn't own#10368
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds declarative [[system.edits]] support: schema, core edit resolution/check/apply logic, CLI wiring (install/status/bootstrap), help/docs/site updates, and end-to-end tests validating behavior and failure cases. ChangesSystem Edits Feature Implementation
Sequence DiagramsequenceDiagram
participant User
participant BootstrapCmd
participant SystemInstallCmd
participant SystemStatusCmd
participant EditsModule
participant FilesModule
participant DefaultsModule
User->>BootstrapCmd: run mise bootstrap
BootstrapCmd->>FilesModule: apply [system.files]
BootstrapCmd->>EditsModule: edits_from_config + apply(dry_run/yes)
BootstrapCmd->>DefaultsModule: apply [system.defaults] (macOS)
User->>SystemInstallCmd: run mise system install
SystemInstallCmd->>FilesModule: apply [system.files]
SystemInstallCmd->>EditsModule: edits_from_config + apply(dry_run/yes)
SystemInstallCmd->>DefaultsModule: apply [system.defaults] (macOS)
User->>SystemStatusCmd: run mise system status
SystemStatusCmd->>EditsModule: edits_from_config + check per edit
SystemStatusCmd-->>User: JSON/table with edit states
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 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 adds experimental
Confidence Score: 5/5Safe to merge; the core apply/check logic is correct and the e2e suite covers the critical edge cases. The implementation correctly handles all the tricky cases: No files require special attention, though Important Files Changed
Reviews (8): Last reviewed commit: "fix(config): reject multi-line [system.e..." | Re-trigger Greptile |
938e7fe to
333ffb6
Compare
6df9954 to
e63229a
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
man/man1/mise.1 (1)
2822-2836:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
--missinghelp text is stale now that edits are included in status checks.Line 2835-2836 says
--missingexits non-zero when packages/files/defaults are not converged, butsystem statusnow also evaluates[[system.edits]]. Please include edits in this option description to match actual behavior and avoid misleading CI checks.Suggested patch
-\fB\-\-missing\fR -Exit with code 1 if any configured packages, files, or defaults are +\fB\-\-missing\fR +Exit with code 1 if any configured packages, files, edits, or defaults are not in their desired state (missing, version mismatch, differs)🤖 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 `@man/man1/mise.1` around lines 2822 - 2836, Update the help text for the --missing option in the mise system status man page to include edits: change the description that currently says it exits non-zero when packages, files, or defaults are not in their desired state to also mention that it exits non-zero when configured edits (from [[system.edits]]) are not converged; modify the paragraph under "Options" for the \-\-missing flag so it accurately references packages, files, defaults, and edits (or "edits") and reflects the current behavior of the system status command.xtasks/fig/src/mise.ts (1)
3299-3302:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winInclude edits in the
--missinghelp text.The current wording still says packages, files, or defaults only, but
mise system statusnow also checks[[system.edits]]. Please update both copies so the help/docs match the runtime behavior.
xtasks/fig/src/mise.ts#L3299-L3302: add edits to the--missingdescription.docs/cli/system/status.md#L18-L21: mirror the same wording in the generated docs.Suggested wording
-Exit with code 1 if any configured packages, files, or defaults are -not in their desired state (missing, version mismatch, differs) +Exit with code 1 if any configured packages, files, edits, or defaults are +not in their desired state (missing, version mismatch, differs)🤖 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 `@xtasks/fig/src/mise.ts` around lines 3299 - 3302, Update the help text for the --missing flag so it mentions "edits" as well as packages, files, and defaults: change the description in the flag definition named "--missing" (in mise.ts) to read something like "Exit with code 1 if any configured packages, files, defaults, or edits are not in their desired state (missing, version mismatch, differs)"; then mirror that exact wording in the generated docs entry for the system status command (the docs/cli/system/status.md copy) so runtime behavior and docs match.
🤖 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 `@docs/system-edits.md`:
- Around line 107-109: Update the status summary for the `mise system status`
output to include the distinct "source missing" state in addition to `applied`,
`missing`, and `differs`; specifically mention that `[[system.edits]]` blocks
can report `source missing` when an edit's external file source is absent, and
add that string to any lists, examples, and descriptions that enumerate possible
edit statuses so the docs match the implementation.
- Around line 92-94: Update the paragraph so it explicitly states that both
marker blocks and standalone line edits remain in the file when a config entry
is removed; clarify that mise does not maintain a state DB and therefore neither
the marker block (the "mise" marker plus id) nor any individual "line" edits are
automatically deleted, so users must remove both by hand. Ensure the wording
references the marker block naming convention ("mise" and the id) and calls out
"line" entries/edits so readers don't expect automatic cleanup for line edits.
In `@e2e/cli/test_system_edits`:
- Around line 170-173: Replace the two invocations of "mise system install
--yes" with a single invocation: run the command once (capture its stdout/stderr
into install_out with the exit allowed, e.g., "install_out="$(mise system
install --yes 2>&1 || true)""), then assert the failure and both expected
messages against that captured output using assert_fail/assert_contains_text
(check the exit status or use assert_fail on the captured result) so both
"failed to render" and "source does not exist" are verified from the same run;
update references to install_out and remove the duplicate initial assert_fail
invocation.
In `@src/cli/system/mod.rs`:
- Around line 21-22: Update the module doc comment for the system command and
the "system" help text in the usage KDL so they mention both "mise system
install" and "mise bootstrap": adjust the sentence that currently only
references "mise system install" to say that both "mise system install" and
"mise bootstrap" apply system edits (noting that bootstrap applies
[[system.edits]] before defaults), so the user-facing contract matches the
actual flow; edit the module-level comment in the system module and the
corresponding "system" entry in the usage KDL to mirror the same wording.
---
Outside diff comments:
In `@man/man1/mise.1`:
- Around line 2822-2836: Update the help text for the --missing option in the
mise system status man page to include edits: change the description that
currently says it exits non-zero when packages, files, or defaults are not in
their desired state to also mention that it exits non-zero when configured edits
(from [[system.edits]]) are not converged; modify the paragraph under "Options"
for the \-\-missing flag so it accurately references packages, files, defaults,
and edits (or "edits") and reflects the current behavior of the system status
command.
In `@xtasks/fig/src/mise.ts`:
- Around line 3299-3302: Update the help text for the --missing flag so it
mentions "edits" as well as packages, files, and defaults: change the
description in the flag definition named "--missing" (in mise.ts) to read
something like "Exit with code 1 if any configured packages, files, defaults, or
edits are not in their desired state (missing, version mismatch, differs)"; then
mirror that exact wording in the generated docs entry for the system status
command (the docs/cli/system/status.md copy) so runtime behavior and docs match.
🪄 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: 3395e128-9738-4c5b-ad91-5d056791df5b
📒 Files selected for processing (19)
docs/.vitepress/config.tsdocs/cli/bootstrap.mddocs/cli/system.mddocs/cli/system/install.mddocs/cli/system/status.mddocs/system-edits.mddocs/system-files.mddocs/tips-and-tricks.mde2e/cli/test_system_editsman/man1/mise.1mise.usage.kdlschema/mise.jsonsrc/cli/bootstrap.rssrc/cli/system/install.rssrc/cli/system/mod.rssrc/cli/system/status.rssrc/system/edits.rssrc/system/mod.rsxtasks/fig/src/mise.ts
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.6.5 x -- echo |
19.0 ± 0.9 | 17.0 | 23.7 | 1.00 |
mise x -- echo |
19.9 ± 1.9 | 18.0 | 52.5 | 1.05 ± 0.11 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.6.5 env |
18.5 ± 0.8 | 16.9 | 23.1 | 1.00 |
mise env |
19.2 ± 0.9 | 17.4 | 23.4 | 1.03 ± 0.07 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.6.5 hook-env |
19.2 ± 0.8 | 17.5 | 23.6 | 1.00 |
mise hook-env |
19.9 ± 1.0 | 17.9 | 26.1 | 1.04 ± 0.07 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.6.5 ls |
15.9 ± 0.7 | 14.3 | 19.5 | 1.00 |
mise ls |
16.4 ± 0.9 | 14.6 | 20.5 | 1.03 ± 0.07 |
xtasks/test/perf
| Command | mise-2026.6.5 | mise | Variance |
|---|---|---|---|
| install (cached) | 129ms | 132ms | -2% |
| ls (cached) | 58ms | 58ms | +0% |
| bin-paths (cached) | 63ms | 63ms | +0% |
| task-ls (cached) | 122ms | 125ms | -2% |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/system/edits.rs (3)
538-575:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPreserve the file's existing newline style when rewriting.
This rebuilds the entire file via
text.lines()andjoin("\n"), which normalizes any CRLF file to LF. For a feature that only owns a fragment of somebody else's file, changing every untouched line ending is a real integration break.Suggested fix
let text = if req.path.exists() { file::read_to_string(&req.path)? } else { String::new() }; + let newline = if text.contains("\r\n") { "\r\n" } else { "\n" }; let mut lines: Vec<String> = text.lines().map(|l| l.to_string()).collect(); @@ - let mut out = lines.join("\n"); - out.push('\n'); + let mut out = lines.join(newline); + out.push_str(newline);🤖 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/system/edits.rs` around lines 538 - 575, The code currently normalizes line endings by using text.lines() and joining with "\n"; update it to detect and preserve the file's original newline sequence and final-newline presence: inspect the original text variable for "\r\n" to choose the separator (use "\r\n" if present, otherwise "\n"), keep track whether text ended with a newline before rebuilding (so you only append the final newline when originally present), and then join lines with the chosen separator instead of hardcoding "\n" (make changes around variables text, lines, out and the final file::write call so EditOp::Block and EditOp::Line still operate the same while preserving CRLF vs LF and trailing newline behavior).
182-185:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReject multiline
linevalues during resolution.
lineis supposed to represent one exact line, but nothing here rejects\n/\r. A multiline TOML string will never compare equal incheck(), andapply_one()will inject multiple lines into the target file.Suggested fix
- (false, Some(line)) => ( - format!("{}\u{0}line:{line}", path.display()), - EditOp::Line { line: line.clone() }, - ), + (false, Some(line)) => { + if line.contains('\n') || line.contains('\r') { + bail!("\"{path_raw}\": line must be a single line, ignoring entry"); + } + ( + format!("{}\u{0}line:{line}", path.display()), + EditOp::Line { line: line.clone() }, + ) + }🤖 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/system/edits.rs` around lines 182 - 185, The match arm that constructs EditOp::Line for (false, Some(line)) currently allows multiline values; add validation there to reject any 'line' containing '\n' or '\r' and return a resolution error (with a clear message referencing the offending value) instead of producing EditOp::Line. This prevents check() from failing to match and apply_one() from injecting multiple lines; ensure the error path is used by the resolver that builds the tuple so callers see the failure.
104-108:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon't print the full managed line in operation labels.
For
EditOp::Line, this returns the literal file content, and that value is surfaced in dry-run output, prompts, error messages, and the final apply log. That can leak secrets or PII from shell/config files into CI logs and terminals.Suggested fix
pub fn describe_op(&self) -> String { match &self.op { EditOp::Block { id, .. } => format!("block:{id}"), - EditOp::Line { line } => format!("line:{line}"), + EditOp::Line { .. } => "line".to_string(), } }🤖 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/system/edits.rs` around lines 104 - 108, The describe_op function currently includes the full managed line for EditOp::Line which can leak secrets; update describe_op (the match arm handling EditOp::Line) to avoid embedding the line content—e.g., return a safe label like "line:len=<n>" (use the line's length) or a redacted placeholder such as "line:<redacted>" or a short deterministic fingerprint (e.g., first N chars or a hash) instead of format!("line:{line}"), so the function no longer prints the full line text.
🤖 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.
Outside diff comments:
In `@src/system/edits.rs`:
- Around line 538-575: The code currently normalizes line endings by using
text.lines() and joining with "\n"; update it to detect and preserve the file's
original newline sequence and final-newline presence: inspect the original text
variable for "\r\n" to choose the separator (use "\r\n" if present, otherwise
"\n"), keep track whether text ended with a newline before rebuilding (so you
only append the final newline when originally present), and then join lines with
the chosen separator instead of hardcoding "\n" (make changes around variables
text, lines, out and the final file::write call so EditOp::Block and
EditOp::Line still operate the same while preserving CRLF vs LF and trailing
newline behavior).
- Around line 182-185: The match arm that constructs EditOp::Line for (false,
Some(line)) currently allows multiline values; add validation there to reject
any 'line' containing '\n' or '\r' and return a resolution error (with a clear
message referencing the offending value) instead of producing EditOp::Line. This
prevents check() from failing to match and apply_one() from injecting multiple
lines; ensure the error path is used by the resolver that builds the tuple so
callers see the failure.
- Around line 104-108: The describe_op function currently includes the full
managed line for EditOp::Line which can leak secrets; update describe_op (the
match arm handling EditOp::Line) to avoid embedding the line content—e.g.,
return a safe label like "line:len=<n>" (use the line's length) or a redacted
placeholder such as "line:<redacted>" or a short deterministic fingerprint
(e.g., first N chars or a hash) instead of format!("line:{line}"), so the
function no longer prints the full line text.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 3df885ef-8ff4-4b40-ab57-18c8a61f3741
📒 Files selected for processing (6)
docs/cli/system.mddocs/system-edits.mde2e/cli/test_system_editsmise.usage.kdlsrc/cli/system/mod.rssrc/system/edits.rs
✅ Files skipped from review due to trivial changes (4)
- src/cli/system/mod.rs
- docs/cli/system.md
- mise.usage.kdl
- docs/system-edits.md
🚧 Files skipped from review as they are similar to previous changes (1)
- e2e/cli/test_system_edits
Where [system.files] manages whole files, an edit owns one small piece
of a file something else owns — the mise activate line in a shell rc,
an entry in /etc/hosts:
[[system.edits]]
path = "~/.zshrc"
id = "activate"
block = 'eval "$(mise activate zsh)"'
[[system.edits]]
path = "/etc/hosts"
line = "127.0.0.1 dev.local"
Blocks are delimited by marker comments (`# >>> mise:<id> >>>` /
`# <<< mise:<id> <<<`) which double as the ownership record, keeping
the design stateless: apply replaces only what's between the markers.
Content can be inline, from a source file, or rendered through the
template engine; the comment prefix is inferred from the file
extension. Lines ensure an exact line exists, appending if absent.
Entries merge across the config hierarchy keyed by (path, id) /
(path, line). Edits never replace files so there is no --force;
corrupted markers and symlink targets (which edits would write
through) are refused with an error instead of guessed at. Applied by
`mise system install` and `mise bootstrap` after files, reported by
`mise system status`.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Substring matching treated any line mentioning a marker pattern (e.g. echo ">>> mise:x >>>" in managed block content) as a marker, producing duplicate/corrupted-marker errors. A marker line now requires the pattern at the start of the line, preceded by at most a short non-alphanumeric comment token. Block content that would contain its own marker lines is refused up front instead of writing a file that can't be parsed back. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
--dry-run no longer renders template blocks (exec() in a template must not run side effects during a dry run); they are listed as '(if changed)' since their state wasn't computed, matching [system.files]. mise system status keeps rendering templates — same trust model as [env] templates — now documented on check() and in the docs page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- recognize markers written with an exotic configured comment token (e.g. REM): the configured comment always counts as a marker prefix, so written markers can always be found again (Cursor Bugbot) - replace string matching on Differs reasons with an EditCheck enum distinguishing ordinary states from blocked conditions (greptile) - dedupe/override entries by the expanded target path instead of the raw config string, so ~/.zshrc and /home/user/.zshrc merge (both) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- move System Packages/Files/Edits and macOS Defaults out of Dev Tools into a top-level 'System (experimental)' sidebar section, with mise bootstrap alongside them - label System Files with '(dotfiles)' — the term users search for - add a machine-bootstrapping section to tips & tricks showing the whole [system] story in one example Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirror the [system.files] fix: a template render or check failure on one entry no longer aborts the gather loop and hides problems with the remaining entries — all errors are collected and reported together. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lates check and apply decided symlink targets, missing files, and marker integrity only after rendering template blocks, so exec() in a template ran even when the entry was about to be refused or was trivially missing. A render-free precheck now settles those outcomes (and line presence) first; rendering only happens when existing markers require a content comparison. Also per review: docs note that removed line entries linger like blocks do, document the source-missing status, the system command help mentions mise bootstrap, and the aggregated-failure e2e asserts on a single install invocation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the [[system.edits]] array of tables with a map keyed by target
path, then by an id naming each edit within the file:
[system.edits]
"~/.zshrc" = {
activate = 'eval "$(mise activate zsh)"',
aliases = '''
alias ll='ls -l'
''',
}
"/etc/hosts".dev = { line = "127.0.0.1 dev.local" }
This matches the shape of every other mise config section, makes the
(path, id) merge identity structural instead of synthetic (hierarchy
override now works exactly like [system.files]), turns duplicate ids in
one file into a TOML parse error, and lets a local config override a
global line entry by id. A string value is inline block content — TOML
multiline strings keep larger blocks readable — and the optional id
field with its 'mise' default is gone: the key is the id, always
explicit in the markers. Ids are restricted to marker-safe characters.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
[[system.edits]] for editing files mise doesn't own[system.edits] for editing files mise doesn't own
The bootstrap e2e covered files, tools, and the bootstrap task but not the [system.edits] step — add an activate-block entry using the dotted map syntax and assert its markers land. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…atch Two entries with different ids but identical line text both planned an append (each prechecked while the file still lacked the line), and the Line apply pushed unconditionally — one install could write the same line twice. apply_one now checks the file's current content, which it re-reads per entry, before appending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/system/edits.rs (1)
565-603:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPreserve original newline style when rewriting edited files.
Line [570] + Line [600] rebuild the whole file with
\n, which silently converts CRLF files to LF. Since this feature edits files mise doesn’t own, this can unintentionally alter unrelated content and break Windows-oriented files.🔧 Proposed fix
fn apply_one(req: &EditRequest, desired: Option<&str>) -> Result<()> { @@ - let text = if req.path.exists() { + let text = if req.path.exists() { file::read_to_string(&req.path)? } else { String::new() }; + let newline = if text.contains("\r\n") { "\r\n" } else { "\n" }; let mut lines: Vec<String> = text.lines().map(|l| l.to_string()).collect(); @@ - let mut out = lines.join("\n"); - out.push('\n'); + let mut out = lines.join(newline); + out.push_str(newline);🤖 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/system/edits.rs` around lines 565 - 603, The code currently normalizes line endings by rebuilding the file with "\n"; detect and preserve the original newline style and trailing newline when rewriting. Use the already-read text to determine a separator (e.g., if text.contains("\r\n") use "\r\n" else use "\n"), build the lines vector by splitting on '\n' but stripping a trailing '\r' per line (to keep content same), and when composing out use that separator to join (and preserve whether the original ended with a newline by checking text.ends_with("\r\n") or text.ends_with('\n') rather than always pushing '\n'); update the logic around text -> lines creation and out construction before file::write(&req.path, &out) so EditOp::Block / EditOp::Line behavior and markers (begin_marker, end_marker, find_block, req.id, req.path_raw) remain unchanged.
🤖 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 `@schema/mise.json`:
- Around line 2970-3001: The schema currently allows arbitrary keys under the id
map via "additionalProperties", but runtime code enforces IDs matching
[A-Za-z0-9_.-] and will skip invalid entries; add a "propertyNames" constraint
alongside the existing "additionalProperties" block to require keys match the
regex ^[A-Za-z0-9_.-]+$ so tooling validation matches runtime behavior. Locate
the object that contains "additionalProperties" (the id map definition) and add
a sibling "propertyNames": { "pattern": "^[A-Za-z0-9_.-]+$" } ensuring the
schema rejects keys that the runtime (edits validation) would skip. Ensure the
pattern uses anchors and the same character class as the runtime to keep them in
sync.
---
Outside diff comments:
In `@src/system/edits.rs`:
- Around line 565-603: The code currently normalizes line endings by rebuilding
the file with "\n"; detect and preserve the original newline style and trailing
newline when rewriting. Use the already-read text to determine a separator
(e.g., if text.contains("\r\n") use "\r\n" else use "\n"), build the lines
vector by splitting on '\n' but stripping a trailing '\r' per line (to keep
content same), and when composing out use that separator to join (and preserve
whether the original ended with a newline by checking text.ends_with("\r\n") or
text.ends_with('\n') rather than always pushing '\n'); update the logic around
text -> lines creation and out construction before file::write(&req.path, &out)
so EditOp::Block / EditOp::Line behavior and markers (begin_marker, end_marker,
find_block, req.id, req.path_raw) remain unchanged.
🪄 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: f42f9b5c-ff83-448c-80f2-065bf12dfc60
📒 Files selected for processing (17)
docs/cli/bootstrap.mddocs/cli/system.mddocs/cli/system/install.mddocs/cli/system/status.mddocs/system-edits.mddocs/tips-and-tricks.mde2e/cli/test_system_editsman/man1/mise.1mise.usage.kdlschema/mise.jsonsrc/cli/bootstrap.rssrc/cli/system/install.rssrc/cli/system/mod.rssrc/cli/system/status.rssrc/system/edits.rssrc/system/mod.rsxtasks/fig/src/mise.ts
✅ Files skipped from review due to trivial changes (7)
- docs/cli/system/status.md
- docs/cli/bootstrap.md
- docs/tips-and-tricks.md
- src/cli/system/mod.rs
- docs/system-edits.md
- xtasks/fig/src/mise.ts
- mise.usage.kdl
🚧 Files skipped from review as they are similar to previous changes (5)
- docs/cli/system.md
- src/cli/system/install.rs
- man/man1/mise.1
- e2e/cli/test_system_edits
- src/cli/system/status.rs
…docs Show the single [system.edits] header with inline tables throughout, matching [system.files]; the sub-table form is valid TOML but not the style we want to teach. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…"tera") template becomes a string naming the engine instead of a boolean, so other engines can be added later without changing the field. Currently only "tera" is accepted; unknown engines warn and skip the entry, like unrecognized operations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5be8a1f. Configure here.
…hema ids - a line edit whose value contains a newline could never converge (precheck compares against individual file lines, apply re-appends every run) — reject it at parse time and point at blocks for multi-line content - add propertyNames to the schema edit-id map so ids outside [A-Za-z0-9_.-] fail in tooling instead of only at runtime Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Experimental, like the rest of
[system].[system.edits]Where
[system.files]manages whole files, an edit owns one small piece of a file something else owns — themise activateline in a shell rc, an entry in/etc/hosts. Entries are keyed by target path (matching[system.files]), then by an id naming each edit within the file:Blocks
Delimited by marker comments in the target file, named by the entry's id:
The markers are the ownership record, stored in the file itself, so the design stays stateless like the rest of
[system]: apply replaces only what's between them (or appends the block if absent); status (applied/missing/differs) falls out trivially. Content can be inline (string shorthand, with TOML multiline strings for larger blocks), from a file (source, relative to the declaring config), or rendered through the template engine (template = "tera"— a string naming the engine for future use;env,vars,exec()). The comment prefix is inferred from the file extension (#,--,//,;,") and overridable withcomment; ids are restricted to marker-safe characters.Lines
lineensures an exact line exists, appending it at the end if absent. It never modifies or removes other lines — that's what makes it safely idempotent. The id is a label and the merge identity; it isn't written to the file. (Amatch-regex replace mode was considered and deliberately left out of v1.)Semantics
(path, id)— structural, exactly like[system.files]overrides by target; a local config can override a global edit by idmise system install/mise bootstrap(after packages and files, before defaults), never implicitly;mise system statusgains an edits table, JSON output, and--missingcoverageexec();--dry-runnever renders templates at all and lists them as(if changed), matching the[system.files]policy--forceinteraction: edits never replace files. Corrupted markers and symlink targets (which edits would write through) are refused with errors; all problems across entries are reported in one passDocs
/system-editspage; top-level System (experimental) sidebar section (System Packages, System Files (dotfiles), System Edits, macOS Defaults,mise bootstrap); machine-bootstrapping section in tips & tricksTests
e2e/cli/test_system_edits: multiline-inline-table parsing, two blocks per file, line append preserving existing content, comment inference, template rendering + exec dry-run probe, idempotency, block replacement preserving surrounding content, corrupted-marker and symlink refusal, source-file blocks + missing sources, one-pass error aggregation, invalid entries and id validationThis PR was generated by an AI coding assistant.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests
CLI / UX
Note
Medium Risk
Changes write directly to user config paths and template evaluation on status/install can run
exec()from config; mitigated by experimental flag, explicit install/bootstrap only, and hard errors for symlinks and bad markers.Overview
Introduces experimental
[system.edits]so mise can manage one piece of a file it does not own (e.g.mise activatein~/.zshrc, a hosts entry), complementing whole-file[system.files].Config is keyed by target path then edit id. Blocks use
>>> mise:<id> >>>/<<< mise:<id> <<<markers (inline string,source, optionaltemplate = "tera"). Lines append an exact line if missing. Entries merge on(path, id); invalid or unknown shapes warn and skip.mise system installandmise bootstrapapply edits after packages and files (before defaults on bootstrap); package-only installs still skip edits.mise system statusadds an edits table, JSONedits, and--missingcoverage. Apply refuses corrupted markers and symlink targets;--dry-rundoes not render templates (listed as(if changed)), while status/install may render tera includingexec()like other trusted templates.Docs add
/system-edits, regroup the sidebar under System (experimental), update CLI/man/usage/schema/completions, cross-link from system files and tips. E2e tests cover bootstrap + a broadtest_system_editssuite; unit tests cover marker parsing.Reviewed by Cursor Bugbot for commit ebd569d. Bugbot is set up for automated code reviews on this repo. Configure here.