Skip to content

feat: add [system.edits] for editing files mise doesn't own#10368

Merged
jdx merged 13 commits into
mainfrom
feat/system-edits
Jun 13, 2026
Merged

feat: add [system.edits] for editing files mise doesn't own#10368
jdx merged 13 commits into
mainfrom
feat/system-edits

Conversation

@jdx

@jdx jdx commented Jun 12, 2026

Copy link
Copy Markdown
Owner

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 — the mise activate line 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:

[system.edits]
"~/.zshrc" = {
  activate = 'eval "$(mise activate zsh)"',  # string = inline block content
  aliases = '''
alias ll='ls -l'
alias la='ls -la'
''',
}
"/etc/hosts".dev = { line = "127.0.0.1 dev.local" }

Blocks

Delimited by marker comments in the target file, named by the entry's id:

# >>> mise:activate >>> managed by mise — do not edit between markers
eval "$(mise activate zsh)"
# <<< mise:activate <<<

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 with comment; ids are restricted to marker-safe characters.

Lines

line ensures 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. (A match-regex replace mode was considered and deliberately left out of v1.)

Semantics

  • Merge across the config hierarchy as a union keyed by (path, id) — structural, exactly like [system.files] overrides by target; a local config can override a global edit by id
  • Applied only by mise system install / mise bootstrap (after packages and files, before defaults), never implicitly; mise system status gains an edits table, JSON output, and --missing coverage
  • Render-free outcomes (symlink targets, marker integrity, line presence) are decided before templates render, so blocked entries never execute exec(); --dry-run never renders templates at all and lists them as (if changed), matching the [system.files] policy
  • No --force interaction: 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 pass
  • Removing an entry leaves its block/line behind (no state database); markers carry provenance — documented tradeoff
  • No sudo: edits write as the current user
  • Unrecognized entries warn and are skipped, like unknown package managers and file modes

Docs

  • New /system-edits page; top-level System (experimental) sidebar section (System Packages, System Files (dotfiles), System Edits, macOS Defaults, mise bootstrap); machine-bootstrapping section in tips & tricks

Tests

  • 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 validation
  • Unit tests for marker parsing (id delimiting, mention-in-content, exotic comment tokens) and comment inference

This PR was generated by an AI coding assistant.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Experimental "System Edits" to declaratively manage marker-delimited blocks or single-line edits; supported by schema and integrated into install/bootstrap flows and status reporting.
  • Documentation

    • Reorganized sidebar under “System (experimental)”; added System Edits docs and updated CLI help, man page, usage text, tips, and minor formatting tweaks.
  • Tests

    • Added e2e tests validating edits, templates, idempotency, reporting, and failure cases.
  • CLI / UX

    • Status output and help now show edits; bootstrap/install flows and completions mention edits.

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 activate in ~/.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, optional template = "tera"). Lines append an exact line if missing. Entries merge on (path, id); invalid or unknown shapes warn and skip.

mise system install and mise bootstrap apply edits after packages and files (before defaults on bootstrap); package-only installs still skip edits. mise system status adds an edits table, JSON edits, and --missing coverage. Apply refuses corrupted markers and symlink targets; --dry-run does not render templates (listed as (if changed)), while status/install may render tera including exec() 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 broad test_system_edits suite; 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.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds 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.

Changes

System Edits Feature Implementation

Layer / File(s) Summary
Schema and module structure
schema/mise.json, src/system/mod.rs
system.edits schema added and edits submodule exposed with SystemTomlConfig.edits field.
System edits core logic
src/system/edits.rs
Parses [[system.edits]], resolves EditRequests, infers comment tokens, detects markers, computes desired content (templating), inspects state, and applies edits with dry-run, confirmation, permission-preserving writes, and unit tests.
CLI command integration
src/cli/bootstrap.rs, src/cli/system/install.rs, src/cli/system/status.rs, src/cli/system/mod.rs
Wires edits into bootstrap/install flows (applied after files and before macOS defaults), loads edit requests, applies them, and reports edit state in status (JSON and table).
Help, man, and completion text
mise.usage.kdl, man/man1/mise.1, xtasks/fig/src/mise.ts
Updates help/man/completion text to mention [[system.edits]] and the updated install/bootstrap ordering.
User-facing documentation & site
docs/system-edits.md, docs/system-files.md, docs/cli/*, docs/tips-and-tricks.md, docs/.vitepress/config.ts
Adds comprehensive system-edits docs, guidance for using edits vs files, bootstrap examples, and reorganizes the docs sidebar under a consolidated "System (experimental)".
End-to-end test coverage
e2e/cli/test_system_edits, e2e/cli/test_bootstrap
New e2e script(s) verifying status/dry-run/install behavior, idempotency, targeted updates, marker corruption and symlink refusal handling, source imports, template execution, error accumulation, and forward-compat warnings.

Sequence Diagram

sequenceDiagram
  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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • jdx/mise#10363: Related work that introduced system.defaults handling and modified the same install/status plumbing this change extends.
  • jdx/mise#10370: Edits to the tips-and-tricks/bootstrap docs area that may overlap with the new bootstrap examples and guidance.
  • jdx/mise#10365: Also modifies mise bootstrap/mise system install flows to add system-file handling; touches the same command entry points this PR extends.

Poem

🐰 I nibble markers, tuck small lines in place,

Templates bloom, files keep the user's trace,
Bootstrap hums, installs tidy and neat,
Edits converge—no duplicate repeat,
A rabbit's patchwork makes machines complete.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title accurately and concisely summarizes the main change: adding a new [system.edits] feature for editing files that mise does not own.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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.

Comment thread src/system/edits.rs
Comment thread src/system/edits.rs
@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds experimental [system.edits] — a way for mise to own a slice of a file it doesn't wholly manage (e.g. the mise activate line in ~/.zshrc, an entry in /etc/hosts), complementing the existing [system.files] whole-file ownership. The implementation is stateless: marker comments written into the target file serve as the ownership record, so no external database is needed.

  • src/system/edits.rs introduces the new module with block (marker-delimited) and line (append-only) edit operations, config merging keyed by (resolved_path, id), one-pass error aggregation, dry-run skipping of template rendering, and refusals for corrupted markers and symlink targets.
  • mise system install, mise bootstrap, and mise system status are updated to apply and report edits; the JSON schema, usage spec, man page, and docs are updated in parallel.
  • The e2e test suite covers idempotency, multi-block coexistence, template rendering with exec() dry-run probes, source files, duplicate-line deduplication, and all failure modes.

Confidence Score: 5/5

Safe 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: apply_one re-reads the file so duplicate-line deduplication works across multiple entries in a single batch; EditCheck::Blocked is a typed variant so the apply path never string-matches on error messages; the merge key uses the resolved path so ~/.zshrc and /home/user/.zshrc deduplicate correctly. The two nits are minor quality issues, not correctness problems.

No files require special attention, though src/system/edits.rs is the only net-new logic and is worth a careful read.

Important Files Changed

Filename Overview
src/system/edits.rs New 684-line module implementing block/line edits. Well-structured with correct idempotency handling and one-pass error aggregation. Minor: template field on line edits is silently ignored (no warning), and the target file is read twice per check() cycle for existing blocks.
src/cli/system/install.rs Adds edits to the install flow alongside files; follows the pre-existing pattern of calling Config::get().await? per phase. Correct guard to skip the package driver when only edits/files/defaults are configured.
src/cli/system/status.rs Adds edits table to status output and JSON; correctly maps FileState variants to display strings and propagates render errors as Differs rather than crashing.
src/cli/bootstrap.rs Inserts edits step after files and before defaults, consistent with mise system install ordering.
e2e/cli/test_system_edits Comprehensive e2e test covering blocks, lines, templates, idempotency, corrupted markers, symlink refusal, source files, duplicate-line deduplication, and one-pass error aggregation.
schema/mise.json Adds [system.edits] schema with correct property definitions; block/source and block/source+line mutual exclusion constraints are not encoded in the schema (only enforced at runtime).
src/system/mod.rs Adds edits module declaration and SystemTomlConfig.edits field with correct IndexMap<String, IndexMap<String, EditTomlEntry>> type.

Reviews (8): Last reviewed commit: "fix(config): reject multi-line [system.e..." | Re-trigger Greptile

Comment thread src/system/edits.rs Outdated
Comment thread src/system/edits.rs Outdated
Comment thread src/system/edits.rs
@jdx jdx force-pushed the feat/system-edits branch from ddbcfd5 to c56099d Compare June 13, 2026 00:02
Comment thread src/system/edits.rs Outdated
@jdx jdx force-pushed the feat/system-edits branch from c56099d to 27f6802 Compare June 13, 2026 00:08
@jdx jdx force-pushed the feat/bootstrap-system-files branch from 938e7fe to 333ffb6 Compare June 13, 2026 00:14
@jdx jdx force-pushed the feat/system-edits branch 2 times, most recently from 6df9954 to e63229a Compare June 13, 2026 00:34
Base automatically changed from feat/bootstrap-system-files to main June 13, 2026 00:49
@jdx jdx force-pushed the feat/system-edits branch from e63229a to 1b482b1 Compare June 13, 2026 00:52
Comment thread src/system/edits.rs Outdated

@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

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

--missing help text is stale now that edits are included in status checks.

Line 2835-2836 says --missing exits non-zero when packages/files/defaults are not converged, but system status now 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 win

Include edits in the --missing help text.

The current wording still says packages, files, or defaults only, but mise system status now 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 --missing description.
  • 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

📥 Commits

Reviewing files that changed from the base of the PR and between b7866f9 and 1b482b1.

📒 Files selected for processing (19)
  • docs/.vitepress/config.ts
  • docs/cli/bootstrap.md
  • docs/cli/system.md
  • docs/cli/system/install.md
  • docs/cli/system/status.md
  • docs/system-edits.md
  • docs/system-files.md
  • docs/tips-and-tricks.md
  • e2e/cli/test_system_edits
  • man/man1/mise.1
  • mise.usage.kdl
  • schema/mise.json
  • src/cli/bootstrap.rs
  • src/cli/system/install.rs
  • src/cli/system/mod.rs
  • src/cli/system/status.rs
  • src/system/edits.rs
  • src/system/mod.rs
  • xtasks/fig/src/mise.ts

Comment thread docs/system-edits.md Outdated
Comment thread docs/system-edits.md Outdated
Comment thread e2e/cli/test_system_edits Outdated
Comment thread src/cli/system/mod.rs Outdated
@github-actions

github-actions Bot commented Jun 13, 2026

Copy link
Copy Markdown

Hyperfine Performance

mise x -- echo

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%

@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.

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 win

Preserve the file's existing newline style when rewriting.

This rebuilds the entire file via text.lines() and join("\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 win

Reject multiline line values during resolution.

line is supposed to represent one exact line, but nothing here rejects \n/\r. A multiline TOML string will never compare equal in check(), and apply_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 win

Don'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

📥 Commits

Reviewing files that changed from the base of the PR and between 1b482b1 and 5c9caf8.

📒 Files selected for processing (6)
  • docs/cli/system.md
  • docs/system-edits.md
  • e2e/cli/test_system_edits
  • mise.usage.kdl
  • src/cli/system/mod.rs
  • src/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

jdx and others added 7 commits June 13, 2026 01:17
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>
@jdx jdx force-pushed the feat/system-edits branch from 5c9caf8 to 8371206 Compare June 13, 2026 01:20
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>
@jdx jdx changed the title feat: add [[system.edits]] for editing files mise doesn't own feat: add [system.edits] for editing files mise doesn't own Jun 13, 2026
Comment thread src/system/edits.rs Outdated
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>

@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: 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 win

Preserve 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8371206 and 5730bed.

📒 Files selected for processing (17)
  • docs/cli/bootstrap.md
  • docs/cli/system.md
  • docs/cli/system/install.md
  • docs/cli/system/status.md
  • docs/system-edits.md
  • docs/tips-and-tricks.md
  • e2e/cli/test_system_edits
  • man/man1/mise.1
  • mise.usage.kdl
  • schema/mise.json
  • src/cli/bootstrap.rs
  • src/cli/system/install.rs
  • src/cli/system/mod.rs
  • src/cli/system/status.rs
  • src/system/edits.rs
  • src/system/mod.rs
  • xtasks/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

Comment thread schema/mise.json
jdx and others added 2 commits June 13, 2026 01:52
…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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread src/system/edits.rs
…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>
@jdx jdx merged commit 3e3ff2f into main Jun 13, 2026
35 checks passed
@jdx jdx deleted the feat/system-edits branch June 13, 2026 02:16
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