Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .claude/skills/writing-user-outputs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -526,12 +526,13 @@ clearer:
```

**Description + command in single message:** For warnings/errors that include a
recovery command, join with semicolon. Use `<bold>` for commands in
recovery command, join with semicolon, and order the recovery "To X, run Y" as
hints do, so the command still ends the line. Use `<bold>` for commands in
warnings/errors (only hints use `<underline>`):

```rust
// Warning with inline recovery command (bold for commands)
warning_message("Failed to restore stash; run <bold>git stash pop {ref}</> to restore manually")
warning_message("Failed to restore stash; to restore manually, run <bold>git stash pop {ref}</>")
warning_message("{tool} not authenticated; run <bold>{tool} auth login</>")

// For longer suggestions, use separate hint message (underline for commands)
Expand Down
13 changes: 2 additions & 11 deletions docs/src/content/docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -799,15 +799,6 @@ Updates deprecated settings in user and project config files
to their current equivalents, removes deprecated keys that have no equivalent,
and reports each one. Shows a diff and asks for confirmation.

Migrations are computed in memory on demand; nothing is written outside this
command. Set `--output <path>` to write a migrated config to that destination
instead of applying it in place. Use `-` for stdout. When both user and project
config need migration, stdout emits a labeled inspection artifact and file
output fails rather than combining the configs.

`--output` omits deprecated `approved-commands` with a stderr warning; only an
in-place update migrates them to `approvals.toml`.

### Examples

Preview and apply updates:
Expand All @@ -820,12 +811,12 @@ Apply without confirmation:
$ wt config update --yes
```

Write the migration artifact to a file:
Write the migrated config to a file instead of updating in place:
```console
$ wt config update --output migrated.toml
```

Write the migration artifact to stdout:
Print the migrated config:
```console
$ wt config update --output=-
```
Expand Down
13 changes: 2 additions & 11 deletions plugins/worktrunk/skills/worktrunk/reference/config.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 2 additions & 11 deletions skills/worktrunk/reference/config.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 2 additions & 11 deletions src/cli/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,15 +677,6 @@ This tests:
to their current equivalents, removes deprecated keys that have no equivalent,
and reports each one. Shows a diff and asks for confirmation.

Migrations are computed in memory on demand; nothing is written outside this
command. Set `--output <path>` to write a migrated config to that destination
instead of applying it in place. Use `-` for stdout. When both user and project
config need migration, stdout emits a labeled inspection artifact and file
output fails rather than combining the configs.

`--output` omits deprecated `approved-commands` with a stderr warning; only an
in-place update migrates them to `approvals.toml`.

## Examples

Preview and apply updates:
Expand All @@ -698,12 +689,12 @@ Apply without confirmation:
$ wt config update --yes
```

Write the migration artifact to a file:
Write the migrated config to a file instead of updating in place:
```console
$ wt config update --output migrated.toml
```

Write the migration artifact to stdout:
Print the migrated config:
```console
$ wt config update --output=-
```"#
Expand Down
5 changes: 3 additions & 2 deletions src/commands/config/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -968,8 +968,9 @@ fn render_project_config(out: &mut String, repo: Option<&Repository>) -> anyhow:
return Ok(false);
}

// Check for deprecations with emit_inline_warnings=false (silent mode)
// Only write migration file in main worktree, not linked worktrees.
// Check for deprecations with emit_inline_warnings=false (silent mode).
// They are actionable only from the main worktree, where `wt config update`
// rewrites the file.
// Deprecated patterns supersede the TOML dump below because their diff
// covers the file.
let is_main_worktree = !repo.current_worktree().is_linked().unwrap_or(true);
Expand Down
81 changes: 57 additions & 24 deletions src/commands/config/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
//!
//! Computes user- and project-config migrations in memory. The default mode
//! previews and applies them atomically; output mode writes one migration
//! artifact to the named destination instead. The previous `.new` file flow
//! was removed — nothing writes to disk outside this command.
//! artifact to the named destination instead. No other command writes a
//! migration to disk.

use std::fmt::Write as _;
use std::io::IsTerminal;
use std::path::{Path, PathBuf};

use anyhow::{Context, bail};
Expand All @@ -21,6 +22,7 @@ use worktrunk::styling::{
eprint, eprintln, format_bash_with_gutter, hint_message, info_message, print, success_message,
suggest_command_in_dir, warning_message,
};
use worktrunk::utils::{write_atomically, write_new_atomically};

use crate::output::prompt::{PromptResponse, prompt_yes_no_preview};

Expand Down Expand Up @@ -68,7 +70,7 @@ pub fn handle_config_update(yes: bool, output: Option<PathBuf>) -> anyhow::Resul
}

if let Some(output) = output {
write_migrated_output(&output, &candidates)?;
write_migrated_output(&output, &candidates, yes)?;
return Ok(());
}

Expand Down Expand Up @@ -110,7 +112,7 @@ pub fn handle_config_update(yes: bool, output: Option<PathBuf>) -> anyhow::Resul
})?;
if current != candidate.original {
bail!(cformat!(
"{} changed @ <bold>{}</> since the preview; re-run <bold>wt config update</> to migrate the current contents",
"{} changed @ <bold>{}</> since the preview; to migrate the current contents, re-run <bold>wt config update</>",
candidate.info.label(),
format_path_for_display(&candidate.config_path)
));
Expand Down Expand Up @@ -140,7 +142,7 @@ pub fn handle_config_update(yes: bool, output: Option<PathBuf>) -> anyhow::Resul
);
}

worktrunk::utils::write_atomically(&candidate.config_path, &candidate.migrated)
write_atomically(&candidate.config_path, &candidate.migrated)
.with_context(|| format!("Failed to update {}", candidate.info.label()))?;
eprintln!(
"{}",
Expand All @@ -157,8 +159,21 @@ pub fn handle_config_update(yes: bool, output: Option<PathBuf>) -> anyhow::Resul
/// paths rather than one path testing `-` at each step. Stdout labels and
/// concatenates every candidate and needs no confirmation — the artifact is
/// right there. A file takes exactly one migration, so the checks below and
/// the confirmation can name the config it came from.
fn write_migrated_output(output: &Path, candidates: &[UpdateCandidate]) -> anyhow::Result<()> {
/// the success line can name the config it came from.
///
/// A file destination replaces nothing without consent. The config being
/// migrated is refused outright: rewriting it is the in-place update's job,
/// which previews the diff, re-reads the file after the prompt, and moves
/// `approved-commands` to approvals.toml. Any other existing file is replaced
/// only after a prompt, which `--yes` answers in advance; with no terminal to
/// prompt on, the command fails instead. A destination that was absent is
/// created without clobbering, so a file that appears before the write lands
/// survives it.
fn write_migrated_output(
output: &Path,
candidates: &[UpdateCandidate],
yes: bool,
) -> anyhow::Result<()> {
if output == Path::new("-") {
for candidate in candidates {
eprint!("{}", format_dropped_approvals_warning(candidate));
Expand All @@ -174,34 +189,52 @@ fn write_migrated_output(output: &Path, candidates: &[UpdateCandidate]) -> anyho

let [candidate] = candidates else {
bail!(cformat!(
"Cannot write <bold>user config</> and <bold>project config</> migrations to one file; use <bold>--output=-</> to inspect both or run <bold>wt config update</> to apply them in place"
"Cannot write <bold>user config</> and <bold>project config</> migrations to one file; to inspect both, use <bold>--output=-</>; to apply them in place, run <bold>wt config update</>"
));
};

let output = resolve_input_path(output);
if drops_approved_commands(candidate) && paths_match(&output, &candidate.config_path) {
let label = candidate.info.label().to_lowercase();
if paths_match(&output, &candidate.config_path) {
bail!(cformat!(
"Cannot overwrite <bold>{}</> with <bold>--output</>; run <bold>wt config update</> to apply the migration in place",
candidate.info.label().to_lowercase()
"Cannot overwrite <bold>{label}</> with <bold>--output</>; to apply the migration in place, run <bold>wt config update</>"
));
}
let display_path = format_path_for_display(&output);

eprint!("{}", format_dropped_approvals_warning(candidate));
let approvals_warning = format_dropped_approvals_warning(candidate);
eprint!("{approvals_warning}");

let replace = output.exists();
if replace && !yes {
if !std::io::stdin().is_terminal() {
bail!(cformat!(
"{display_path} already exists; to overwrite it with the {label} migration, add <bold>--yes</>"
));
}
if !approvals_warning.is_empty() {
eprintln!();
}
let prompt = format!("Overwrite {display_path} with the {label} migration?");
match prompt_yes_no_preview(&prompt, || {})? {
PromptResponse::Accepted => {}
PromptResponse::Declined => {
eprintln!("{}", info_message("Update cancelled"));
return Ok(());
}
}
}

let artifact = format_migrated_output(candidates);
worktrunk::utils::write_atomically(&output, &artifact).with_context(|| {
format!(
"Failed to write output @ {}",
format_path_for_display(&output)
)
})?;
let written = if replace {
write_atomically(&output, &artifact)
} else {
write_new_atomically(&output, &artifact)
};
written.with_context(|| format!("Failed to write output @ {display_path}"))?;
eprintln!(
"{}",
success_message(format!(
"Wrote {} migration @ {}",
candidate.info.label().to_lowercase(),
format_path_for_display(&output)
))
success_message(format!("Wrote {label} migration @ {display_path}"))
);
Ok(())
}
Expand Down Expand Up @@ -236,7 +269,7 @@ fn format_dropped_approvals_warning(candidate: &UpdateCandidate) -> String {
format!(
"{}\n",
warning_message(cformat!(
"Output omits deprecated <bold>approved-commands</>; run <underline>wt config update</> to migrate them to approvals.toml"
"Output omits deprecated <bold>approved-commands</>; to migrate them to approvals.toml, run <bold>wt config update</>"
))
)
}
Expand Down
2 changes: 1 addition & 1 deletion src/commands/relocate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ pub fn gather_candidates(
bail!(
"{}",
cformat!(
"Cannot relocate worktree @ {} — its directory is gone; run <bold>wt step prune</> to clear the entry",
"Cannot relocate worktree @ {} — its directory is gone; to clear the entry, run <bold>wt step prune</>",
format_path_for_display(&path)
)
);
Expand Down
4 changes: 2 additions & 2 deletions src/config/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,8 @@ impl ProjectConfig {
},
};

// Check for deprecated template variables and create migration file if needed
// Only write migration file in main worktree, not linked worktrees
// Check for deprecated patterns. They are actionable only from the main
// worktree, where `wt config update` rewrites the file.
// emit_inline_warnings=true: print per-kind warnings inline during config load
let is_main_worktree = !repo.current_worktree().is_linked().unwrap_or(true);
let repo_for_hints = if write_hints { Some(repo) } else { None };
Expand Down
Loading
Loading