From eb2d5fc5cef595d272ecc19a118888060fd0b3fe Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 15:47:15 +0200 Subject: [PATCH 01/38] fix(init): strip hash suffix when unloading project aliases in force-init - force-init reads _AM_PROJECT_ALIASES which now uses name|hash format - the raw entries were passed to force_unalias, causing shell errors where | was interpreted as a pipe (e.g. `functions -e foo|0de26fd`) - strip the |hash suffix before passing to unalias --- crates/am/src/update.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs index 86623464..8bacf883 100644 --- a/crates/am/src/update.rs +++ b/crates/am/src/update.rs @@ -597,6 +597,8 @@ pub fn update(model: &mut AppModel, message: Message) -> Result Date: Wed, 22 Apr 2026 16:37:27 +0200 Subject: [PATCH 02/38] fix(hook): restore shadowed profile/global aliases on project unload - add profile_aliases parameter to generate_hook_with_security - after unloading project aliases, re-emit any global/profile alias that was shadowed by the now-removed project alias - in incremental (trusted) path, only restore for removed aliases (changed aliases get reloaded with new project value) - resolve global + active profile aliases in update.rs Hook handler - update snapshot tests to pass new parameter - add tests for leave, tamper, incremental remove, and no-shadow cases --- crates/am/src/hook.rs | 201 ++++++++++++++++++++++++++++++++++- crates/am/src/update.rs | 11 ++ crates/am/tests/snapshots.rs | 12 +-- 3 files changed, 217 insertions(+), 7 deletions(-) diff --git a/crates/am/src/hook.rs b/crates/am/src/hook.rs index 91f0ad52..d9a7f62e 100644 --- a/crates/am/src/hook.rs +++ b/crates/am/src/hook.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; use std::path::Path; +use crate::alias::AliasSet; use crate::env_vars; use crate::project::ProjectAliases; use crate::security::{SecurityConfig, TrustStatus}; @@ -47,6 +48,7 @@ pub fn generate_hook(ctx: &ShellContext, previous_aliases: Option<&str>) -> crat prev_project_path.as_deref(), &mut security, false, + &AliasSet::default(), )?; Ok(output) } @@ -68,6 +70,7 @@ pub fn generate_hook_with_security( prev_project_path: Option<&str>, security_config: &mut SecurityConfig, quiet: bool, + profile_aliases: &AliasSet, ) -> crate::Result<(String, bool)> { let shell_impl = ctx.shell.clone().as_shell( ctx.cfg, @@ -94,6 +97,17 @@ pub fn generate_hook_with_security( } }; + // Helper: after unloading project aliases, re-emit any global/profile + // aliases that were shadowed by the now-removed project alias. + let restore_shadowed = |lines: &mut Vec, names: &[String]| { + for name in names { + if let Some(alias) = profile_aliases.get(&crate::alias::AliasName::from(name.as_str())) + { + lines.push(shell_impl.alias(&alias.as_entry(name))); + } + } + }; + let project_path = ProjectAliases::find_path(cwd)?; match project_path { @@ -188,6 +202,13 @@ pub fn generate_hook_with_security( } } + // Restore global/profile aliases that were shadowed by + // removed project aliases. Changed aliases will be + // reloaded with the new project value below. + let removed_shell_names: Vec = + removed.iter().filter(|n| !n.contains(':')).cloned().collect(); + restore_shadowed(&mut lines, &removed_shell_names); + // 2. Show messages if show_messages { if is_fresh_load { @@ -289,6 +310,7 @@ pub fn generate_hook_with_security( } TrustStatus::Unknown => { unload_prev(&mut lines); + restore_shadowed(&mut lines, &unload_prev_names); if show_messages { lines.push(shell_impl.echo( "am: .aliases found but not trusted. Run 'am trust' to review and allow.", @@ -297,9 +319,11 @@ pub fn generate_hook_with_security( } TrustStatus::Untrusted => { unload_prev(&mut lines); + restore_shadowed(&mut lines, &unload_prev_names); } TrustStatus::Tampered => { unload_prev(&mut lines); + restore_shadowed(&mut lines, &unload_prev_names); security_changed = true; if show_messages { lines.push(shell_impl.echo( @@ -325,6 +349,7 @@ pub fn generate_hook_with_security( None => { if !prev.is_empty() { unload_prev(&mut lines); + restore_shadowed(&mut lines, &unload_prev_names); if !quiet { let prev_names: Vec<&str> = unload_prev_names.iter().map(|s| s.as_str()).collect(); @@ -446,6 +471,16 @@ mod tests { } fn run(&mut self, shell: &Shell, cwd: &Path, prev: Option<&str>) -> (String, bool) { + self.run_with_profile_aliases(shell, cwd, prev, &AliasSet::default()) + } + + fn run_with_profile_aliases( + &mut self, + shell: &Shell, + cwd: &Path, + prev: Option<&str>, + profile_aliases: &AliasSet, + ) -> (String, bool) { use crate::config::ShellsTomlConfig; let cfg = ShellsTomlConfig::default(); let ctx = ShellContext { @@ -455,7 +490,15 @@ mod tests { external_functions: Default::default(), external_aliases: Default::default(), }; - generate_hook_with_security(&ctx, prev, None, &mut self.security, false).unwrap() + generate_hook_with_security( + &ctx, + prev, + None, + &mut self.security, + false, + profile_aliases, + ) + .unwrap() } /// Update the .aliases content and re-trust. @@ -965,4 +1008,160 @@ mod tests { let prev = extract_prev_aliases(output, &Shell::Bash); assert_eq!(prev, Some("b|abc1234,t|def5678".to_string())); } + + // ─── Shadow restoration tests ────────────────────────────────── + + #[test] + fn test_hook_restores_shadowed_profile_alias_on_leave() { + let mut t = TestBed::new() + .with_aliases("[aliases]\nt = \"cargo test --release\"\n") + .with_security_trusted() + .setup(); + + let cwd = t.root(); + + // Build a profile alias set that also has "t" + let mut profile_aliases = AliasSet::default(); + profile_aliases.insert("t".into(), crate::TomlAlias::Command("cargo test".into())); + + // First: load project aliases + let (output, _) = + t.run_with_profile_aliases(&Shell::Fish, &cwd, None, &profile_aliases); + assert!(output.contains("alias t \"cargo test --release\"")); + let prev = extract_prev_aliases(&output, &Shell::Fish); + + // Now simulate cd away (no .aliases in new dir) + let empty_dir = tempfile::tempdir().unwrap(); + let (output, _) = t.run_with_profile_aliases( + &Shell::Fish, + empty_dir.path(), + prev.as_deref(), + &profile_aliases, + ); + + // Project alias "t" should be unloaded + assert!( + output.contains("functions -e t"), + "should unload project alias t, got: {output}" + ); + // Profile alias "t" should be restored + assert!( + output.contains("alias t \"cargo test\""), + "should restore profile alias t, got: {output}" + ); + } + + #[test] + fn test_hook_restores_shadowed_alias_on_untrusted() { + let mut t = TestBed::new() + .with_aliases("[aliases]\nt = \"cargo test --release\"\n") + .with_security_trusted() + .setup(); + + let cwd = t.root(); + let mut profile_aliases = AliasSet::default(); + profile_aliases.insert("t".into(), crate::TomlAlias::Command("cargo test".into())); + + // Load project aliases + let (output, _) = + t.run_with_profile_aliases(&Shell::Fish, &cwd, None, &profile_aliases); + let prev = extract_prev_aliases(&output, &Shell::Fish); + + // Tamper with the file (change without re-trusting) + std::fs::write(t.dir.path().join(".aliases"), "[aliases]\nt = \"hacked\"\n").unwrap(); + + // Hook detects tamper -> unloads project aliases -> should restore profile alias + let (output, _) = t.run_with_profile_aliases( + &Shell::Fish, + &cwd, + prev.as_deref(), + &profile_aliases, + ); + assert!( + output.contains("functions -e t"), + "should unload project alias t, got: {output}" + ); + assert!( + output.contains("alias t \"cargo test\""), + "should restore profile alias t, got: {output}" + ); + } + + #[test] + fn test_hook_restores_shadowed_alias_on_incremental_remove() { + let mut t = TestBed::new() + .with_aliases("[aliases]\nb = \"make build\"\nt = \"cargo test --release\"\n") + .with_security_trusted() + .setup(); + + let cwd = t.root(); + let mut profile_aliases = AliasSet::default(); + profile_aliases.insert("t".into(), crate::TomlAlias::Command("cargo test".into())); + + // Load both project aliases + let (output, _) = + t.run_with_profile_aliases(&Shell::Fish, &cwd, None, &profile_aliases); + let prev = extract_prev_aliases(&output, &Shell::Fish); + + // Remove "t" from project (keep "b") + t.update_aliases("[aliases]\nb = \"make build\"\n"); + + let (output, _) = t.run_with_profile_aliases( + &Shell::Fish, + &cwd, + prev.as_deref(), + &profile_aliases, + ); + + // "t" was removed from project -> should be unloaded then restored from profile + assert!( + output.contains("functions -e t"), + "removed alias t should be unloaded, got: {output}" + ); + assert!( + output.contains("alias t \"cargo test\""), + "profile alias t should be restored after project removal, got: {output}" + ); + // "b" should NOT be touched (unchanged) + assert!( + !output.contains("functions -e b"), + "unchanged alias b should not be unloaded, got: {output}" + ); + } + + #[test] + fn test_hook_no_restore_when_no_profile_shadow() { + let mut t = TestBed::new() + .with_aliases("[aliases]\nt = \"cargo test --release\"\n") + .with_security_trusted() + .setup(); + + let cwd = t.root(); + + // Empty profile — no shadow to restore + let profile_aliases = AliasSet::default(); + + let (output, _) = + t.run_with_profile_aliases(&Shell::Fish, &cwd, None, &profile_aliases); + let prev = extract_prev_aliases(&output, &Shell::Fish); + + // cd away + let empty_dir = tempfile::tempdir().unwrap(); + let (output, _) = t.run_with_profile_aliases( + &Shell::Fish, + empty_dir.path(), + prev.as_deref(), + &profile_aliases, + ); + + assert!( + output.contains("functions -e t"), + "should unload project alias t, got: {output}" + ); + // No restore — there was no profile alias to bring back + assert!( + !output.contains("alias t"), + "should not restore any alias when profile has none, got: {output}" + ); + } } diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs index 8bacf883..f8a49671 100644 --- a/crates/am/src/update.rs +++ b/crates/am/src/update.rs @@ -664,12 +664,23 @@ pub fn update(model: &mut AppModel, message: Message) -> Result Date: Wed, 22 Apr 2026 20:22:40 +0200 Subject: [PATCH 03/38] feat: scaffold precedence module with public types - add Precedence builder, PrecedenceDiff, EffectiveEntry, EntryKind - resolve() returns empty diff for now - derive PartialEq on TomlAlias/AliasDetail so EntryKind::Alias compiles --- crates/am/src/alias.rs | 4 +-- crates/am/src/lib.rs | 1 + crates/am/src/precedence.rs | 70 +++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 crates/am/src/precedence.rs diff --git a/crates/am/src/alias.rs b/crates/am/src/alias.rs index 948f9899..51405f10 100644 --- a/crates/am/src/alias.rs +++ b/crates/am/src/alias.rs @@ -101,14 +101,14 @@ impl AliasSet { } } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(untagged)] pub enum TomlAlias { Command(String), Detailed(AliasDetail), } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct AliasDetail { pub command: String, pub description: Option, diff --git a/crates/am/src/lib.rs b/crates/am/src/lib.rs index 89509ee9..711bdc7a 100644 --- a/crates/am/src/lib.rs +++ b/crates/am/src/lib.rs @@ -12,6 +12,7 @@ pub mod hook; pub mod import_export; pub mod init; pub mod messages; +pub mod precedence; pub mod profile; pub mod project; pub mod prompt; diff --git a/crates/am/src/precedence.rs b/crates/am/src/precedence.rs new file mode 100644 index 00000000..5a7cd86a --- /dev/null +++ b/crates/am/src/precedence.rs @@ -0,0 +1,70 @@ +use std::collections::{BTreeMap, BTreeSet, HashSet}; + +use crate::alias::{AliasName, AliasSet, TomlAlias}; +use crate::subcommand::{SubcommandEntry, SubcommandSet}; + +#[derive(Debug, Clone, PartialEq)] +pub enum EntryKind { + Alias(TomlAlias), + SubcommandWrapper { + program: String, + entries: Vec, + base_cmd: Option, + }, + /// Per-key subcommand entry tracked in `_AM_SUBCOMMANDS` for fine-grained + /// change detection. Never emitted as shell code — the program-level + /// `SubcommandWrapper` is the shell-visible unit. + SubcommandKey { + longs: Vec, + }, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct EffectiveEntry { + pub name: String, + pub kind: EntryKind, + pub hash: String, +} + +#[derive(Debug, Default, Clone, PartialEq)] +pub struct PrecedenceDiff { + pub added: Vec, + pub changed: Vec, + pub removed: Vec, + pub unchanged: Vec, +} + +#[derive(Debug, Default)] +pub struct Precedence { + global_aliases: AliasSet, + global_subcommands: SubcommandSet, + profile_aliases: AliasSet, + profile_subcommands: SubcommandSet, + project_aliases: AliasSet, + project_subcommands: SubcommandSet, + shell_alias_state: BTreeMap>, + shell_subcmd_state: BTreeMap>, + external_functions: HashSet, + external_aliases: HashSet, +} + +impl Precedence { + pub fn new() -> Self { + Self::default() + } + + pub fn resolve(self) -> PrecedenceDiff { + PrecedenceDiff::default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_inputs_produce_empty_diff() { + let diff = Precedence::new().resolve(); + assert_eq!(diff, PrecedenceDiff::default()); + } +} From dbb0188cae591402b9faf4878f6332413aa2f2f1 Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 20:27:33 +0200 Subject: [PATCH 04/38] feat: layer merge with project > profile > global - with_global/with_profiles/with_project consume AliasSet/SubcommandSet - merged_aliases/merged_subcommands overlay by name in precedence order --- crates/am/src/precedence.rs | 84 +++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/crates/am/src/precedence.rs b/crates/am/src/precedence.rs index 5a7cd86a..fca61c96 100644 --- a/crates/am/src/precedence.rs +++ b/crates/am/src/precedence.rs @@ -53,6 +53,53 @@ impl Precedence { Self::default() } + pub fn with_global(mut self, aliases: &AliasSet, subs: &SubcommandSet) -> Self { + self.global_aliases = aliases.clone(); + self.global_subcommands = subs.clone(); + self + } + + pub fn with_profiles(mut self, aliases: &AliasSet, subs: &SubcommandSet) -> Self { + self.profile_aliases = aliases.clone(); + self.profile_subcommands = subs.clone(); + self + } + + pub fn with_project(mut self, aliases: &AliasSet, subs: &SubcommandSet) -> Self { + self.project_aliases = aliases.clone(); + self.project_subcommands = subs.clone(); + self + } + + /// Internal: merged alias set keyed by shell-visible name, + /// with project > profile > global precedence. + fn merged_aliases(&self) -> BTreeMap { + let mut out = BTreeMap::new(); + for layer in [&self.global_aliases, &self.profile_aliases, &self.project_aliases] { + for (name, alias) in layer.iter() { + out.insert(name.as_ref().to_string(), alias.clone()); + } + } + out + } + + /// Internal: merged subcommand set keyed by full "program:seg:..." key, + /// with project > profile > global precedence. + fn merged_subcommands(&self) -> SubcommandSet { + let mut out = SubcommandSet::new(); + for layer in [&self.global_subcommands, &self.profile_subcommands, &self.project_subcommands] { + for (k, v) in layer { + out.insert(k.clone(), v.clone()); + } + } + out + } + + #[cfg(test)] + fn merged_aliases_for_test(&self) -> BTreeMap { + self.merged_aliases() + } + pub fn resolve(self) -> PrecedenceDiff { PrecedenceDiff::default() } @@ -67,4 +114,41 @@ mod tests { let diff = Precedence::new().resolve(); assert_eq!(diff, PrecedenceDiff::default()); } + + fn aset(pairs: &[(&str, &str)]) -> AliasSet { + let mut s = AliasSet::default(); + for (n, c) in pairs { + s.insert(AliasName::from(*n), TomlAlias::Command((*c).into())); + } + s + } + + #[test] + fn merge_project_overrides_profile_overrides_global() { + let global = aset(&[("ll", "ls -lha"), ("t", "global-t")]); + let profile = aset(&[("gs", "git status"), ("t", "profile-t")]); + let project = aset(&[("b", "make build"), ("t", "project-t")]); + + let p = Precedence::new() + .with_global(&global, &SubcommandSet::new()) + .with_profiles(&profile, &SubcommandSet::new()) + .with_project(&project, &SubcommandSet::new()); + + let merged = p.merged_aliases_for_test(); + assert_eq!(merged.get("ll").unwrap().command(), "ls -lha"); + assert_eq!(merged.get("gs").unwrap().command(), "git status"); + assert_eq!(merged.get("b").unwrap().command(), "make build"); + assert_eq!(merged.get("t").unwrap().command(), "project-t"); + } + + #[test] + fn merge_without_project_falls_back_to_profile() { + let global = aset(&[("t", "global-t")]); + let profile = aset(&[("t", "profile-t")]); + let p = Precedence::new() + .with_global(&global, &SubcommandSet::new()) + .with_profiles(&profile, &SubcommandSet::new()); + let merged = p.merged_aliases_for_test(); + assert_eq!(merged.get("t").unwrap().command(), "profile-t"); + } } From 1dc12d9d0bbf8e95e034cf6d0a7ea97e30849324 Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 20:30:03 +0200 Subject: [PATCH 05/38] feat: content hashing for aliases and subcommand programs - alias_hash: blake3(command)[..7] - subcmd_program_hash: blake3 of 'key=long,long;key=long' serialization - subcmd_key_hash: blake3 of 'long,long' --- crates/am/src/precedence.rs | 67 +++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/crates/am/src/precedence.rs b/crates/am/src/precedence.rs index fca61c96..4bbabc4b 100644 --- a/crates/am/src/precedence.rs +++ b/crates/am/src/precedence.rs @@ -95,11 +95,44 @@ impl Precedence { out } + fn alias_hash(alias: &TomlAlias) -> String { + crate::trust::compute_short_hash(alias.command().as_bytes()) + } + + fn subcmd_program_hash(program: &str, subs: &SubcommandSet) -> String { + let entries_str: String = subs + .iter() + .filter(|(k, _)| k.starts_with(&format!("{program}:"))) + .map(|(k, v)| format!("{k}={}", v.join(","))) + .collect::>() + .join(";"); + crate::trust::compute_short_hash(entries_str.as_bytes()) + } + + fn subcmd_key_hash(longs: &[String]) -> String { + crate::trust::compute_short_hash(longs.join(",").as_bytes()) + } + #[cfg(test)] fn merged_aliases_for_test(&self) -> BTreeMap { self.merged_aliases() } + #[cfg(test)] + pub(crate) fn alias_hash_for_test(alias: &TomlAlias) -> String { + Self::alias_hash(alias) + } + + #[cfg(test)] + pub(crate) fn subcmd_program_hash_for_test(program: &str, subs: &SubcommandSet) -> String { + Self::subcmd_program_hash(program, subs) + } + + #[cfg(test)] + pub(crate) fn subcmd_key_hash_for_test(longs: &[String]) -> String { + Self::subcmd_key_hash(longs) + } + pub fn resolve(self) -> PrecedenceDiff { PrecedenceDiff::default() } @@ -151,4 +184,38 @@ mod tests { let merged = p.merged_aliases_for_test(); assert_eq!(merged.get("t").unwrap().command(), "profile-t"); } + + #[test] + fn hash_alias_stable_and_differs_by_command() { + let a = TomlAlias::Command("make build".into()); + let b = TomlAlias::Command("cargo build".into()); + let h_a = Precedence::alias_hash_for_test(&a); + let h_b = Precedence::alias_hash_for_test(&b); + assert_eq!(h_a.len(), 7); + assert_ne!(h_a, h_b); + assert_eq!(h_a, Precedence::alias_hash_for_test(&a)); + } + + #[test] + fn hash_subcmd_program_includes_all_entries_under_it() { + let mut a = SubcommandSet::new(); + a.insert("jj:ab".into(), vec!["abandon".into()]); + let mut b = a.clone(); + b.insert("jj:bl".into(), vec!["branch".into(), "list".into()]); + + let h_a = Precedence::subcmd_program_hash_for_test("jj", &a); + let h_b = Precedence::subcmd_program_hash_for_test("jj", &b); + assert_eq!(h_a.len(), 7); + assert_ne!(h_a, h_b, "adding jj:bl must change jj program hash"); + } + + #[test] + fn hash_subcmd_key_hashes_long_subcommands() { + let key_hash = Precedence::subcmd_key_hash_for_test(&["branch".into(), "list".into()]); + assert_eq!(key_hash.len(), 7); + assert_eq!( + key_hash, + Precedence::subcmd_key_hash_for_test(&["branch".into(), "list".into()]) + ); + } } From 20fd21e842b91e2bd00627e476247d0d24d58845 Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 20:32:38 +0200 Subject: [PATCH 06/38] feat: parse shell state from _AM_ALIASES / _AM_SUBCOMMANDS - name|hash form stored as Some(hash) - bare name (backward compat) stored as None -> diff treats as always-differs --- crates/am/src/precedence.rs | 80 +++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/crates/am/src/precedence.rs b/crates/am/src/precedence.rs index 4bbabc4b..273a14e0 100644 --- a/crates/am/src/precedence.rs +++ b/crates/am/src/precedence.rs @@ -71,6 +71,16 @@ impl Precedence { self } + pub fn with_shell_state_from_env( + mut self, + aliases: Option<&str>, + subcommands: Option<&str>, + ) -> Self { + self.shell_alias_state = Self::parse_state(aliases); + self.shell_subcmd_state = Self::parse_state(subcommands); + self + } + /// Internal: merged alias set keyed by shell-visible name, /// with project > profile > global precedence. fn merged_aliases(&self) -> BTreeMap { @@ -113,6 +123,21 @@ impl Precedence { crate::trust::compute_short_hash(longs.join(",").as_bytes()) } + fn parse_state(raw: Option<&str>) -> BTreeMap> { + let mut map = BTreeMap::new(); + let Some(s) = raw.filter(|s| !s.is_empty()) else { + return map; + }; + for entry in s.split(',') { + if let Some((name, hash)) = entry.split_once('|') { + map.insert(name.to_string(), Some(hash.to_string())); + } else { + map.insert(entry.to_string(), None); + } + } + map + } + #[cfg(test)] fn merged_aliases_for_test(&self) -> BTreeMap { self.merged_aliases() @@ -133,6 +158,16 @@ impl Precedence { Self::subcmd_key_hash(longs) } + #[cfg(test)] + pub(crate) fn shell_alias_state_for_test(&self) -> &BTreeMap> { + &self.shell_alias_state + } + + #[cfg(test)] + pub(crate) fn shell_subcmd_state_for_test(&self) -> &BTreeMap> { + &self.shell_subcmd_state + } + pub fn resolve(self) -> PrecedenceDiff { PrecedenceDiff::default() } @@ -218,4 +253,49 @@ mod tests { Precedence::subcmd_key_hash_for_test(&["branch".into(), "list".into()]) ); } + + #[test] + fn parse_shell_state_new_format() { + let p = Precedence::new() + .with_shell_state_from_env(Some("b|abc1234,t|def5678"), None); + let aliases = p.shell_alias_state_for_test(); + assert_eq!(aliases.get("b"), Some(&Some("abc1234".into()))); + assert_eq!(aliases.get("t"), Some(&Some("def5678".into()))); + } + + #[test] + fn parse_shell_state_old_name_only_format_treated_as_unknown() { + let p = Precedence::new().with_shell_state_from_env(Some("b,t"), None); + let aliases = p.shell_alias_state_for_test(); + assert_eq!(aliases.get("b"), Some(&None)); + assert_eq!(aliases.get("t"), Some(&None)); + } + + #[test] + fn parse_shell_state_empty_and_none() { + let p1 = Precedence::new().with_shell_state_from_env(None, None); + assert!(p1.shell_alias_state_for_test().is_empty()); + let p2 = Precedence::new().with_shell_state_from_env(Some(""), None); + assert!(p2.shell_alias_state_for_test().is_empty()); + } + + #[test] + fn parse_shell_state_mixed_format() { + let p = Precedence::new() + .with_shell_state_from_env(Some("b|abc1234,t,gs|fed9876"), None); + let aliases = p.shell_alias_state_for_test(); + assert_eq!(aliases.get("b"), Some(&Some("abc1234".into()))); + assert_eq!(aliases.get("t"), Some(&None)); + assert_eq!(aliases.get("gs"), Some(&Some("fed9876".into()))); + } + + #[test] + fn parse_shell_state_subcommands_stored_separately() { + let p = Precedence::new() + .with_shell_state_from_env(Some("b|aaa0000"), Some("jj|bbb1111,jj:ab|ccc2222")); + assert!(p.shell_alias_state_for_test().contains_key("b")); + let subs = p.shell_subcmd_state_for_test(); + assert_eq!(subs.get("jj"), Some(&Some("bbb1111".into()))); + assert_eq!(subs.get("jj:ab"), Some(&Some("ccc2222".into()))); + } } From 91a9f3f3f135f5ed54b838d1cd5ef9a1e2d1de5a Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 20:35:18 +0200 Subject: [PATCH 07/38] feat: diff algorithm for regular aliases - added/changed/removed/unchanged derived from effective vs shell_alias_state - shadow restoration is automatic: removed project layer flips hash -> Changed --- crates/am/src/precedence.rs | 135 +++++++++++++++++++++++++++++++++++- 1 file changed, 134 insertions(+), 1 deletion(-) diff --git a/crates/am/src/precedence.rs b/crates/am/src/precedence.rs index 273a14e0..ab2ab37c 100644 --- a/crates/am/src/precedence.rs +++ b/crates/am/src/precedence.rs @@ -169,7 +169,42 @@ impl Precedence { } pub fn resolve(self) -> PrecedenceDiff { - PrecedenceDiff::default() + let mut effective: BTreeMap = BTreeMap::new(); + + for (name, alias) in self.merged_aliases() { + let hash = Self::alias_hash(&alias); + effective.insert( + name.clone(), + EffectiveEntry { + name, + kind: EntryKind::Alias(alias), + hash, + }, + ); + } + + let mut diff = PrecedenceDiff::default(); + + for (name, _prev_hash) in &self.shell_alias_state { + if !effective.contains_key(name) { + diff.removed.push(name.clone()); + } + } + + for (name, entry) in effective { + match self.shell_alias_state.get(&name) { + None => diff.added.push(entry), + Some(prev) => { + if prev.as_deref() == Some(entry.hash.as_str()) { + diff.unchanged.push(entry); + } else { + diff.changed.push(entry); + } + } + } + } + + diff } } @@ -298,4 +333,102 @@ mod tests { assert_eq!(subs.get("jj"), Some(&Some("bbb1111".into()))); assert_eq!(subs.get("jj:ab"), Some(&Some("ccc2222".into()))); } + + fn find<'a>(v: &'a [EffectiveEntry], name: &str) -> Option<&'a EffectiveEntry> { + v.iter().find(|e| e.name == name) + } + + fn cmd_of(entry: &EffectiveEntry) -> &str { + match &entry.kind { + EntryKind::Alias(a) => a.command(), + _ => panic!("expected Alias, got {:?}", entry.kind), + } + } + + #[test] + fn resolve_fresh_load_everything_added() { + let global = aset(&[("ll", "ls -lha")]); + let profile = aset(&[("gs", "git status")]); + let project = aset(&[("b", "make build")]); + let diff = Precedence::new() + .with_global(&global, &SubcommandSet::new()) + .with_profiles(&profile, &SubcommandSet::new()) + .with_project(&project, &SubcommandSet::new()) + .resolve(); + let added_names: BTreeSet<_> = diff.added.iter().map(|e| e.name.as_str()).collect(); + assert_eq!( + added_names, + BTreeSet::from(["ll", "gs", "b"]), + ); + assert!(diff.changed.is_empty()); + assert!(diff.removed.is_empty()); + assert!(diff.unchanged.is_empty()); + } + + #[test] + fn resolve_unchanged_when_hashes_match() { + let project = aset(&[("b", "make build")]); + let hash = Precedence::alias_hash_for_test(&TomlAlias::Command("make build".into())); + let prev = format!("b|{hash}"); + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .with_shell_state_from_env(Some(&prev), None) + .resolve(); + assert!(diff.added.is_empty()); + assert!(diff.changed.is_empty()); + assert!(diff.removed.is_empty()); + assert_eq!(diff.unchanged.len(), 1); + assert_eq!(diff.unchanged[0].name, "b"); + } + + #[test] + fn resolve_changed_when_hash_differs() { + let project = aset(&[("b", "cargo build")]); + let prev = "b|0000000"; // obviously not the real hash + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .with_shell_state_from_env(Some(prev), None) + .resolve(); + assert_eq!(diff.changed.len(), 1); + assert_eq!(cmd_of(&diff.changed[0]), "cargo build"); + assert!(diff.added.is_empty()); + assert!(diff.removed.is_empty()); + } + + #[test] + fn resolve_backward_compat_bare_name_triggers_reload() { + let project = aset(&[("b", "make build")]); + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .with_shell_state_from_env(Some("b"), None) // old format + .resolve(); + assert_eq!(diff.changed.len(), 1); + assert_eq!(diff.changed[0].name, "b"); + } + + #[test] + fn resolve_removed_when_no_layer_contains_name() { + let diff = Precedence::new() + .with_shell_state_from_env(Some("gone|abc1234"), None) + .resolve(); + assert_eq!(diff.removed, vec!["gone".to_string()]); + } + + #[test] + fn resolve_shadow_restoration_via_changed_entry() { + // Previous session: project 't' shadowed profile 't'. Now project layer is + // gone (we left the project directory). Effective 't' reverts to profile. + // The stored hash was the project's; the new effective hash is the profile's. + // This must be detected as Changed -> the shell reloads with the profile value. + let profile = aset(&[("t", "profile-t")]); + let project_hash = Precedence::alias_hash_for_test(&TomlAlias::Command("project-t".into())); + let prev = format!("t|{project_hash}"); + let diff = Precedence::new() + .with_profiles(&profile, &SubcommandSet::new()) + .with_shell_state_from_env(Some(&prev), None) + .resolve(); + assert_eq!(diff.changed.len(), 1, "shadow restoration must emit a reload"); + assert_eq!(cmd_of(&diff.changed[0]), "profile-t"); + assert!(diff.removed.is_empty()); + } } From 40867d2a1463b219b524781f8648c421e07d625d Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 20:42:42 +0200 Subject: [PATCH 08/38] feat: subcommand precedence and program-level wrappers - per-key precedence: project jj:ab wins over profile jj:ab - different keys under same program coexist across layers - wrapper entry hash = hash of all merged entries under program - per-key SubcommandKey entries tracked in _AM_SUBCOMMANDS - regular alias absorbed into wrapper sets base_cmd --- crates/am/src/precedence.rs | 243 ++++++++++++++++++++++++++++++++++-- 1 file changed, 231 insertions(+), 12 deletions(-) diff --git a/crates/am/src/precedence.rs b/crates/am/src/precedence.rs index ab2ab37c..653c0f2c 100644 --- a/crates/am/src/precedence.rs +++ b/crates/am/src/precedence.rs @@ -168,16 +168,90 @@ impl Precedence { &self.shell_subcmd_state } + /// Permissive grouping of subcommand entries by program. Unlike + /// [`crate::subcommand::group_by_program`], this does NOT enforce equal + /// short/long counts — it treats `long_subcommands` as the verbatim + /// expansion for a given short key (e.g. `ab` → `abandon --force`). + fn group_subcommands_by_program( + subs: &SubcommandSet, + ) -> BTreeMap> { + let mut groups: BTreeMap> = BTreeMap::new(); + for (key, longs) in subs { + let Some((program, rest)) = key.split_once(':') else { + continue; + }; + if program.is_empty() || rest.is_empty() { + continue; + } + let short_subcommands: Vec = + rest.split(':').map(|s| s.to_string()).collect(); + if short_subcommands.iter().any(|s| s.is_empty()) { + continue; + } + groups.entry(program.to_string()).or_default().push(SubcommandEntry { + program: program.to_string(), + short_subcommands, + long_subcommands: longs.clone(), + }); + } + groups + } + pub fn resolve(self) -> PrecedenceDiff { + let merged_aliases = self.merged_aliases(); + let merged_subcommands = self.merged_subcommands(); + let subcmd_groups = Self::group_subcommands_by_program(&merged_subcommands); + let program_names: BTreeSet = subcmd_groups.keys().cloned().collect(); + let mut effective: BTreeMap = BTreeMap::new(); - for (name, alias) in self.merged_aliases() { - let hash = Self::alias_hash(&alias); + // Regular aliases — skip names absorbed by a subcommand wrapper. + for (name, alias) in merged_aliases.iter() { + if program_names.contains(name) { + continue; + } + let hash = Self::alias_hash(alias); effective.insert( name.clone(), EffectiveEntry { - name, - kind: EntryKind::Alias(alias), + name: name.clone(), + kind: EntryKind::Alias(alias.clone()), + hash, + }, + ); + } + + // Subcommand wrappers (one entry per program). + for (program, entries) in &subcmd_groups { + let base_cmd = merged_aliases + .get(program) + .map(|a| a.command().to_string()); + let hash = Self::subcmd_program_hash(program, &merged_subcommands); + effective.insert( + program.clone(), + EffectiveEntry { + name: program.clone(), + kind: EntryKind::SubcommandWrapper { + program: program.clone(), + entries: entries.clone(), + base_cmd, + }, + hash, + }, + ); + } + + // Per-key subcommand tracking for `_AM_SUBCOMMANDS`. + let mut effective_subkeys: BTreeMap = BTreeMap::new(); + for (key, longs) in merged_subcommands.iter() { + let hash = Self::subcmd_key_hash(longs); + effective_subkeys.insert( + key.clone(), + EffectiveEntry { + name: key.clone(), + kind: EntryKind::SubcommandKey { + longs: longs.clone(), + }, hash, }, ); @@ -185,22 +259,43 @@ impl Precedence { let mut diff = PrecedenceDiff::default(); - for (name, _prev_hash) in &self.shell_alias_state { + // --- Regular + wrapper diff against shell_alias_state --- + for (name, _) in &self.shell_alias_state { if !effective.contains_key(name) { diff.removed.push(name.clone()); } } - for (name, entry) in effective { match self.shell_alias_state.get(&name) { None => diff.added.push(entry), - Some(prev) => { - if prev.as_deref() == Some(entry.hash.as_str()) { - diff.unchanged.push(entry); - } else { - diff.changed.push(entry); - } + Some(prev) if prev.as_deref() == Some(entry.hash.as_str()) => { + diff.unchanged.push(entry) + } + Some(_) => diff.changed.push(entry), + } + } + + // --- Per-key subcommand diff against shell_subcmd_state --- + // + // The program-level wrapper already lives in `effective`/`diff` above. + // Here we additionally track individual keys so they appear in + // `_AM_SUBCOMMANDS` with fine-grained hashes. + for (name, _) in &self.shell_subcmd_state { + // A program-level entry (no ':') is tracked in shell_alias_state, not here. + if !name.contains(':') { + continue; + } + if !effective_subkeys.contains_key(name) { + diff.removed.push(name.clone()); + } + } + for (name, entry) in effective_subkeys { + match self.shell_subcmd_state.get(&name) { + None => diff.added.push(entry), + Some(prev) if prev.as_deref() == Some(entry.hash.as_str()) => { + diff.unchanged.push(entry) } + Some(_) => diff.changed.push(entry), } } @@ -431,4 +526,128 @@ mod tests { assert_eq!(cmd_of(&diff.changed[0]), "profile-t"); assert!(diff.removed.is_empty()); } + + fn subset(pairs: &[(&str, &[&str])]) -> SubcommandSet { + let mut s = SubcommandSet::new(); + for (k, longs) in pairs { + s.insert((*k).into(), longs.iter().map(|x| (*x).into()).collect()); + } + s + } + + #[test] + fn resolve_subcommand_fresh_load_emits_wrapper() { + let project_subs = subset(&[("jj:ab", &["abandon"])]); + let diff = Precedence::new() + .with_project(&AliasSet::default(), &project_subs) + .resolve(); + let wrapper = find(&diff.added, "jj").expect("expected jj wrapper in added"); + match &wrapper.kind { + EntryKind::SubcommandWrapper { program, entries, base_cmd } => { + assert_eq!(program, "jj"); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].short_subcommands, vec!["ab"]); + assert_eq!(entries[0].long_subcommands, vec!["abandon"]); + assert!(base_cmd.is_none()); + } + other => panic!("expected SubcommandWrapper, got {other:?}"), + } + // per-key entry also added (for env-var tracking) + let key = find(&diff.added, "jj:ab").expect("expected per-key entry"); + assert!(matches!(key.kind, EntryKind::SubcommandKey { .. })); + } + + #[test] + fn resolve_subcommand_base_cmd_from_regular_alias_same_name() { + let aliases = aset(&[("jj", "just-a-joke")]); + let subs = subset(&[("jj:ab", &["abandon"])]); + let diff = Precedence::new() + .with_project(&aliases, &subs) + .resolve(); + let wrapper = find(&diff.added, "jj").unwrap(); + match &wrapper.kind { + EntryKind::SubcommandWrapper { base_cmd, .. } => { + assert_eq!(base_cmd.as_deref(), Some("just-a-joke")); + } + _ => panic!(), + } + // Only one entry named "jj" — the wrapper, which absorbs the alias. + let jj_hits = diff.added.iter().filter(|e| e.name == "jj").count(); + assert_eq!(jj_hits, 1, "only the wrapper entry should represent 'jj'"); + } + + #[test] + fn resolve_subcommand_different_keys_coexist_across_layers() { + let profile_subs = subset(&[("jj:ab", &["abandon"])]); + let project_subs = subset(&[("jj:bl", &["branch", "list"])]); + let diff = Precedence::new() + .with_profiles(&AliasSet::default(), &profile_subs) + .with_project(&AliasSet::default(), &project_subs) + .resolve(); + let wrapper = find(&diff.added, "jj").unwrap(); + match &wrapper.kind { + EntryKind::SubcommandWrapper { entries, .. } => { + let keys: BTreeSet<_> = entries.iter().map(|e| e.to_key()).collect(); + assert_eq!(keys, BTreeSet::from(["jj:ab".into(), "jj:bl".into()])); + } + _ => panic!(), + } + } + + #[test] + fn resolve_subcommand_project_key_overrides_profile_same_key() { + let profile_subs = subset(&[("jj:ab", &["abandon"])]); + let project_subs = subset(&[("jj:ab", &["abandon", "--force"])]); + let diff = Precedence::new() + .with_profiles(&AliasSet::default(), &profile_subs) + .with_project(&AliasSet::default(), &project_subs) + .resolve(); + let wrapper = find(&diff.added, "jj").unwrap(); + match &wrapper.kind { + EntryKind::SubcommandWrapper { entries, .. } => { + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].long_subcommands, vec!["abandon", "--force"]); + } + _ => panic!(), + } + } + + #[test] + fn resolve_subcommand_unchanged_when_program_hash_matches() { + let subs = subset(&[("jj:ab", &["abandon"])]); + let merged = subs.clone(); + let program_hash = Precedence::subcmd_program_hash_for_test("jj", &merged); + let key_hash = Precedence::subcmd_key_hash_for_test(&["abandon".into()]); + let prev_aliases = format!("jj|{program_hash}"); + let prev_subs = format!("jj:ab|{key_hash}"); + let diff = Precedence::new() + .with_project(&AliasSet::default(), &subs) + .with_shell_state_from_env(Some(&prev_aliases), Some(&prev_subs)) + .resolve(); + assert!(diff.added.is_empty(), "got added: {:?}", diff.added); + assert!(diff.changed.is_empty(), "got changed: {:?}", diff.changed); + assert!(diff.removed.is_empty(), "got removed: {:?}", diff.removed); + assert_eq!(diff.unchanged.len(), 2, "jj wrapper + jj:ab key both unchanged"); + } + + #[test] + fn resolve_subcommand_regenerates_wrapper_when_entry_added() { + // Previous: only jj:ab was tracked. Now jj:bl is added too. + // The program hash changes -> wrapper must be in `changed`. + let subs_before = subset(&[("jj:ab", &["abandon"])]); + let program_hash_before = Precedence::subcmd_program_hash_for_test("jj", &subs_before); + let key_hash_ab = Precedence::subcmd_key_hash_for_test(&["abandon".into()]); + let prev_aliases = format!("jj|{program_hash_before}"); + let prev_subs = format!("jj:ab|{key_hash_ab}"); + + let subs_after = subset(&[("jj:ab", &["abandon"]), ("jj:bl", &["branch", "list"])]); + let diff = Precedence::new() + .with_project(&AliasSet::default(), &subs_after) + .with_shell_state_from_env(Some(&prev_aliases), Some(&prev_subs)) + .resolve(); + assert!(find(&diff.changed, "jj").is_some(), "wrapper must be regenerated"); + assert!(find(&diff.added, "jj:bl").is_some(), "new key must be added"); + // jj:ab itself unchanged + assert!(find(&diff.unchanged, "jj:ab").is_some(), "jj:ab entry itself is unchanged"); + } } From a75d9b094f4a94bfc377cd647438cb3d5f5df66b Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 20:45:29 +0200 Subject: [PATCH 09/38] refactor: use subcommand::group_by_program directly - drop local permissive grouping helper introduced in 40867d2a - fix test inputs to respect 1:1 short -> long parity - jj:bl with 2 longs becomes jj:b:l (valid nested path) - jj:ab with 2 longs becomes jj:ab with 1 combined long --- crates/am/src/precedence.rs | 39 +++++-------------------------------- 1 file changed, 5 insertions(+), 34 deletions(-) diff --git a/crates/am/src/precedence.rs b/crates/am/src/precedence.rs index 653c0f2c..a9451617 100644 --- a/crates/am/src/precedence.rs +++ b/crates/am/src/precedence.rs @@ -168,39 +168,10 @@ impl Precedence { &self.shell_subcmd_state } - /// Permissive grouping of subcommand entries by program. Unlike - /// [`crate::subcommand::group_by_program`], this does NOT enforce equal - /// short/long counts — it treats `long_subcommands` as the verbatim - /// expansion for a given short key (e.g. `ab` → `abandon --force`). - fn group_subcommands_by_program( - subs: &SubcommandSet, - ) -> BTreeMap> { - let mut groups: BTreeMap> = BTreeMap::new(); - for (key, longs) in subs { - let Some((program, rest)) = key.split_once(':') else { - continue; - }; - if program.is_empty() || rest.is_empty() { - continue; - } - let short_subcommands: Vec = - rest.split(':').map(|s| s.to_string()).collect(); - if short_subcommands.iter().any(|s| s.is_empty()) { - continue; - } - groups.entry(program.to_string()).or_default().push(SubcommandEntry { - program: program.to_string(), - short_subcommands, - long_subcommands: longs.clone(), - }); - } - groups - } - pub fn resolve(self) -> PrecedenceDiff { let merged_aliases = self.merged_aliases(); let merged_subcommands = self.merged_subcommands(); - let subcmd_groups = Self::group_subcommands_by_program(&merged_subcommands); + let subcmd_groups = crate::subcommand::group_by_program(&merged_subcommands); let program_names: BTreeSet = subcmd_groups.keys().cloned().collect(); let mut effective: BTreeMap = BTreeMap::new(); @@ -579,7 +550,7 @@ mod tests { #[test] fn resolve_subcommand_different_keys_coexist_across_layers() { let profile_subs = subset(&[("jj:ab", &["abandon"])]); - let project_subs = subset(&[("jj:bl", &["branch", "list"])]); + let project_subs = subset(&[("jj:b:l", &["branch", "list"])]); let diff = Precedence::new() .with_profiles(&AliasSet::default(), &profile_subs) .with_project(&AliasSet::default(), &project_subs) @@ -588,7 +559,7 @@ mod tests { match &wrapper.kind { EntryKind::SubcommandWrapper { entries, .. } => { let keys: BTreeSet<_> = entries.iter().map(|e| e.to_key()).collect(); - assert_eq!(keys, BTreeSet::from(["jj:ab".into(), "jj:bl".into()])); + assert_eq!(keys, BTreeSet::from(["jj:ab".into(), "jj:b:l".into()])); } _ => panic!(), } @@ -597,7 +568,7 @@ mod tests { #[test] fn resolve_subcommand_project_key_overrides_profile_same_key() { let profile_subs = subset(&[("jj:ab", &["abandon"])]); - let project_subs = subset(&[("jj:ab", &["abandon", "--force"])]); + let project_subs = subset(&[("jj:ab", &["abandon-force"])]); let diff = Precedence::new() .with_profiles(&AliasSet::default(), &profile_subs) .with_project(&AliasSet::default(), &project_subs) @@ -606,7 +577,7 @@ mod tests { match &wrapper.kind { EntryKind::SubcommandWrapper { entries, .. } => { assert_eq!(entries.len(), 1); - assert_eq!(entries[0].long_subcommands, vec!["abandon", "--force"]); + assert_eq!(entries[0].long_subcommands, vec!["abandon-force"]); } _ => panic!(), } From aff99e0f54e0ec1f35ad469afc1b16b39770203b Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 20:49:09 +0200 Subject: [PATCH 10/38] feat: introspected shell state input Accepts external functions/aliases from bash/zsh scans. Names not already in env state get hash=None (always differ). Used by 'am init --force' to see everything before the nuke. --- crates/am/src/precedence.rs | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/crates/am/src/precedence.rs b/crates/am/src/precedence.rs index a9451617..e314c704 100644 --- a/crates/am/src/precedence.rs +++ b/crates/am/src/precedence.rs @@ -81,6 +81,19 @@ impl Precedence { self } + pub fn with_shell_state_from_introspection( + mut self, + functions: &HashSet, + aliases: &HashSet, + ) -> Self { + for name in functions.iter().chain(aliases.iter()) { + self.shell_alias_state.entry(name.clone()).or_insert(None); + } + self.external_functions = functions.clone(); + self.external_aliases = aliases.clone(); + self + } + /// Internal: merged alias set keyed by shell-visible name, /// with project > profile > global precedence. fn merged_aliases(&self) -> BTreeMap { @@ -601,6 +614,31 @@ mod tests { assert_eq!(diff.unchanged.len(), 2, "jj wrapper + jj:ab key both unchanged"); } + #[test] + fn introspection_adds_names_with_unknown_hash() { + let mut fns = HashSet::new(); + fns.insert("gs".to_string()); + let mut aliases = HashSet::new(); + aliases.insert("ll".to_string()); + let p = Precedence::new() + .with_shell_state_from_env(Some("b|abc1234"), None) + .with_shell_state_from_introspection(&fns, &aliases); + let state = p.shell_alias_state_for_test(); + assert_eq!(state.get("b"), Some(&Some("abc1234".into()))); + assert_eq!(state.get("gs"), Some(&None)); + assert_eq!(state.get("ll"), Some(&None)); + } + + #[test] + fn introspection_does_not_overwrite_known_hashes() { + let mut fns = HashSet::new(); + fns.insert("b".to_string()); + let p = Precedence::new() + .with_shell_state_from_env(Some("b|abc1234"), None) + .with_shell_state_from_introspection(&fns, &HashSet::new()); + assert_eq!(p.shell_alias_state_for_test().get("b"), Some(&Some("abc1234".into()))); + } + #[test] fn resolve_subcommand_regenerates_wrapper_when_entry_added() { // Previous: only jj:ab was tracked. Now jj:bl is added too. From 33040a9aeaf1db2fb407661cbb9b196c29943110 Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 20:52:20 +0200 Subject: [PATCH 11/38] feat: render_diff translates diff to shell code - unloads (removed + changed) then loads (added + changed) then env var updates - adds _AM_SUBCOMMANDS env var constant - program-level entries go in _AM_ALIASES alongside regular aliases - per-key SubcommandKey entries go in _AM_SUBCOMMANDS --- crates/am/src/env_vars.rs | 6 +++ crates/am/src/precedence.rs | 101 ++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/crates/am/src/env_vars.rs b/crates/am/src/env_vars.rs index 16c36f88..9c191c12 100644 --- a/crates/am/src/env_vars.rs +++ b/crates/am/src/env_vars.rs @@ -8,6 +8,12 @@ pub const AM_ALIASES: &str = "_AM_ALIASES"; /// enabling per-alias change detection on reload. pub const AM_PROJECT_ALIASES: &str = "_AM_PROJECT_ALIASES"; +/// Tracks effective subcommand-key state in the shell. +/// Value: comma-separated entries of `name|short_hash`, covering both +/// program-level wrapper hashes (e.g. `jj|abc1234`) and per-key entry +/// hashes (e.g. `jj:ab|def5678`). +pub const AM_SUBCOMMANDS: &str = "_AM_SUBCOMMANDS"; + /// Path of the `.aliases` file currently in scope, used to suppress /// duplicate hook messages when navigating into subdirectories. pub const AM_PROJECT_PATH: &str = "_AM_PROJECT_PATH"; diff --git a/crates/am/src/precedence.rs b/crates/am/src/precedence.rs index e314c704..ef554963 100644 --- a/crates/am/src/precedence.rs +++ b/crates/am/src/precedence.rs @@ -287,6 +287,74 @@ impl Precedence { } } +use crate::env_vars; +use crate::shell::ShellAdapter; + +/// Render a [`PrecedenceDiff`] into shell code using the given adapter. +/// +/// Emission order: +/// 1. unload (removed + changed) — skipping subcommand-key names (they're +/// tracking-only, not shell functions) +/// 2. load (added + changed) +/// 3. set `_AM_ALIASES` / `_AM_SUBCOMMANDS` to the union of added + changed +/// + unchanged +pub fn render_diff(diff: &PrecedenceDiff, shell: &dyn ShellAdapter) -> String { + let mut lines: Vec = Vec::new(); + + // 1. Unload + for name in &diff.removed { + if name.contains(':') { + continue; + } + lines.push(shell.unalias(name)); + } + for entry in &diff.changed { + if matches!(entry.kind, EntryKind::SubcommandKey { .. }) { + continue; + } + if entry.name.contains(':') { + continue; + } + lines.push(shell.unalias(&entry.name)); + } + + // 2. Load (added + changed) + for entry in diff.added.iter().chain(diff.changed.iter()) { + match &entry.kind { + EntryKind::Alias(alias) => { + lines.push(shell.alias(&alias.as_entry(&entry.name))); + } + EntryKind::SubcommandWrapper { program, entries, base_cmd } => { + let cmd = base_cmd + .clone() + .unwrap_or_else(|| format!("command {program}")); + lines.push(shell.subcommand_wrapper(program, &cmd, entries)); + } + EntryKind::SubcommandKey { .. } => {} + } + } + + // 3. Update tracking env vars + let mut alias_pairs = Vec::new(); + let mut sub_pairs = Vec::new(); + for e in diff.added.iter().chain(diff.changed.iter()).chain(diff.unchanged.iter()) { + let pair = format!("{}|{}", e.name, e.hash); + match &e.kind { + EntryKind::SubcommandKey { .. } => sub_pairs.push(pair), + _ => alias_pairs.push(pair), + } + } + + if !alias_pairs.is_empty() { + lines.push(shell.set_env(env_vars::AM_ALIASES, &alias_pairs.join(","))); + } + if !sub_pairs.is_empty() { + lines.push(shell.set_env(env_vars::AM_SUBCOMMANDS, &sub_pairs.join(","))); + } + + lines.join("\n") +} + #[cfg(test)] mod tests { use super::*; @@ -659,4 +727,37 @@ mod tests { // jj:ab itself unchanged assert!(find(&diff.unchanged, "jj:ab").is_some(), "jj:ab entry itself is unchanged"); } + + use crate::config::ShellsTomlConfig; + use crate::shell::Shell; + + #[test] + fn render_emits_unloads_then_loads_then_env() { + let cfg = ShellsTomlConfig::default(); + let shell = Shell::Fish.as_shell(&cfg, Default::default(), Default::default()); + + // Previous shell state: `b|0000000,gone|aaa` ; new effective: `b|make build`. + let project = aset(&[("b", "make build")]); + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .with_shell_state_from_env(Some("b|0000000,gone|aaa"), None) + .resolve(); + + let out = crate::precedence::render_diff(&diff, shell.as_ref()); + assert!(out.contains("functions -e gone"), "gone must be unloaded: {out}"); + assert!(out.contains("functions -e b"), "changed b must be unloaded: {out}"); + assert!(out.contains("alias b \"make build\""), "b must be reloaded: {out}"); + // env-var update must be the last section + let env_pos = out.find("_AM_ALIASES").expect("env update missing"); + let alias_pos = out.find("alias b").unwrap(); + assert!(env_pos > alias_pos, "env update must come after loads"); + } + + #[test] + fn render_empty_diff_produces_empty_string() { + let cfg = ShellsTomlConfig::default(); + let shell = Shell::Fish.as_shell(&cfg, Default::default(), Default::default()); + let out = crate::precedence::render_diff(&PrecedenceDiff::default(), shell.as_ref()); + assert!(out.is_empty()); + } } From e30f70a4305117da60a876f98bd9c137f39c5338 Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 20:57:41 +0200 Subject: [PATCH 12/38] feat: add 'am sync' command backed by Precedence Engine - hidden subcommand, coexists with 'am hook' and 'am reload' for now - Message::Sync handler reads _AM_ALIASES/_AM_SUBCOMMANDS (+ legacy fold) - builds Precedence from config + profiles + (trusted) project + shell state - prints render_diff output to stdout --- completions/bash/am.bash | 38 ++++++++++++++- completions/fish/am.fish | 39 +++++++++------- completions/powershell/_am.ps1 | 14 ++++++ completions/zsh/_am | 27 +++++++++++ crates/am/src/bin/am.rs | 3 +- crates/am/src/cli.rs | 10 ++++ crates/am/src/messages.rs | 1 + crates/am/src/update.rs | 46 +++++++++++++++++++ crates/am/tests/snapshots.rs | 14 ++++++ ..._init_bash_force_with_tracked_aliases.snap | 38 ++++++++++++++- ...ts__snapshot_init_bash_simple_profile.snap | 38 ++++++++++++++- ...ot_init_bash_with_kubectl_subcommands.snap | 38 ++++++++++++++- ..._fish_abbr_force_with_tracked_aliases.snap | 39 +++++++++------- ...pshots__snapshot_init_fish_deep_chain.snap | 39 +++++++++------- ..._snapshot_init_fish_force_no_previous.snap | 39 +++++++++------- ..._init_fish_force_with_tracked_aliases.snap | 39 +++++++++------- ...t_init_fish_globals_and_multi_profile.snap | 39 +++++++++------- ...ots__snapshot_init_fish_multi_profile.snap | 39 +++++++++------- ...ts__snapshot_init_fish_simple_profile.snap | 39 +++++++++------- ...hots__snapshot_init_fish_with_globals.snap | 39 +++++++++------- ...hot_init_fish_with_simple_subcommands.snap | 39 +++++++++------- ...apshot_init_powershell_simple_profile.snap | 14 ++++++ ...ots__snapshot_init_zsh_simple_profile.snap | 27 +++++++++++ 23 files changed, 519 insertions(+), 179 deletions(-) diff --git a/completions/bash/am.bash b/completions/bash/am.bash index cb5bda84..9618c248 100644 --- a/completions/bash/am.bash +++ b/completions/bash/am.bash @@ -55,6 +55,9 @@ _am() { am,status) cmd="am__subcmd__status" ;; + am,sync) + cmd="am__subcmd__sync" + ;; am,trust) cmd="am__subcmd__trust" ;; @@ -106,6 +109,9 @@ _am() { am__subcmd__help,status) cmd="am__subcmd__help__subcmd__status" ;; + am__subcmd__help,sync) + cmd="am__subcmd__help__subcmd__sync" + ;; am__subcmd__help,trust) cmd="am__subcmd__help__subcmd__trust" ;; @@ -167,7 +173,7 @@ _am() { case "${cmd}" in am) - opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust hook reload help" + opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -229,7 +235,7 @@ _am() { return 0 ;; am__subcmd__help) - opts="add remove ls status profile init setup use tui export import share trust untrust hook reload help" + opts="add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -480,6 +486,20 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + am__subcmd__help__subcmd__sync) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; am__subcmd__help__subcmd__trust) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then @@ -852,6 +872,20 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + am__subcmd__sync) + opts="-q -h -V --quiet --help --version bash brush fish powershell zsh" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; am__subcmd__trust) opts="-h -V --help --version" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then diff --git a/completions/fish/am.fish b/completions/fish/am.fish index c55a2fe1..e8501e3a 100644 --- a/completions/fish/am.fish +++ b/completions/fish/am.fish @@ -42,6 +42,7 @@ complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust t complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r @@ -130,23 +131,27 @@ complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' +complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' +complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/completions/powershell/_am.ps1 b/completions/powershell/_am.ps1 index 88304628..4dde83fb 100644 --- a/completions/powershell/_am.ps1 +++ b/completions/powershell/_am.ps1 @@ -41,6 +41,7 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [CompletionResult]::new('untrust', 'untrust', [CompletionResultType]::ParameterValue, 'Remove trust for the project .aliases file in the current directory') [CompletionResult]::new('hook', 'hook', [CompletionResultType]::ParameterValue, 'Internal: called by the cd hook to load/unload project aliases') [CompletionResult]::new('reload', 'reload', [CompletionResultType]::ParameterValue, 'Internal: called by the am wrapper to reload profile aliases after switching') + [CompletionResult]::new('sync', 'sync', [CompletionResultType]::ParameterValue, 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)') [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)') break } @@ -277,6 +278,15 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') break } + 'am;sync' { + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Suppress info and warning messages (still unloads/loads aliases)') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Suppress info and warning messages (still unloads/loads aliases)') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') + break + } 'am;help' { [CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'Add a new alias') [CompletionResult]::new('remove', 'remove', [CompletionResultType]::ParameterValue, 'Remove an alias') @@ -294,6 +304,7 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [CompletionResult]::new('untrust', 'untrust', [CompletionResultType]::ParameterValue, 'Remove trust for the project .aliases file in the current directory') [CompletionResult]::new('hook', 'hook', [CompletionResultType]::ParameterValue, 'Internal: called by the cd hook to load/unload project aliases') [CompletionResult]::new('reload', 'reload', [CompletionResultType]::ParameterValue, 'Internal: called by the am wrapper to reload profile aliases after switching') + [CompletionResult]::new('sync', 'sync', [CompletionResultType]::ParameterValue, 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)') [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)') break } @@ -361,6 +372,9 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { 'am;help;reload' { break } + 'am;help;sync' { + break + } 'am;help;help' { break } diff --git a/completions/zsh/_am b/completions/zsh/_am index d3bcd4fe..d2b62d05 100644 --- a/completions/zsh/_am +++ b/completions/zsh/_am @@ -313,6 +313,17 @@ _arguments "${_arguments_options[@]}" : \ ':shell:(bash brush fish powershell zsh)' \ && ret=0 ;; +(sync) +_arguments "${_arguments_options[@]}" : \ +'-q[Suppress info and warning messages (still unloads/loads aliases)]' \ +'--quiet[Suppress info and warning messages (still unloads/loads aliases)]' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +':shell:(bash brush fish powershell zsh)' \ +&& ret=0 +;; (help) _arguments "${_arguments_options[@]}" : \ ":: :_am__subcmd__help_commands" \ @@ -417,6 +428,10 @@ _arguments "${_arguments_options[@]}" : \ _arguments "${_arguments_options[@]}" : \ && ret=0 ;; +(sync) +_arguments "${_arguments_options[@]}" : \ +&& ret=0 +;; (help) _arguments "${_arguments_options[@]}" : \ && ret=0 @@ -449,6 +464,7 @@ _am_commands() { 'untrust:Remove trust for the project .aliases file in the current directory' \ 'hook:Internal\: called by the cd hook to load/unload project aliases' \ 'reload:Internal\: called by the am wrapper to reload profile aliases after switching' \ +'sync:Internal\: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' \ 'help:Print this message or the help of the given subcommand(s)' \ ) _describe -t commands 'am commands' commands "$@" @@ -482,6 +498,7 @@ _am__subcmd__help_commands() { 'untrust:Remove trust for the project .aliases file in the current directory' \ 'hook:Internal\: called by the cd hook to load/unload project aliases' \ 'reload:Internal\: called by the am wrapper to reload profile aliases after switching' \ +'sync:Internal\: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' \ 'help:Print this message or the help of the given subcommand(s)' \ ) _describe -t commands 'am help commands' commands "$@" @@ -576,6 +593,11 @@ _am__subcmd__help__subcmd__status_commands() { local commands; commands=() _describe -t commands 'am help status commands' commands "$@" } +(( $+functions[_am__subcmd__help__subcmd__sync_commands] )) || +_am__subcmd__help__subcmd__sync_commands() { + local commands; commands=() + _describe -t commands 'am help sync commands' commands "$@" +} (( $+functions[_am__subcmd__help__subcmd__trust_commands] )) || _am__subcmd__help__subcmd__trust_commands() { local commands; commands=() @@ -708,6 +730,11 @@ _am__subcmd__status_commands() { local commands; commands=() _describe -t commands 'am status commands' commands "$@" } +(( $+functions[_am__subcmd__sync_commands] )) || +_am__subcmd__sync_commands() { + local commands; commands=() + _describe -t commands 'am sync commands' commands "$@" +} (( $+functions[_am__subcmd__trust_commands] )) || _am__subcmd__trust_commands() { local commands; commands=() diff --git a/crates/am/src/bin/am.rs b/crates/am/src/bin/am.rs index 06699f12..8b548029 100644 --- a/crates/am/src/bin/am.rs +++ b/crates/am/src/bin/am.rs @@ -48,7 +48,7 @@ fn main() -> anyhow::Result<()> { // Don't log for commands whose stdout is eval'd by the shell if !matches!( &cli.command, - Commands::Init { .. } | Commands::Hook { .. } | Commands::Reload { .. } + Commands::Init { .. } | Commands::Hook { .. } | Commands::Reload { .. } | Commands::Sync { .. } ) { setup_logging(); } @@ -387,6 +387,7 @@ fn main() -> anyhow::Result<()> { Commands::Init { shell, force } => Message::InitShell(shell.clone(), *force), Commands::Hook { shell, quiet } => Message::Hook(shell.clone(), *quiet), Commands::Reload { shell } => Message::Reload(shell.clone()), + Commands::Sync { shell, quiet } => Message::Sync(shell.clone(), *quiet), }; let result = update(&mut model, message)?; diff --git a/crates/am/src/cli.rs b/crates/am/src/cli.rs index c74bc8be..ec708764 100644 --- a/crates/am/src/cli.rs +++ b/crates/am/src/cli.rs @@ -140,6 +140,16 @@ pub enum Commands { /// Internal: called by the am wrapper to reload profile aliases after switching #[command(hide = true)] Reload { shell: Shell }, + + /// Internal: compute and emit the minimal shell ops to sync the shell with + /// the effective merged alias state (global + profile + project). + #[command(hide = true)] + Sync { + /// Suppress info and warning messages (still unloads/loads aliases). + #[arg(short, long)] + quiet: bool, + shell: Shell, + }, } #[derive(Subcommand)] diff --git a/crates/am/src/messages.rs b/crates/am/src/messages.rs index a62d5e7a..1a719d9e 100644 --- a/crates/am/src/messages.rs +++ b/crates/am/src/messages.rs @@ -37,6 +37,7 @@ pub enum Message { InitShell(Shell, bool), Hook(Shell, bool), Reload(Shell), + Sync(Shell, bool), ToggleProfiles(Vec), UseProfilesAt(Vec, usize), diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs index f8a49671..9d740876 100644 --- a/crates/am/src/update.rs +++ b/crates/am/src/update.rs @@ -4,6 +4,7 @@ use crate::display::render_listing; use crate::effects::Effect; use crate::env_vars; use crate::init::{generate_init, generate_reload}; +use crate::precedence::{self, Precedence}; use crate::profile::AliasCollection; use crate::project::ProjectAliases; use crate::shell::bash; @@ -692,6 +693,51 @@ pub fn update(model: &mut AppModel, message: Message) -> Result { + let prev_aliases = std::env::var(env_vars::AM_ALIASES).ok(); + let prev_subs = std::env::var(env_vars::AM_SUBCOMMANDS).ok(); + // Legacy migration: if the new vars are empty but the old + // _AM_PROJECT_ALIASES exists, fold its entries into prev_aliases + // so the first sync after upgrade sees them as "known prior state". + let legacy_project = std::env::var(env_vars::AM_PROJECT_ALIASES).ok(); + let merged_prev_aliases = match (prev_aliases.as_deref(), legacy_project.as_deref()) { + (None, None) => None, + (Some(a), None) => Some(a.to_string()), + (None, Some(b)) => Some(b.to_string()), + (Some(a), Some(b)) => Some(format!("{a},{b}")), + }; + + let resolved_aliases = model + .profile_config() + .resolve_active_aliases(&model.session.active_profiles); + let resolved_subs = model + .profile_config() + .resolve_active_subcommands(&model.session.active_profiles); + + let (project_aliases, project_subs) = match model.project_trust() { + Some(t) if t.is_trusted() => model.project_alias_set_and_subcommands(), + _ => (crate::AliasSet::default(), crate::subcommand::SubcommandSet::new()), + }; + + let shell_cfg = model.config.shell.clone(); + let shell_impl = shell + .clone() + .as_shell(&shell_cfg, Default::default(), Default::default()); + + let diff = Precedence::new() + .with_global(&model.config.aliases, &model.config.subcommands) + .with_profiles(&resolved_aliases, &resolved_subs) + .with_project(&project_aliases, &project_subs) + .with_shell_state_from_env(merged_prev_aliases.as_deref(), prev_subs.as_deref()) + .resolve(); + + let output = precedence::render_diff(&diff, shell_impl.as_ref()); + if !output.is_empty() { + print!("{output}"); + } + let _ = quiet; // messaging/warning added in Task 10 + Ok(UpdateResult::done()) + } Message::ToggleProfiles(names) => { for name in &names { model diff --git a/crates/am/tests/snapshots.rs b/crates/am/tests/snapshots.rs index ccb98474..eeb699d0 100644 --- a/crates/am/tests/snapshots.rs +++ b/crates/am/tests/snapshots.rs @@ -1112,3 +1112,17 @@ fn snapshot_init_bash_force_with_tracked_aliases() { ); insta::assert_snapshot!(output); } + +#[test] +fn sync_fresh_load_emits_aliases_and_env_var() { + use amoxide::precedence::{render_diff, Precedence}; + let aliases = aliases(&[("gs", "git status")]); + let diff = Precedence::new() + .with_profiles(&aliases, &SubcommandSet::new()) + .resolve(); + let shell = Shell::Fish.as_shell(&Default::default(), Default::default(), Default::default()); + let out = render_diff(&diff, shell.as_ref()); + assert!(out.contains("alias gs \"git status\"")); + assert!(out.contains("_AM_ALIASES")); + assert!(out.contains("gs|")); +} diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_force_with_tracked_aliases.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_force_with_tracked_aliases.snap index 7b09232e..f2bacd6e 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_force_with_tracked_aliases.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_force_with_tracked_aliases.snap @@ -114,6 +114,9 @@ _am() { am,status) cmd="am__subcmd__status" ;; + am,sync) + cmd="am__subcmd__sync" + ;; am,trust) cmd="am__subcmd__trust" ;; @@ -165,6 +168,9 @@ _am() { am__subcmd__help,status) cmd="am__subcmd__help__subcmd__status" ;; + am__subcmd__help,sync) + cmd="am__subcmd__help__subcmd__sync" + ;; am__subcmd__help,trust) cmd="am__subcmd__help__subcmd__trust" ;; @@ -226,7 +232,7 @@ _am() { case "${cmd}" in am) - opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust hook reload help" + opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -288,7 +294,7 @@ _am() { return 0 ;; am__subcmd__help) - opts="add remove ls status profile init setup use tui export import share trust untrust hook reload help" + opts="add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -539,6 +545,20 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + am__subcmd__help__subcmd__sync) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; am__subcmd__help__subcmd__trust) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then @@ -911,6 +931,20 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + am__subcmd__sync) + opts="-q -h -V --quiet --help --version bash brush fish powershell zsh" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; am__subcmd__trust) opts="-h -V --help --version" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_simple_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_simple_profile.snap index 57593af5..9c59ac1b 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_simple_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_simple_profile.snap @@ -112,6 +112,9 @@ _am() { am,status) cmd="am__subcmd__status" ;; + am,sync) + cmd="am__subcmd__sync" + ;; am,trust) cmd="am__subcmd__trust" ;; @@ -163,6 +166,9 @@ _am() { am__subcmd__help,status) cmd="am__subcmd__help__subcmd__status" ;; + am__subcmd__help,sync) + cmd="am__subcmd__help__subcmd__sync" + ;; am__subcmd__help,trust) cmd="am__subcmd__help__subcmd__trust" ;; @@ -224,7 +230,7 @@ _am() { case "${cmd}" in am) - opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust hook reload help" + opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -286,7 +292,7 @@ _am() { return 0 ;; am__subcmd__help) - opts="add remove ls status profile init setup use tui export import share trust untrust hook reload help" + opts="add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -537,6 +543,20 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + am__subcmd__help__subcmd__sync) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; am__subcmd__help__subcmd__trust) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then @@ -909,6 +929,20 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + am__subcmd__sync) + opts="-q -h -V --quiet --help --version bash brush fish powershell zsh" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; am__subcmd__trust) opts="-h -V --help --version" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_with_kubectl_subcommands.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_with_kubectl_subcommands.snap index 0af7e3d0..01b049af 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_with_kubectl_subcommands.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_with_kubectl_subcommands.snap @@ -140,6 +140,9 @@ _am() { am,status) cmd="am__subcmd__status" ;; + am,sync) + cmd="am__subcmd__sync" + ;; am,trust) cmd="am__subcmd__trust" ;; @@ -191,6 +194,9 @@ _am() { am__subcmd__help,status) cmd="am__subcmd__help__subcmd__status" ;; + am__subcmd__help,sync) + cmd="am__subcmd__help__subcmd__sync" + ;; am__subcmd__help,trust) cmd="am__subcmd__help__subcmd__trust" ;; @@ -252,7 +258,7 @@ _am() { case "${cmd}" in am) - opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust hook reload help" + opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -314,7 +320,7 @@ _am() { return 0 ;; am__subcmd__help) - opts="add remove ls status profile init setup use tui export import share trust untrust hook reload help" + opts="add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -565,6 +571,20 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + am__subcmd__help__subcmd__sync) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; am__subcmd__help__subcmd__trust) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then @@ -937,6 +957,20 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + am__subcmd__sync) + opts="-q -h -V --quiet --help --version bash brush fish powershell zsh" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; am__subcmd__trust) opts="-h -V --help --version" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_abbr_force_with_tracked_aliases.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_abbr_force_with_tracked_aliases.snap index 5b04ef5f..902ea7b0 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_abbr_force_with_tracked_aliases.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_abbr_force_with_tracked_aliases.snap @@ -98,6 +98,7 @@ complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust t complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r @@ -186,23 +187,27 @@ complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' +complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' +complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap index 50a8c386..eeff4de3 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap @@ -96,6 +96,7 @@ complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust t complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r @@ -184,23 +185,27 @@ complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' +complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' +complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_no_previous.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_no_previous.snap index f86e7bf4..c3a5fec0 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_no_previous.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_no_previous.snap @@ -96,6 +96,7 @@ complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust t complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r @@ -184,23 +185,27 @@ complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' +complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' +complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_with_tracked_aliases.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_with_tracked_aliases.snap index 3817b4ba..a271b414 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_with_tracked_aliases.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_with_tracked_aliases.snap @@ -101,6 +101,7 @@ complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust t complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r @@ -189,23 +190,27 @@ complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' +complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' +complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap index 9037a9a9..287bcf5f 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap @@ -99,6 +99,7 @@ complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust t complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r @@ -187,23 +188,27 @@ complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' +complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' +complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap index 6a71dc32..24cd9bf5 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap @@ -98,6 +98,7 @@ complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust t complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r @@ -186,23 +187,27 @@ complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' +complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' +complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap index c57598b9..c220aee8 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap @@ -95,6 +95,7 @@ complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust t complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r @@ -183,23 +184,27 @@ complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' +complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' +complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap index e9726ddf..34c9466e 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap @@ -95,6 +95,7 @@ complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust t complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r @@ -183,23 +184,27 @@ complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' +complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' +complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap index d48d1d7b..0a497b3d 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap @@ -104,6 +104,7 @@ complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust t complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r @@ -192,23 +193,27 @@ complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' +complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' +complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_powershell_simple_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_powershell_simple_profile.snap index 0a148ba0..da3bd6df 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_powershell_simple_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_powershell_simple_profile.snap @@ -110,6 +110,7 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [System.Management.Automation.CompletionResult]::new('untrust', 'untrust', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Remove trust for the project .aliases file in the current directory') [System.Management.Automation.CompletionResult]::new('hook', 'hook', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Internal: called by the cd hook to load/unload project aliases') [System.Management.Automation.CompletionResult]::new('reload', 'reload', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Internal: called by the am wrapper to reload profile aliases after switching') + [System.Management.Automation.CompletionResult]::new('sync', 'sync', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)') [System.Management.Automation.CompletionResult]::new('help', 'help', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)') break } @@ -346,6 +347,15 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [System.Management.Automation.CompletionResult]::new('--version', '--version', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print version') break } + 'am;sync' { + [System.Management.Automation.CompletionResult]::new('-q', '-q', [System.Management.Automation.CompletionResultType]::ParameterName, 'Suppress info and warning messages (still unloads/loads aliases)') + [System.Management.Automation.CompletionResult]::new('--quiet', '--quiet', [System.Management.Automation.CompletionResultType]::ParameterName, 'Suppress info and warning messages (still unloads/loads aliases)') + [System.Management.Automation.CompletionResult]::new('-h', '-h', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print help') + [System.Management.Automation.CompletionResult]::new('--help', '--help', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print help') + [System.Management.Automation.CompletionResult]::new('-V', '-V ', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print version') + [System.Management.Automation.CompletionResult]::new('--version', '--version', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print version') + break + } 'am;help' { [System.Management.Automation.CompletionResult]::new('add', 'add', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Add a new alias') [System.Management.Automation.CompletionResult]::new('remove', 'remove', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Remove an alias') @@ -363,6 +373,7 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [System.Management.Automation.CompletionResult]::new('untrust', 'untrust', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Remove trust for the project .aliases file in the current directory') [System.Management.Automation.CompletionResult]::new('hook', 'hook', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Internal: called by the cd hook to load/unload project aliases') [System.Management.Automation.CompletionResult]::new('reload', 'reload', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Internal: called by the am wrapper to reload profile aliases after switching') + [System.Management.Automation.CompletionResult]::new('sync', 'sync', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)') [System.Management.Automation.CompletionResult]::new('help', 'help', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)') break } @@ -430,6 +441,9 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { 'am;help;reload' { break } + 'am;help;sync' { + break + } 'am;help;help' { break } diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_zsh_simple_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_zsh_simple_profile.snap index 02348629..a7077c05 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_zsh_simple_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_zsh_simple_profile.snap @@ -351,6 +351,17 @@ _arguments "${_arguments_options[@]}" : \ ':shell:(bash brush fish powershell zsh)' \ && ret=0 ;; +(sync) +_arguments "${_arguments_options[@]}" : \ +'-q[Suppress info and warning messages (still unloads/loads aliases)]' \ +'--quiet[Suppress info and warning messages (still unloads/loads aliases)]' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +':shell:(bash brush fish powershell zsh)' \ +&& ret=0 +;; (help) _arguments "${_arguments_options[@]}" : \ ":: :_am__subcmd__help_commands" \ @@ -455,6 +466,10 @@ _arguments "${_arguments_options[@]}" : \ _arguments "${_arguments_options[@]}" : \ && ret=0 ;; +(sync) +_arguments "${_arguments_options[@]}" : \ +&& ret=0 +;; (help) _arguments "${_arguments_options[@]}" : \ && ret=0 @@ -487,6 +502,7 @@ _am_commands() { 'untrust:Remove trust for the project .aliases file in the current directory' \ 'hook:Internal\: called by the cd hook to load/unload project aliases' \ 'reload:Internal\: called by the am wrapper to reload profile aliases after switching' \ +'sync:Internal\: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' \ 'help:Print this message or the help of the given subcommand(s)' \ ) _describe -t commands 'am commands' commands "$@" @@ -520,6 +536,7 @@ _am__subcmd__help_commands() { 'untrust:Remove trust for the project .aliases file in the current directory' \ 'hook:Internal\: called by the cd hook to load/unload project aliases' \ 'reload:Internal\: called by the am wrapper to reload profile aliases after switching' \ +'sync:Internal\: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' \ 'help:Print this message or the help of the given subcommand(s)' \ ) _describe -t commands 'am help commands' commands "$@" @@ -614,6 +631,11 @@ _am__subcmd__help__subcmd__status_commands() { local commands; commands=() _describe -t commands 'am help status commands' commands "$@" } +(( $+functions[_am__subcmd__help__subcmd__sync_commands] )) || +_am__subcmd__help__subcmd__sync_commands() { + local commands; commands=() + _describe -t commands 'am help sync commands' commands "$@" +} (( $+functions[_am__subcmd__help__subcmd__trust_commands] )) || _am__subcmd__help__subcmd__trust_commands() { local commands; commands=() @@ -746,6 +768,11 @@ _am__subcmd__status_commands() { local commands; commands=() _describe -t commands 'am status commands' commands "$@" } +(( $+functions[_am__subcmd__sync_commands] )) || +_am__subcmd__sync_commands() { + local commands; commands=() + _describe -t commands 'am sync commands' commands "$@" +} (( $+functions[_am__subcmd__trust_commands] )) || _am__subcmd__trust_commands() { local commands; commands=() From 4bc2ad7a054e488b0aba6ccb60360d05e0e3d647 Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 21:02:55 +0200 Subject: [PATCH 13/38] feat: trust gating, messaging, and _AM_PROJECT_PATH bookkeeping for sync - Tampered/Untrusted/Unknown exclude project layer + warn where applicable - Tampered returns SaveSecurity effect - Fresh load prints full listing; incremental prints compact summary - _AM_PROJECT_PATH set when project excluded, unset when included - legacy _AM_PROJECT_ALIASES unset after first sync --- crates/am/src/update.rs | 139 +++++++++++++++++++++++++++++++---- crates/am/tests/snapshots.rs | 29 ++++++++ 2 files changed, 153 insertions(+), 15 deletions(-) diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs index 9d740876..3a65163d 100644 --- a/crates/am/src/update.rs +++ b/crates/am/src/update.rs @@ -696,9 +696,6 @@ pub fn update(model: &mut AppModel, message: Message) -> Result { let prev_aliases = std::env::var(env_vars::AM_ALIASES).ok(); let prev_subs = std::env::var(env_vars::AM_SUBCOMMANDS).ok(); - // Legacy migration: if the new vars are empty but the old - // _AM_PROJECT_ALIASES exists, fold its entries into prev_aliases - // so the first sync after upgrade sees them as "known prior state". let legacy_project = std::env::var(env_vars::AM_PROJECT_ALIASES).ok(); let merged_prev_aliases = match (prev_aliases.as_deref(), legacy_project.as_deref()) { (None, None) => None, @@ -706,6 +703,13 @@ pub fn update(model: &mut AppModel, message: Message) -> Result Some(b.to_string()), (Some(a), Some(b)) => Some(format!("{a},{b}")), }; + let prev_project_path = std::env::var(env_vars::AM_PROJECT_PATH).ok(); + + let shell_cfg = model.config.shell.clone(); + let cwd = model.cwd.clone(); + let shell_impl = shell + .clone() + .as_shell(&shell_cfg, Default::default(), Default::default()); let resolved_aliases = model .profile_config() @@ -714,15 +718,49 @@ pub fn update(model: &mut AppModel, message: Message) -> Result model.project_alias_set_and_subcommands(), - _ => (crate::AliasSet::default(), crate::subcommand::SubcommandSet::new()), + // Decide project inclusion and evaluate trust warnings. + let mut lines: Vec = Vec::new(); + let mut security_changed = false; + let (include_project, project_path) = match model.project_trust() { + Some(crate::trust::ProjectTrust::Trusted(..)) => { + (true, model.project_path().map(|p| p.to_path_buf())) + } + Some(trust) => { + let path = trust.path().to_path_buf(); + let is_direct = path.parent().is_some_and(|p| p == cwd); + let already_seen = prev_project_path + .as_deref() + .is_some_and(|p| std::path::Path::new(p) == path); + let show_msg = !quiet && is_direct && !already_seen; + match trust { + crate::trust::ProjectTrust::Unknown(_) if show_msg => { + lines.push(shell_impl.echo( + "am: .aliases found but not trusted. Run 'am trust' to review and allow.", + )); + } + crate::trust::ProjectTrust::Tampered(_) => { + security_changed = true; + if show_msg { + lines.push(shell_impl.echo( + "am: .aliases was modified since last trusted. Run 'am trust' to review and allow.", + )); + } + } + _ => {} + } + (false, Some(path)) + } + None => (false, None), + }; + + let (project_aliases, project_subs) = if include_project { + model.project_alias_set_and_subcommands() + } else { + (crate::AliasSet::default(), crate::subcommand::SubcommandSet::new()) }; - let shell_cfg = model.config.shell.clone(); - let shell_impl = shell - .clone() - .as_shell(&shell_cfg, Default::default(), Default::default()); + let is_fresh_load = merged_prev_aliases.as_deref().is_none_or(|s| s.is_empty()) + && prev_subs.as_deref().is_none_or(|s| s.is_empty()); let diff = Precedence::new() .with_global(&model.config.aliases, &model.config.subcommands) @@ -731,12 +769,83 @@ pub fn update(model: &mut AppModel, message: Message) -> Result>() + .join("\n"); + if !joined.is_empty() { + print!("{joined}"); + } + + if security_changed { + Ok(UpdateResult::effect(Effect::SaveSecurity)) + } else { + Ok(UpdateResult::done()) } - let _ = quiet; // messaging/warning added in Task 10 - Ok(UpdateResult::done()) } Message::ToggleProfiles(names) => { for name in &names { diff --git a/crates/am/tests/snapshots.rs b/crates/am/tests/snapshots.rs index eeb699d0..0d4a0fa0 100644 --- a/crates/am/tests/snapshots.rs +++ b/crates/am/tests/snapshots.rs @@ -1126,3 +1126,32 @@ fn sync_fresh_load_emits_aliases_and_env_var() { assert!(out.contains("_AM_ALIASES")); assert!(out.contains("gs|")); } + +#[test] +fn sync_tampered_returns_save_security_effect_and_excludes_project() { + use amoxide::app_model::AppModel; + use amoxide::messages::Message; + use amoxide::shell::Shell; + use amoxide::update::update; + + let dir = tempfile::tempdir().unwrap(); + let aliases_path = dir.path().join(".aliases"); + fs::write(&aliases_path, "[aliases]\nt = \"cargo test\"\n").unwrap(); + + let mut sec = amoxide::security::SecurityConfig::default(); + // Trust with a wrong hash to force tamper. + sec.trust(&aliases_path, "wrong_hash"); + let mut model = AppModel::new_with_security( + amoxide::Config::default(), + amoxide::ProfileConfig::default(), + sec, + ) + .with_cwd(dir.path().to_path_buf()); + + // Smoke test: we don't care about stdout, just the effect list. + let res = update(&mut model, Message::Sync(Shell::Fish, true)).unwrap(); + assert!( + res.effects.iter().any(|e| matches!(e, amoxide::Effect::SaveSecurity)), + "tampered file must trigger SaveSecurity effect" + ); +} From 8301e9f644df28505d1eb08d0b4fa450ce1f6ea7 Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 21:12:36 +0200 Subject: [PATCH 14/38] feat: delegate init alias emission to Precedence Engine - generate_init builds Precedence with empty shell state - engine emits all loads and the name|hash tracking env var - scaffolding (wrapper, cd hook, completions) unchanged --- crates/am/src/init.rs | 99 ++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 58 deletions(-) diff --git a/crates/am/src/init.rs b/crates/am/src/init.rs index bcfe71fc..0dd95f1f 100644 --- a/crates/am/src/init.rs +++ b/crates/am/src/init.rs @@ -27,73 +27,42 @@ pub fn generate_init( profile_aliases: &AliasSet, subcommands: &SubcommandSet, ) -> String { + use crate::precedence::{self, Precedence}; + let shell_impl = ctx.shell.clone().as_shell( ctx.cfg, ctx.external_functions.clone(), ctx.external_aliases.clone(), ); - let mut lines: Vec = Vec::new(); - let mut all_names: Vec = Vec::new(); - - // Determine which program names have subcommand wrappers - let subcmd_groups = group_by_program(subcommands); - let programs_with_wrappers: std::collections::BTreeSet<&str> = - subcmd_groups.keys().map(|s| s.as_str()).collect(); - - // Emit global aliases (skip those absorbed by subcommand wrappers) - for (alias_name, alias_value) in global_aliases.iter() { - let name = alias_name.as_ref(); - if !programs_with_wrappers.contains(name) { - lines.push(shell_impl.alias(&alias_value.as_entry(name))); - } - all_names.push(name.to_string()); - } - - // Emit profile aliases (skip those absorbed by subcommand wrappers) - for (alias_name, alias_value) in profile_aliases.iter() { - let name = alias_name.as_ref(); - if !programs_with_wrappers.contains(name) { - lines.push(shell_impl.alias(&alias_value.as_entry(name))); - } - all_names.push(name.to_string()); - } - - // Emit subcommand wrappers - for (program, entries) in &subcmd_groups { - // Determine base command: alias value if regular alias exists, else "command " - let all_aliases = global_aliases.iter().chain(profile_aliases.iter()); - let base_cmd = all_aliases - .filter(|(n, _)| n.as_ref() == program.as_str()) - .map(|(_, v)| v.command().to_string()) - .last() - .unwrap_or_else(|| format!("command {program}")); - - lines.push(shell_impl.subcommand_wrapper(program, &base_cmd, entries)); - all_names.push(program.to_string()); - } - // Track all loaded aliases (global + profile + subcommand wrappers) for reload cleanup - if !all_names.is_empty() { - all_names.sort(); - all_names.dedup(); - lines.push(shell_impl.set_env(env_vars::AM_ALIASES, &all_names.join(","))); + // Split subcommands back into global/profile buckets. Callers today pass + // a single merged SubcommandSet — for init, global = config.subcommands + // and profile = resolved from active profiles. Since both are already + // merged upstream we simply pass the full set as "profile" to keep the + // engine's precedence order intact (global vs profile tier is invisible + // on init — there is no shell state yet). + let diff = Precedence::new() + .with_global(global_aliases, &SubcommandSet::new()) + .with_profiles(profile_aliases, subcommands) + .resolve(); + + let mut output = precedence::render_diff(&diff, shell_impl.as_ref()); + + // Clean up any legacy tracking var from older installs. + if !output.is_empty() { + output.push('\n'); } - // Clean up legacy tracking var from older versions - lines.push(shell_impl.unset_env(env_vars::AM_PROFILE_ALIASES_LEGACY)); + output.push_str(&shell_impl.unset_env(env_vars::AM_PROFILE_ALIASES_LEGACY)); - // Wrapper function - lines.push(String::new()); - lines.push(am_wrapper(ctx.shell)); - - // cd hook for project aliases - lines.push(String::new()); - lines.push(cd_hook_setup(ctx.shell)); - - // Shell completions - lines.push(String::new()); - lines.push(completions(ctx.shell)); + // Wrapper function + cd hook + completions. + output.push('\n'); + output.push_str(&am_wrapper(ctx.shell)); + output.push('\n'); + output.push_str(&cd_hook_setup(ctx.shell)); + output.push('\n'); + output.push_str(&completions(ctx.shell)); - lines.join("\n") + output } /// Like [`generate_init`] but prepends force-cleanup lines for `prev_names`. @@ -666,6 +635,20 @@ mod tests { assert!(output.contains("abbr --add gs \"git status\"")); } + #[test] + fn init_delegates_alias_emission_to_precedence() { + // init output must match render_diff output for the same inputs. + let aliases = test_aliases(); + let ctx = default_ctx(&Shell::Fish); + let output = generate_init(&ctx, &AliasSet::default(), &aliases, &SubcommandSet::new()); + // Everything should be in _AM_ALIASES with name|hash format (not bare names). + let gs_hash = crate::trust::compute_short_hash(b"git status"); + assert!( + output.contains(&format!("gs|{gs_hash}")), + "init must use name|hash format in _AM_ALIASES, got: {output}" + ); + } + #[test] fn test_fish_reload_with_abbr_unloads_via_abbr_erase() { use crate::config::{FishConfig, ShellsTomlConfig}; From 0c2ed2805f5b2ec014a2250c9ecb69bba52d16fa Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 21:21:44 +0200 Subject: [PATCH 15/38] feat: rewrite init --force to strip hashes and union env + introspection - union of _AM_ALIASES + legacy _AM_PROJECT_ALIASES + bash/zsh scans - strip |hash suffix before emitting force_unalias - clear _AM_ALIASES/_AM_SUBCOMMANDS/_AM_PROJECT_ALIASES/_AM_PROJECT_PATH - fresh load delegates to generate_init (engine-backed) Supersedes the intermediate 'strip hash suffix' fix from eb2d5fc5 by making it correct across both env-tracked and introspected name sources. --- crates/am/src/update.rs | 54 ++++++++++++++++++++++++++++-------- crates/am/tests/snapshots.rs | 25 +++++++++++++++++ 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs index 3a65163d..ba1d4388 100644 --- a/crates/am/src/update.rs +++ b/crates/am/src/update.rs @@ -593,24 +593,56 @@ pub fn update(model: &mut AppModel, message: Message) -> Result = prev_global - .split(',') - .chain(prev_project.split(',')) - .filter(|s| !s.is_empty()) - // Strip |hash suffix from project aliases (name|hash format) - .map(|s| s.split_once('|').map_or(s, |(name, _)| name)) - .collect(); - for name in all_prev { + let prev_legacy_project = + std::env::var(env_vars::AM_PROJECT_ALIASES).unwrap_or_default(); + let prev_subs = std::env::var(env_vars::AM_SUBCOMMANDS).unwrap_or_default(); + + let mut names: std::collections::BTreeSet = + std::collections::BTreeSet::new(); + for raw in prev_global.split(',').chain(prev_legacy_project.split(',')) { + let name = raw.split_once('|').map_or(raw, |(n, _)| n); + if !name.is_empty() && !name.contains(':') { + names.insert(name.to_string()); + } + } + // Per-key subcommand entries (containing ':') are tracking-only, + // not shell functions. Program-level wrapper names (no ':') are + // picked up from prev_global because render_diff writes them there. + let _ = prev_subs; + + // Union with shell introspection for bash/zsh. + match shell { + Shell::Zsh => { + for n in zsh::scan_external_functions().iter() { + names.insert(n.clone()); + } + for n in zsh::scan_external_aliases().iter() { + names.insert(n.clone()); + } + } + Shell::Bash => { + for n in bash::scan_external_functions().iter() { + names.insert(n.clone()); + } + for n in bash::scan_external_aliases().iter() { + names.insert(n.clone()); + } + } + _ => {} + } + + for name in &names { output.push_str(&shell_impl.force_unalias(name)); output.push('\n'); } - // Clear project-alias tracking so __am_hook reloads them fresh - // instead of assuming they're still loaded. output.push_str(&shell_impl.unset_env(env_vars::AM_PROJECT_ALIASES)); output.push('\n'); output.push_str(&shell_impl.unset_env(env_vars::AM_PROJECT_PATH)); output.push('\n'); + output.push_str(&shell_impl.unset_env(env_vars::AM_SUBCOMMANDS)); + output.push('\n'); + output.push_str(&shell_impl.unset_env(env_vars::AM_ALIASES)); + output.push('\n'); } output.push_str(&generate_init( diff --git a/crates/am/tests/snapshots.rs b/crates/am/tests/snapshots.rs index 0d4a0fa0..eb7715c9 100644 --- a/crates/am/tests/snapshots.rs +++ b/crates/am/tests/snapshots.rs @@ -1155,3 +1155,28 @@ fn sync_tampered_returns_save_security_effect_and_excludes_project() { "tampered file must trigger SaveSecurity effect" ); } + +#[cfg(feature = "test-util")] +#[test] +fn init_force_unloads_introspected_names_with_hash_suffix_stripped() { + use amoxide::app_model::AppModel; + use amoxide::messages::Message; + use amoxide::shell::Shell; + use amoxide::update::update; + + // Simulate a prior session where _AM_ALIASES held name|hash entries. + // The force init must emit `unalias name` (no `|hash` suffix). + std::env::set_var("_AM_ALIASES", "b|abc1234,t|def5678"); + std::env::set_var("_AM_PROJECT_ALIASES", "p|9999999"); + + let dir = tempfile::tempdir().unwrap(); + let mut model = AppModel::load_from(dir.path().to_path_buf()); + + // The real coverage is the snapshot added in Task 15. Here we just + // assert the handler completes without panicking. + let res = update(&mut model, Message::InitShell(Shell::Fish, true)); + assert!(res.is_ok()); + + std::env::remove_var("_AM_ALIASES"); + std::env::remove_var("_AM_PROJECT_ALIASES"); +} From 43d743e5a3f63a1ddfd47ae11e24fa2138cbe28e Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 21:25:55 +0200 Subject: [PATCH 16/38] feat: shell wrappers and cd hooks invoke 'am sync' - every mutation (add/remove/use/trust/tui + profile mutations) -> 'am sync' - untrust -> 'am sync --quiet' - cd hook -> 'am sync' - bash/zsh/fish/powershell wrappers and hooks updated --- crates/am/src/init.rs | 18 +++----- crates/am/src/shell_wrappers/hook.bash | 4 +- crates/am/src/shell_wrappers/hook.fish | 4 +- crates/am/src/shell_wrappers/hook.ps1 | 4 +- crates/am/src/shell_wrappers/hook.zsh | 4 +- crates/am/src/shell_wrappers/wrapper.bash | 22 +++------- crates/am/src/shell_wrappers/wrapper.fish | 42 ++++++------------ crates/am/src/shell_wrappers/wrapper.ps1 | 52 +++++++---------------- crates/am/src/shell_wrappers/wrapper.zsh | 24 ++++------- 9 files changed, 57 insertions(+), 117 deletions(-) diff --git a/crates/am/src/init.rs b/crates/am/src/init.rs index 0dd95f1f..e3cb9bee 100644 --- a/crates/am/src/init.rs +++ b/crates/am/src/init.rs @@ -310,9 +310,7 @@ mod tests { &SubcommandSet::new(), ); assert!(output.contains("function am --wraps=am")); - assert!(output.contains("am reload fish")); - assert!(output.contains("--local")); - assert!(output.contains("am hook fish")); + assert!(output.contains("am sync fish")); } #[test] @@ -325,7 +323,7 @@ mod tests { &SubcommandSet::new(), ); assert!(output.contains("--on-variable PWD")); - assert!(output.contains("am hook fish")); + assert!(output.contains("am sync fish")); } #[test] @@ -351,9 +349,7 @@ mod tests { &SubcommandSet::new(), ); assert!(output.contains("am()")); - assert!(output.contains("am reload zsh")); - assert!(output.contains("--local")); - assert!(output.contains("am hook zsh")); + assert!(output.contains("am sync zsh")); } #[test] @@ -366,7 +362,7 @@ mod tests { &SubcommandSet::new(), ); assert!(output.contains("chpwd_functions")); - assert!(output.contains("am hook zsh")); + assert!(output.contains("am sync zsh")); } #[test] @@ -518,9 +514,7 @@ mod tests { &SubcommandSet::new(), ); assert!(output.contains("am()")); - assert!(output.contains("am reload bash")); - assert!(output.contains("--local")); - assert!(output.contains("am hook bash")); + assert!(output.contains("am sync bash")); } #[test] @@ -535,7 +529,7 @@ mod tests { assert!(output.contains("PROMPT_COMMAND")); assert!(output.contains("__am_hook")); assert!(output.contains("__am_prev_dir")); - assert!(output.contains("am hook bash")); + assert!(output.contains("am sync bash")); } #[test] diff --git a/crates/am/src/shell_wrappers/hook.bash b/crates/am/src/shell_wrappers/hook.bash index 34620d83..249b05e5 100644 --- a/crates/am/src/shell_wrappers/hook.bash +++ b/crates/am/src/shell_wrappers/hook.bash @@ -1,9 +1,9 @@ -# am cd hook: track directory changes and reload project aliases +# am cd hook: sync project aliases on directory change __am_hook() { local previous_exit_status=$? if [[ "${__am_prev_dir:-}" != "$PWD" ]]; then __am_prev_dir="$PWD" - eval "$(command am hook __SHELL__)" + eval "$(command am sync __SHELL__)" fi return $previous_exit_status } diff --git a/crates/am/src/shell_wrappers/hook.fish b/crates/am/src/shell_wrappers/hook.fish index 03bc5366..c79aafed 100644 --- a/crates/am/src/shell_wrappers/hook.fish +++ b/crates/am/src/shell_wrappers/hook.fish @@ -1,5 +1,5 @@ # am cd hook function __am_hook --on-variable PWD - am hook __SHELL__ | source + am sync __SHELL__ | source end -__am_hook \ No newline at end of file +__am_hook diff --git a/crates/am/src/shell_wrappers/hook.ps1 b/crates/am/src/shell_wrappers/hook.ps1 index 9b52aba1..54050a3f 100644 --- a/crates/am/src/shell_wrappers/hook.ps1 +++ b/crates/am/src/shell_wrappers/hook.ps1 @@ -1,11 +1,11 @@ -# am cd hook: track directory changes and reload project aliases +# am cd hook: sync project aliases on directory change $env:__AM_LAST_DIR = $PWD.Path $__am_original_prompt = $function:prompt function global:prompt { if ($PWD.Path -ne $env:__AM_LAST_DIR) { $env:__AM_LAST_DIR = $PWD.Path $amBin = (Get-Command -CommandType Application am | Select-Object -First 1).Source - $hookCode = (& $amBin hook __SHELL__) -join "`r`n" + $hookCode = (& $amBin sync __SHELL__) -join "`r`n" if ($hookCode) { Invoke-Command -ScriptBlock ([scriptblock]::Create($hookCode)) -NoNewScope } } if ($__am_original_prompt) { & $__am_original_prompt } else { "PS $($PWD.Path)> " } diff --git a/crates/am/src/shell_wrappers/hook.zsh b/crates/am/src/shell_wrappers/hook.zsh index 0516dbc5..90c50441 100644 --- a/crates/am/src/shell_wrappers/hook.zsh +++ b/crates/am/src/shell_wrappers/hook.zsh @@ -1,4 +1,4 @@ # am cd hook -__am_hook() { eval "$(am hook __SHELL__)"; } +__am_hook() { eval "$(am sync __SHELL__)"; } chpwd_functions+=(__am_hook) -__am_hook \ No newline at end of file +__am_hook diff --git a/crates/am/src/shell_wrappers/wrapper.bash b/crates/am/src/shell_wrappers/wrapper.bash index 93670ced..e9ed87e7 100644 --- a/crates/am/src/shell_wrappers/wrapper.bash +++ b/crates/am/src/shell_wrappers/wrapper.bash @@ -3,21 +3,13 @@ am() { local am_status=$? if [[ $am_status -ne 0 ]]; then return $am_status; fi case "$1" in - tui|t) eval "$(command am reload __SHELL__)"; eval "$(command am hook __SHELL__)"; return ;; - esac - case "$1" in - use|u) eval "$(command am reload __SHELL__)"; return ;; - esac - case "$1:$2" in - profile:use|p:use|profile:u|p:u|profile:add|p:add|profile:a|p:a|profile:remove|p:remove|profile:r|p:r) eval "$(command am reload __SHELL__)" ;; - esac - case "$1" in - add|a|remove|r) - case "$*" in - *\ -l\ *|*\ --local\ *|*\ -l|*\ --local) eval "$(command am hook __SHELL__)" ;; - *) eval "$(command am reload __SHELL__)" ;; + add|a|remove|r|use|u|trust|tui|t) + eval "$(command am sync __SHELL__)" ;; + untrust) + eval "$(command am sync --quiet __SHELL__)" ;; + profile|p) + case "$2" in + use|u|add|a|remove|r) eval "$(command am sync __SHELL__)" ;; esac ;; - trust) eval "$(command am hook __SHELL__)" ;; - untrust) eval "$(command am hook --quiet __SHELL__)" ;; esac } diff --git a/crates/am/src/shell_wrappers/wrapper.fish b/crates/am/src/shell_wrappers/wrapper.fish index d9cd6783..65b3ece6 100644 --- a/crates/am/src/shell_wrappers/wrapper.fish +++ b/crates/am/src/shell_wrappers/wrapper.fish @@ -1,37 +1,19 @@ -# am wrapper: reload aliases after mutations +# am wrapper: sync after mutations function am --wraps=am command am $argv set -l am_status $status if test $am_status -ne 0 return $am_status end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload __SHELL__ | source - command am hook __SHELL__ | source - return + switch "$argv[1]" + case add a remove r use u trust tui t + command am sync __SHELL__ | source + case untrust + command am sync --quiet __SHELL__ | source + case profile p + switch "$argv[2]" + case use u add a remove r + command am sync __SHELL__ | source + end end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload __SHELL__ | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload __SHELL__ | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook __SHELL__ | source - else - # profile/global alias change → reload - command am reload __SHELL__ | source - end - else if test "$argv[1]" = trust - command am hook __SHELL__ | source - else if test "$argv[1]" = untrust - command am hook --quiet __SHELL__ | source - end -end \ No newline at end of file +end diff --git a/crates/am/src/shell_wrappers/wrapper.ps1 b/crates/am/src/shell_wrappers/wrapper.ps1 index 7b476750..c8071015 100644 --- a/crates/am/src/shell_wrappers/wrapper.ps1 +++ b/crates/am/src/shell_wrappers/wrapper.ps1 @@ -1,46 +1,26 @@ -# am wrapper: reload aliases after mutations +# am wrapper: sync after mutations function am { $amBin = (Get-Command -CommandType Application am | Select-Object -First 1).Source & $amBin @args if ($LASTEXITCODE -ne 0) { return } - # tui — always reload - if ($args.Count -ge 1 -and $args[0] -in 'tui', 't') { - $out = (& $amBin reload __SHELL__) -join "`r`n" + if ($args.Count -lt 1) { return } + $first = $args[0] + $second = if ($args.Count -ge 2) { $args[1] } else { $null } + + $runSync = { + $out = (& $amBin sync __SHELL__) -join "`r`n" if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - $out = (& $amBin hook __SHELL__) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - return - } - # top-level use — reload - if ($args.Count -ge 1 -and $args[0] -in 'use', 'u') { - $out = (& $amBin reload __SHELL__) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - return } - # profile mutation — reload - if ($args.Count -ge 1 -and $args[0] -in 'profile', 'p') { - if ($args.Count -ge 2 -and $args[1] -in 'use', 'u', 'add', 'a', 'remove', 'r') { - $out = (& $amBin reload __SHELL__) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - } - } - # alias mutation — reload - elseif ($args.Count -ge 1 -and $args[0] -in 'add', 'a', 'remove', 'r') { - if ($args -contains '-l' -or $args -contains '--local') { - $out = (& $amBin hook __SHELL__) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - } else { - $out = (& $amBin reload __SHELL__) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - } - } - # trust/untrust — reload project aliases - elseif ($args.Count -ge 1 -and $args[0] -eq 'trust') { - $out = (& $amBin hook __SHELL__) -join "`r`n" + $runSyncQuiet = { + $out = (& $amBin sync --quiet __SHELL__) -join "`r`n" if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } } - elseif ($args.Count -ge 1 -and $args[0] -eq 'untrust') { - $out = (& $amBin hook --quiet __SHELL__) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } + + if ($first -in 'add', 'a', 'remove', 'r', 'use', 'u', 'trust', 'tui', 't') { + & $runSync + } elseif ($first -eq 'untrust') { + & $runSyncQuiet + } elseif ($first -in 'profile', 'p') { + if ($second -in 'use', 'u', 'add', 'a', 'remove', 'r') { & $runSync } } } diff --git a/crates/am/src/shell_wrappers/wrapper.zsh b/crates/am/src/shell_wrappers/wrapper.zsh index e5daec0d..e9ed87e7 100644 --- a/crates/am/src/shell_wrappers/wrapper.zsh +++ b/crates/am/src/shell_wrappers/wrapper.zsh @@ -3,21 +3,13 @@ am() { local am_status=$? if [[ $am_status -ne 0 ]]; then return $am_status; fi case "$1" in - tui|t) eval "$(command am reload __SHELL__)"; eval "$(command am hook __SHELL__)"; return ;; - esac - case "$1" in - use|u) eval "$(command am reload __SHELL__)"; return ;; - esac - case "$1:$2" in - profile:use|p:use|profile:u|p:u|profile:add|p:add|profile:a|p:a|profile:remove|p:remove|profile:r|p:r) eval "$(command am reload __SHELL__)" ;; - esac - case "$1" in - add|a|remove|r) - case "$*" in - *\ -l\ *|*\ --local\ *|*\ -l|*\ --local) eval "$(command am hook __SHELL__)" ;; - *) eval "$(command am reload __SHELL__)" ;; + add|a|remove|r|use|u|trust|tui|t) + eval "$(command am sync __SHELL__)" ;; + untrust) + eval "$(command am sync --quiet __SHELL__)" ;; + profile|p) + case "$2" in + use|u|add|a|remove|r) eval "$(command am sync __SHELL__)" ;; esac ;; - trust) eval "$(command am hook __SHELL__)" ;; - untrust) eval "$(command am hook --quiet __SHELL__)" ;; esac -} \ No newline at end of file +} From 8224e9066c1617b683091712772dbadee4ae72ca Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 21:40:48 +0200 Subject: [PATCH 17/38] refactor: remove 'am hook', 'am reload', hook.rs, legacy env vars - Precedence Engine fully supersedes hook/reload codegen paths - delete Commands::Hook, Commands::Reload, Message::Hook, Message::Reload - delete hook.rs (supersedes intermediate shadow-restoration patch) - delete generate_reload + generate_force_init - remove _AM_PROJECT_ALIASES, _AM_PROFILE_ALIASES_LEGACY env vars - users upgrading must run 'am init -f' once to clear legacy env vars --- completions/bash/am.bash | 72 +- completions/fish/am.fish | 41 +- completions/powershell/_am.ps1 | 26 - completions/zsh/_am | 52 - crates/am/src/bin/am.rs | 8 +- crates/am/src/cli.rs | 13 - crates/am/src/env_vars.rs | 26 +- crates/am/src/hook.rs | 1167 ----------------- crates/am/src/init.rs | 234 +--- crates/am/src/lib.rs | 1 - crates/am/src/messages.rs | 2 - crates/am/src/update.rs | 95 +- crates/am/tests/e2e.rs | 11 +- crates/am/tests/snapshots.rs | 448 +------ ...hots__snapshot_hook_bash_with_aliases.snap | 11 - ...s__snapshot_hook_fish_leaving_project.snap | 8 - ...pshots__snapshot_hook_fish_transition.snap | 10 - ...hots__snapshot_hook_fish_with_aliases.snap | 11 - ...snapshot_hook_powershell_with_aliases.snap | 11 - ...shots__snapshot_hook_zsh_with_aliases.snap | 11 - ..._init_bash_force_with_tracked_aliases.snap | 1019 -------------- ..._fish_abbr_force_with_tracked_aliases.snap | 214 --- ..._snapshot_init_fish_force_no_previous.snap | 212 --- ..._init_fish_force_with_tracked_aliases.snap | 217 --- ...pshot_reload_after_active_set_changed.snap | 9 - ...t_reload_after_parent_profile_removed.snap | 11 - ...snapshot_reload_after_profile_removed.snap | 10 - ...snapshot_reload_bash_after_global_add.snap | 12 - ...__snapshot_reload_bash_switch_profile.snap | 10 - ...snapshot_reload_fish_after_global_add.snap | 14 - ...t_reload_fish_globals_only_no_profile.snap | 8 - ...__snapshot_reload_fish_switch_profile.snap | 12 - ...shot_reload_powershell_switch_profile.snap | 8 - ..._snapshot_reload_zsh_after_global_add.snap | 12 - ...s__snapshot_reload_zsh_switch_profile.snap | 10 - 35 files changed, 41 insertions(+), 3995 deletions(-) delete mode 100644 crates/am/src/hook.rs delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_hook_bash_with_aliases.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_hook_fish_leaving_project.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_hook_fish_transition.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_hook_fish_with_aliases.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_hook_powershell_with_aliases.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_hook_zsh_with_aliases.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_init_bash_force_with_tracked_aliases.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_init_fish_abbr_force_with_tracked_aliases.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_no_previous.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_with_tracked_aliases.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_reload_after_active_set_changed.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_reload_after_parent_profile_removed.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_reload_after_profile_removed.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_reload_bash_after_global_add.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_reload_bash_switch_profile.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_reload_fish_after_global_add.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_reload_fish_globals_only_no_profile.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_reload_fish_switch_profile.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_reload_powershell_switch_profile.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_reload_zsh_after_global_add.snap delete mode 100644 crates/am/tests/snapshots/snapshots__snapshot_reload_zsh_switch_profile.snap diff --git a/completions/bash/am.bash b/completions/bash/am.bash index 9618c248..bc82bffa 100644 --- a/completions/bash/am.bash +++ b/completions/bash/am.bash @@ -25,9 +25,6 @@ _am() { am,help) cmd="am__subcmd__help" ;; - am,hook) - cmd="am__subcmd__hook" - ;; am,import) cmd="am__subcmd__import" ;; @@ -40,9 +37,6 @@ _am() { am,profile) cmd="am__subcmd__profile" ;; - am,reload) - cmd="am__subcmd__reload" - ;; am,remove) cmd="am__subcmd__remove" ;; @@ -79,9 +73,6 @@ _am() { am__subcmd__help,help) cmd="am__subcmd__help__subcmd__help" ;; - am__subcmd__help,hook) - cmd="am__subcmd__help__subcmd__hook" - ;; am__subcmd__help,import) cmd="am__subcmd__help__subcmd__import" ;; @@ -94,9 +85,6 @@ _am() { am__subcmd__help,profile) cmd="am__subcmd__help__subcmd__profile" ;; - am__subcmd__help,reload) - cmd="am__subcmd__help__subcmd__reload" - ;; am__subcmd__help,remove) cmd="am__subcmd__help__subcmd__remove" ;; @@ -173,7 +161,7 @@ _am() { case "${cmd}" in am) - opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" + opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -235,7 +223,7 @@ _am() { return 0 ;; am__subcmd__help) - opts="add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" + opts="add remove ls status profile init setup use tui export import share trust untrust sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -290,20 +278,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__hook) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__help__subcmd__import) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then @@ -416,20 +390,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__reload) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__help__subcmd__remove) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then @@ -556,20 +516,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__hook) - opts="-q -h -V --quiet --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__import) opts="-l -g -p -b -y -h -V --local --global --profile --all --base64 --yes --trust --help --version " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then @@ -782,20 +728,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__reload) - opts="-h -V --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__remove) opts="-p -l -g -h -V --profile --local --global --sub --help --version " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then diff --git a/completions/fish/am.fish b/completions/fish/am.fish index e8501e3a..6b47fc01 100644 --- a/completions/fish/am.fish +++ b/completions/fish/am.fish @@ -40,8 +40,6 @@ complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases fr complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r @@ -126,32 +124,25 @@ complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print v complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/completions/powershell/_am.ps1 b/completions/powershell/_am.ps1 index 4dde83fb..94c27c54 100644 --- a/completions/powershell/_am.ps1 +++ b/completions/powershell/_am.ps1 @@ -39,8 +39,6 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [CompletionResult]::new('share', 'share', [CompletionResultType]::ParameterValue, 'Generate a share command for posting aliases to a pastebin service') [CompletionResult]::new('trust', 'trust', [CompletionResultType]::ParameterValue, 'Review and trust the project .aliases file in the current directory') [CompletionResult]::new('untrust', 'untrust', [CompletionResultType]::ParameterValue, 'Remove trust for the project .aliases file in the current directory') - [CompletionResult]::new('hook', 'hook', [CompletionResultType]::ParameterValue, 'Internal: called by the cd hook to load/unload project aliases') - [CompletionResult]::new('reload', 'reload', [CompletionResultType]::ParameterValue, 'Internal: called by the am wrapper to reload profile aliases after switching') [CompletionResult]::new('sync', 'sync', [CompletionResultType]::ParameterValue, 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)') [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)') break @@ -262,22 +260,6 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') break } - 'am;hook' { - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Suppress info and warning messages (still unloads/loads aliases)') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Suppress info and warning messages (still unloads/loads aliases)') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') - [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') - break - } - 'am;reload' { - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') - [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') - break - } 'am;sync' { [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Suppress info and warning messages (still unloads/loads aliases)') [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Suppress info and warning messages (still unloads/loads aliases)') @@ -302,8 +284,6 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [CompletionResult]::new('share', 'share', [CompletionResultType]::ParameterValue, 'Generate a share command for posting aliases to a pastebin service') [CompletionResult]::new('trust', 'trust', [CompletionResultType]::ParameterValue, 'Review and trust the project .aliases file in the current directory') [CompletionResult]::new('untrust', 'untrust', [CompletionResultType]::ParameterValue, 'Remove trust for the project .aliases file in the current directory') - [CompletionResult]::new('hook', 'hook', [CompletionResultType]::ParameterValue, 'Internal: called by the cd hook to load/unload project aliases') - [CompletionResult]::new('reload', 'reload', [CompletionResultType]::ParameterValue, 'Internal: called by the am wrapper to reload profile aliases after switching') [CompletionResult]::new('sync', 'sync', [CompletionResultType]::ParameterValue, 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)') [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)') break @@ -366,12 +346,6 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { 'am;help;untrust' { break } - 'am;help;hook' { - break - } - 'am;help;reload' { - break - } 'am;help;sync' { break } diff --git a/completions/zsh/_am b/completions/zsh/_am index d2b62d05..e9299d8c 100644 --- a/completions/zsh/_am +++ b/completions/zsh/_am @@ -293,26 +293,6 @@ _arguments "${_arguments_options[@]}" : \ '--version[Print version]' \ && ret=0 ;; -(hook) -_arguments "${_arguments_options[@]}" : \ -'-q[Suppress info and warning messages (still unloads/loads aliases)]' \ -'--quiet[Suppress info and warning messages (still unloads/loads aliases)]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'-V[Print version]' \ -'--version[Print version]' \ -':shell:(bash brush fish powershell zsh)' \ -&& ret=0 -;; -(reload) -_arguments "${_arguments_options[@]}" : \ -'-h[Print help]' \ -'--help[Print help]' \ -'-V[Print version]' \ -'--version[Print version]' \ -':shell:(bash brush fish powershell zsh)' \ -&& ret=0 -;; (sync) _arguments "${_arguments_options[@]}" : \ '-q[Suppress info and warning messages (still unloads/loads aliases)]' \ @@ -420,14 +400,6 @@ _arguments "${_arguments_options[@]}" : \ _arguments "${_arguments_options[@]}" : \ && ret=0 ;; -(hook) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(reload) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; (sync) _arguments "${_arguments_options[@]}" : \ && ret=0 @@ -462,8 +434,6 @@ _am_commands() { 'share:Generate a share command for posting aliases to a pastebin service' \ 'trust:Review and trust the project .aliases file in the current directory' \ 'untrust:Remove trust for the project .aliases file in the current directory' \ -'hook:Internal\: called by the cd hook to load/unload project aliases' \ -'reload:Internal\: called by the am wrapper to reload profile aliases after switching' \ 'sync:Internal\: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' \ 'help:Print this message or the help of the given subcommand(s)' \ ) @@ -496,8 +466,6 @@ _am__subcmd__help_commands() { 'share:Generate a share command for posting aliases to a pastebin service' \ 'trust:Review and trust the project .aliases file in the current directory' \ 'untrust:Remove trust for the project .aliases file in the current directory' \ -'hook:Internal\: called by the cd hook to load/unload project aliases' \ -'reload:Internal\: called by the am wrapper to reload profile aliases after switching' \ 'sync:Internal\: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' \ 'help:Print this message or the help of the given subcommand(s)' \ ) @@ -518,11 +486,6 @@ _am__subcmd__help__subcmd__help_commands() { local commands; commands=() _describe -t commands 'am help help commands' commands "$@" } -(( $+functions[_am__subcmd__help__subcmd__hook_commands] )) || -_am__subcmd__help__subcmd__hook_commands() { - local commands; commands=() - _describe -t commands 'am help hook commands' commands "$@" -} (( $+functions[_am__subcmd__help__subcmd__import_commands] )) || _am__subcmd__help__subcmd__import_commands() { local commands; commands=() @@ -568,11 +531,6 @@ _am__subcmd__help__subcmd__profile__subcmd__use_commands() { local commands; commands=() _describe -t commands 'am help profile use commands' commands "$@" } -(( $+functions[_am__subcmd__help__subcmd__reload_commands] )) || -_am__subcmd__help__subcmd__reload_commands() { - local commands; commands=() - _describe -t commands 'am help reload commands' commands "$@" -} (( $+functions[_am__subcmd__help__subcmd__remove_commands] )) || _am__subcmd__help__subcmd__remove_commands() { local commands; commands=() @@ -618,11 +576,6 @@ _am__subcmd__help__subcmd__use_commands() { local commands; commands=() _describe -t commands 'am help use commands' commands "$@" } -(( $+functions[_am__subcmd__hook_commands] )) || -_am__subcmd__hook_commands() { - local commands; commands=() - _describe -t commands 'am hook commands' commands "$@" -} (( $+functions[_am__subcmd__import_commands] )) || _am__subcmd__import_commands() { local commands; commands=() @@ -705,11 +658,6 @@ _am__subcmd__profile__subcmd__use_commands() { local commands; commands=() _describe -t commands 'am profile use commands' commands "$@" } -(( $+functions[_am__subcmd__reload_commands] )) || -_am__subcmd__reload_commands() { - local commands; commands=() - _describe -t commands 'am reload commands' commands "$@" -} (( $+functions[_am__subcmd__remove_commands] )) || _am__subcmd__remove_commands() { local commands; commands=() diff --git a/crates/am/src/bin/am.rs b/crates/am/src/bin/am.rs index 8b548029..a13da576 100644 --- a/crates/am/src/bin/am.rs +++ b/crates/am/src/bin/am.rs @@ -34,7 +34,7 @@ fn setup_logging() { fn main() -> anyhow::Result<()> { // Guard against recursive invocation during alias scanning. // When `zsh -i -c alias` is spawned to enumerate existing shell aliases it - // sources the user's startup files, which call `am hook` (or `am init`). + // sources the user's startup files, which call `am sync` (or `am init`). // If those calls were allowed to run normally they could trigger another // scan, causing infinite recursion. Exiting here makes `eval "$(...)"` a // no-op, which is safe. @@ -48,7 +48,7 @@ fn main() -> anyhow::Result<()> { // Don't log for commands whose stdout is eval'd by the shell if !matches!( &cli.command, - Commands::Init { .. } | Commands::Hook { .. } | Commands::Reload { .. } | Commands::Sync { .. } + Commands::Init { .. } | Commands::Sync { .. } ) { setup_logging(); } @@ -371,7 +371,7 @@ fn main() -> anyhow::Result<()> { if answer == Answer::Yes { let result = update(&mut model, Message::Trust)?; execute_effects(&mut model, &result.effects)?; - // The shell wrapper calls `am hook` after this, which loads + // The shell wrapper calls `am sync` after this, which loads // the aliases and shows the load message. } else { let result = update(&mut model, Message::Untrust { forget: false })?; @@ -385,8 +385,6 @@ fn main() -> anyhow::Result<()> { return Ok(()); } Commands::Init { shell, force } => Message::InitShell(shell.clone(), *force), - Commands::Hook { shell, quiet } => Message::Hook(shell.clone(), *quiet), - Commands::Reload { shell } => Message::Reload(shell.clone()), Commands::Sync { shell, quiet } => Message::Sync(shell.clone(), *quiet), }; diff --git a/crates/am/src/cli.rs b/crates/am/src/cli.rs index ec708764..7a94a8ad 100644 --- a/crates/am/src/cli.rs +++ b/crates/am/src/cli.rs @@ -128,19 +128,6 @@ pub enum Commands { forget: bool, }, - /// Internal: called by the cd hook to load/unload project aliases - #[command(hide = true)] - Hook { - /// Suppress info and warning messages (still unloads/loads aliases) - #[arg(short, long)] - quiet: bool, - shell: Shell, - }, - - /// Internal: called by the am wrapper to reload profile aliases after switching - #[command(hide = true)] - Reload { shell: Shell }, - /// Internal: compute and emit the minimal shell ops to sync the shell with /// the effective merged alias state (global + profile + project). #[command(hide = true)] diff --git a/crates/am/src/env_vars.rs b/crates/am/src/env_vars.rs index 9c191c12..f42ba1b8 100644 --- a/crates/am/src/env_vars.rs +++ b/crates/am/src/env_vars.rs @@ -1,29 +1,15 @@ -/// Tracks globally-loaded alias names (global + active-profile aliases). -/// Value: comma-separated list, e.g. `"gs,ll"`. +/// Tracks effective alias state in the shell (aliases + subcommand wrappers). +/// Value: comma-separated entries of `name|short_hash`, enabling per-entry +/// change detection on sync. pub const AM_ALIASES: &str = "_AM_ALIASES"; -/// Tracks project-level aliases loaded by the cd hook. -/// Value: comma-separated entries of `name|short_hash`, e.g. `"b|a132b21,t|1241ab1"`. -/// The short hash is the first 7 hex chars of the BLAKE3 hash of the alias value, -/// enabling per-alias change detection on reload. -pub const AM_PROJECT_ALIASES: &str = "_AM_PROJECT_ALIASES"; - /// Tracks effective subcommand-key state in the shell. -/// Value: comma-separated entries of `name|short_hash`, covering both -/// program-level wrapper hashes (e.g. `jj|abc1234`) and per-key entry -/// hashes (e.g. `jj:ab|def5678`). +/// Value: comma-separated entries of `name|short_hash`. pub const AM_SUBCOMMANDS: &str = "_AM_SUBCOMMANDS"; /// Path of the `.aliases` file currently in scope, used to suppress -/// duplicate hook messages when navigating into subdirectories. +/// duplicate warnings when navigating into subdirectories. pub const AM_PROJECT_PATH: &str = "_AM_PROJECT_PATH"; -/// Set during `zsh -i -c alias` alias-detection scans to prevent recursive -/// `am` invocations from triggering another scan. When present, the `am` -/// binary exits immediately with no output so that `eval "$(...)"` in shell -/// startup scripts is a no-op. +/// Set during shell-scanning to prevent recursive am invocation. pub const AM_DETECTING_ALIASES: &str = "_AM_DETECTING_ALIASES"; - -/// Legacy tracking variable replaced by `AM_ALIASES`. Unset on startup so -/// that old installations do not leave stale state. -pub const AM_PROFILE_ALIASES_LEGACY: &str = "_AM_PROFILE_ALIASES"; diff --git a/crates/am/src/hook.rs b/crates/am/src/hook.rs deleted file mode 100644 index d9a7f62e..00000000 --- a/crates/am/src/hook.rs +++ /dev/null @@ -1,1167 +0,0 @@ -use std::collections::BTreeMap; -use std::path::Path; - -use crate::alias::AliasSet; -use crate::env_vars; -use crate::project::ProjectAliases; -use crate::security::{SecurityConfig, TrustStatus}; -use crate::shell::ShellContext; -use crate::trust::{ - compute_file_hash, compute_short_hash, render_load_message, render_unload_message, -}; - -/// Parse `_AM_PROJECT_ALIASES` value: `"name|hash,name|hash,..."` into a map. -/// Falls back to name-only format (no `|`) for backward compat during upgrade. -fn parse_prev_aliases(raw: Option<&str>) -> BTreeMap> { - let mut map = BTreeMap::new(); - let Some(s) = raw.filter(|s| !s.is_empty()) else { - return map; - }; - for entry in s.split(',') { - if let Some((name, hash)) = entry.split_once('|') { - map.insert(name.to_string(), Some(hash.to_string())); - } else { - // Backward compat: name without hash — always triggers reload - map.insert(entry.to_string(), None); - } - } - map -} - -/// Compute a short content hash for a regular alias. -/// -/// Hashes the command string which determines the shell-visible behaviour. -fn alias_content_hash(alias: &crate::alias::TomlAlias) -> String { - compute_short_hash(alias.command().as_bytes()) -} - -/// Generate shell code for the cd hook. -/// -/// `ctx.cwd` — the current working directory to search for `.aliases`. -/// `previous_aliases` — comma-separated alias entries from `_AM_PROJECT_ALIASES` env var. -pub fn generate_hook(ctx: &ShellContext, previous_aliases: Option<&str>) -> crate::Result { - let mut security = SecurityConfig::load().unwrap_or_default(); - let prev_project_path = std::env::var(env_vars::AM_PROJECT_PATH).ok(); - let (output, _changed) = generate_hook_with_security( - ctx, - previous_aliases, - prev_project_path.as_deref(), - &mut security, - false, - &AliasSet::default(), - )?; - Ok(output) -} - -/// Generate shell code for the cd hook with explicit security config. -/// -/// Returns `(shell_code, security_changed)` — `security_changed` is true -/// when a tamper was detected and `security_config` was mutated in memory. -/// -/// When `quiet` is true, info and warning echo messages are suppressed -/// (alias loading/unloading still happens). -/// -/// `prev_project_path` — the value of `_AM_PROJECT_PATH` from the shell -/// environment, used to suppress duplicate warnings. Pass `None` to treat -/// the env var as unset (e.g. in tests). -pub fn generate_hook_with_security( - ctx: &ShellContext, - previous_aliases: Option<&str>, - prev_project_path: Option<&str>, - security_config: &mut SecurityConfig, - quiet: bool, - profile_aliases: &AliasSet, -) -> crate::Result<(String, bool)> { - let shell_impl = ctx.shell.clone().as_shell( - ctx.cfg, - ctx.external_functions.clone(), - ctx.external_aliases.clone(), - ); - let cwd = ctx.cwd; - let mut lines: Vec = Vec::new(); - let mut security_changed = false; - - let prev = parse_prev_aliases(previous_aliases); - - // `prev_project_path` tracks which .aliases file was last seen, to avoid - // repeating warnings. It is passed in explicitly rather than read from the - // environment so that callers (e.g. tests) can control it independently. - - // Helper: unalias only shell-level names (no `:` — subcommand keys like `c:l` - // are tracked for change detection but are not themselves shell functions). - let unload_prev_names: Vec = - prev.keys().filter(|n| !n.contains(':')).cloned().collect(); - let unload_prev = |lines: &mut Vec| { - for name in &unload_prev_names { - lines.push(shell_impl.unalias(name)); - } - }; - - // Helper: after unloading project aliases, re-emit any global/profile - // aliases that were shadowed by the now-removed project alias. - let restore_shadowed = |lines: &mut Vec, names: &[String]| { - for name in names { - if let Some(alias) = profile_aliases.get(&crate::alias::AliasName::from(name.as_str())) - { - lines.push(shell_impl.alias(&alias.as_entry(name))); - } - } - }; - - let project_path = ProjectAliases::find_path(cwd)?; - - match project_path { - Some(path) => { - let hash = compute_file_hash(&path)?; - let status = security_config.check(&path, &hash); - - // Only show info/warning messages when: - // - not in quiet mode - // - the .aliases file is directly in cwd (not inherited from parent) - // - we haven't already shown a message for this exact file - let is_direct = path.parent().is_some_and(|p| p == cwd); - let already_seen = prev_project_path.is_some_and(|p| Path::new(p) == path); - let show_messages = !quiet && is_direct && !already_seen; - - match status { - TrustStatus::Trusted => { - let project = ProjectAliases::load(&path)?; - if !project.aliases.is_empty() || !project.subcommands.is_empty() { - let subcmd_groups = - crate::subcommand::group_by_program(&project.subcommands); - let subcmd_program_names: Vec = - subcmd_groups.keys().cloned().collect(); - - // Build current alias map: name -> short content hash - let mut current: BTreeMap = BTreeMap::new(); - - for (alias_name, alias_value) in project.aliases.iter() { - current.insert( - alias_name.as_ref().to_string(), - alias_content_hash(alias_value), - ); - } - - // Subcommand program names (shell-level wrapper function) - for program in &subcmd_program_names { - let entries_str: String = project - .subcommands - .iter() - .filter(|(k, _)| k.starts_with(&format!("{program}:"))) - .map(|(k, v)| format!("{k}={}", v.join(","))) - .collect::>() - .join(";"); - current.insert( - program.clone(), - compute_short_hash(entries_str.as_bytes()), - ); - } - - // Individual subcommand keys for fine-grained tracking - for (key, longs) in project.subcommands.iter() { - current.insert( - key.clone(), - compute_short_hash(longs.join(",").as_bytes()), - ); - } - - // Compute diff against previous state - let mut removed: Vec = Vec::new(); - let mut added: Vec = Vec::new(); - let mut changed: Vec = Vec::new(); - - for name in prev.keys() { - if !current.contains_key(name) { - removed.push(name.clone()); - } - } - for (name, hash) in ¤t { - match prev.get(name) { - None => added.push(name.clone()), - Some(prev_hash) => { - // If prev had no hash (backward compat) or hash - // differs -> changed - if prev_hash.as_deref() != Some(hash.as_str()) { - changed.push(name.clone()); - } - } - } - } - - // If nothing changed at all, skip entirely - if removed.is_empty() && added.is_empty() && changed.is_empty() { - return Ok((String::new(), false)); - } - - let is_fresh_load = prev.is_empty(); - - // 1. Unload removed + changed (not unchanged!) - for name in removed.iter().chain(changed.iter()) { - if !name.contains(':') { - lines.push(shell_impl.unalias(name)); - } - } - - // Restore global/profile aliases that were shadowed by - // removed project aliases. Changed aliases will be - // reloaded with the new project value below. - let removed_shell_names: Vec = - removed.iter().filter(|n| !n.contains(':')).cloned().collect(); - restore_shadowed(&mut lines, &removed_shell_names); - - // 2. Show messages - if show_messages { - if is_fresh_load { - // Full load message (same as cd-into-project) - for line in - render_load_message(&project.aliases, &project.subcommands) - .lines() - { - lines.push(shell_impl.echo(line)); - } - } else { - // Incremental change summary - let mut parts = Vec::new(); - if !added.is_empty() { - parts.push(format!("{} added", added.len())); - } - if !changed.is_empty() { - parts.push(format!("{} updated", changed.len())); - } - if !removed.is_empty() { - parts.push(format!("{} removed", removed.len())); - } - lines.push( - shell_impl.echo(&format!( - "am: .aliases changed ({})", - parts.join(", ") - )), - ); - } - } - - let programs_set: std::collections::BTreeSet<&str> = - subcmd_groups.keys().map(|s| s.as_str()).collect(); - - if is_fresh_load { - // 3a. Fresh load: emit all aliases - for (alias_name, alias_value) in project.aliases.iter() { - let name = alias_name.as_ref(); - if !programs_set.contains(name) { - lines.push(shell_impl.alias(&alias_value.as_entry(name))); - } - } - - // All subcommand wrappers - for (program, entries) in &subcmd_groups { - let base_cmd = project - .aliases - .iter() - .find(|(n, _)| n.as_ref() == program.as_str()) - .map(|(_, v)| v.command().to_string()) - .unwrap_or_else(|| format!("command {program}")); - lines.push( - shell_impl.subcommand_wrapper(program, &base_cmd, entries), - ); - } - } else { - // 3b. Incremental: only load added + changed aliases - for (alias_name, alias_value) in project.aliases.iter() { - let name = alias_name.as_ref(); - if !programs_set.contains(name) - && (added.contains(&name.to_string()) - || changed.contains(&name.to_string())) - { - lines.push(shell_impl.alias(&alias_value.as_entry(name))); - } - } - - // Reload subcommand wrappers if any subcmd was - // added/changed/removed - let subcmd_changed = added - .iter() - .chain(changed.iter()) - .chain(removed.iter()) - .any(|n| n.contains(':') || subcmd_program_names.contains(n)); - if subcmd_changed { - for (program, entries) in &subcmd_groups { - let base_cmd = project - .aliases - .iter() - .find(|(n, _)| n.as_ref() == program.as_str()) - .map(|(_, v)| v.command().to_string()) - .unwrap_or_else(|| format!("command {program}")); - lines.push( - shell_impl.subcommand_wrapper(program, &base_cmd, entries), - ); - } - } - } - - // 4. Update tracking env var with name|hash format - let tracking: Vec = current - .iter() - .map(|(name, hash)| format!("{name}|{hash}")) - .collect(); - lines.push( - shell_impl.set_env(env_vars::AM_PROJECT_ALIASES, &tracking.join(",")), - ); - } - } - TrustStatus::Unknown => { - unload_prev(&mut lines); - restore_shadowed(&mut lines, &unload_prev_names); - if show_messages { - lines.push(shell_impl.echo( - "am: .aliases found but not trusted. Run 'am trust' to review and allow.", - )); - } - } - TrustStatus::Untrusted => { - unload_prev(&mut lines); - restore_shadowed(&mut lines, &unload_prev_names); - } - TrustStatus::Tampered => { - unload_prev(&mut lines); - restore_shadowed(&mut lines, &unload_prev_names); - security_changed = true; - if show_messages { - lines.push(shell_impl.echo( - "am: .aliases was modified since last trusted. Run 'am trust' to review and allow.", - )); - } - } - } - - // For non-trusted states: track the path to avoid repeating warnings, - // and clear the alias tracking env var. - if !matches!(status, TrustStatus::Trusted) { - lines.push( - shell_impl.set_env(env_vars::AM_PROJECT_PATH, &path.display().to_string()), - ); - if !prev.is_empty() { - lines.push(shell_impl.unset_env(env_vars::AM_PROJECT_ALIASES)); - } - } else if prev_project_path.is_some() { - lines.push(shell_impl.unset_env(env_vars::AM_PROJECT_PATH)); - } - } - None => { - if !prev.is_empty() { - unload_prev(&mut lines); - restore_shadowed(&mut lines, &unload_prev_names); - if !quiet { - let prev_names: Vec<&str> = - unload_prev_names.iter().map(|s| s.as_str()).collect(); - lines.push(shell_impl.echo(&render_unload_message(&prev_names))); - } - lines.push(shell_impl.unset_env(env_vars::AM_PROJECT_ALIASES)); - } - if prev_project_path.is_some() { - lines.push(shell_impl.unset_env(env_vars::AM_PROJECT_PATH)); - } - } - } - - Ok((lines.join("\n"), security_changed)) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::shell::Shell; - use crate::trust::compute_short_hash; - use std::path::{Path, PathBuf}; - - /// Extract the `_AM_PROJECT_ALIASES` value from generated shell code. - fn extract_prev_aliases(output: &str, shell: &Shell) -> Option { - let prefix = match shell { - Shell::Fish => "set -gx _AM_PROJECT_ALIASES \"", - _ => "export _AM_PROJECT_ALIASES=\"", - }; - output - .lines() - .find(|l| l.contains("_AM_PROJECT_ALIASES")) - .and_then(|l| { - let start = l.find(prefix).map(|i| i + prefix.len())?; - let end = l[start..].find('"').map(|i| start + i)?; - Some(l[start..end].to_string()) - }) - } - - /// Builder for hook test fixtures. - struct TestBed { - dir: tempfile::TempDir, - aliases_content: Option, - subdirs: Vec, - security: SecurityConfig, - } - - impl TestBed { - fn new() -> Self { - Self { - dir: tempfile::tempdir().unwrap(), - aliases_content: None, - subdirs: Vec::new(), - security: SecurityConfig::default(), - } - } - - fn with_aliases(mut self, content: &str) -> Self { - self.aliases_content = Some(content.to_string()); - self - } - - fn with_subdir(mut self, rel_path: &str) -> Self { - self.subdirs.push(PathBuf::from(rel_path)); - self - } - - fn with_security_trusted(mut self) -> Self { - let path = self.aliases_path(); - if let Some(content) = &self.aliases_content { - std::fs::write(&path, content).unwrap(); - } - let hash = compute_file_hash(&path).unwrap(); - self.security.trust(&path, &hash); - self - } - - fn with_security_untrusted(mut self) -> Self { - self.security.untrust(&self.aliases_path()); - self - } - - fn with_security_tampered(mut self) -> Self { - self.security.trust(&self.aliases_path(), "wrong_hash"); - self - } - - fn setup(self) -> SetupTestBed { - let aliases_path = self.dir.path().join(".aliases"); - if let Some(content) = &self.aliases_content { - std::fs::write(&aliases_path, content).unwrap(); - } - for sub in &self.subdirs { - std::fs::create_dir_all(self.dir.path().join(sub)).unwrap(); - } - SetupTestBed { - dir: self.dir, - security: self.security, - } - } - - fn aliases_path(&self) -> PathBuf { - self.dir.path().join(".aliases") - } - } - - struct SetupTestBed { - dir: tempfile::TempDir, - security: SecurityConfig, - } - - impl SetupTestBed { - fn root(&self) -> PathBuf { - self.dir.path().to_path_buf() - } - - fn subdir(&self, rel_path: &str) -> PathBuf { - self.dir.path().join(rel_path) - } - - fn run(&mut self, shell: &Shell, cwd: &Path, prev: Option<&str>) -> (String, bool) { - self.run_with_profile_aliases(shell, cwd, prev, &AliasSet::default()) - } - - fn run_with_profile_aliases( - &mut self, - shell: &Shell, - cwd: &Path, - prev: Option<&str>, - profile_aliases: &AliasSet, - ) -> (String, bool) { - use crate::config::ShellsTomlConfig; - let cfg = ShellsTomlConfig::default(); - let ctx = ShellContext { - shell, - cfg: &cfg, - cwd, - external_functions: Default::default(), - external_aliases: Default::default(), - }; - generate_hook_with_security( - &ctx, - prev, - None, - &mut self.security, - false, - profile_aliases, - ) - .unwrap() - } - - /// Update the .aliases content and re-trust. - fn update_aliases(&mut self, content: &str) { - let path = self.dir.path().join(".aliases"); - std::fs::write(&path, content).unwrap(); - let hash = compute_file_hash(&path).unwrap(); - self.security.trust(&path, &hash); - } - } - - // ─── Basic hook behavior ──────────────────────────────────────── - - #[test] - fn test_hook_with_aliases_file() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\nt = \"make test\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, None); - assert!(output.contains("alias b \"make build\"")); - assert!(output.contains("alias t \"make test\"")); - assert!(output.contains(env_vars::AM_PROJECT_ALIASES)); - } - - #[test] - fn test_hook_unloads_previous_aliases() { - let mut t = TestBed::new().setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, Some("old1,old2")); - assert!(output.contains("functions -e old1")); - assert!(output.contains("functions -e old2")); - assert!(output.contains("set -e _AM_PROJECT_ALIASES")); - } - - #[test] - fn test_hook_no_aliases_no_previous() { - let mut t = TestBed::new().setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, None); - assert!(output.is_empty()); - } - - #[test] - fn test_hook_transitions_between_projects() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nnew1 = \"echo new\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, Some("old1,old2")); - assert!(output.contains("functions -e old1")); - assert!(output.contains("functions -e old2")); - assert!(output.contains("alias new1 \"echo new\"")); - let new1_hash = compute_short_hash(b"echo new"); - assert!( - output.contains(&format!("\"new1|{new1_hash}\"")), - "expected new1|hash in env var, got: {output}" - ); - } - - #[test] - fn test_hook_zsh_output() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Zsh, &cwd, Some("old")); - assert!(output.contains("unset -f old")); - assert!(output.contains("alias b=\"make build\"")); - assert!(output.contains("export _AM_PROJECT_ALIASES=")); - } - - #[test] - fn test_hook_picks_up_added_alias() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, None); - assert!(output.contains("alias b \"make build\"")); - assert!(!output.contains("alias t")); - - // Extract prev from first run to feed back as realistic input - let prev = extract_prev_aliases(&output, &Shell::Fish); - - t.update_aliases("[aliases]\nb = \"make build\"\nt = \"make test\"\n"); - - let (output, _) = t.run(&Shell::Fish, &cwd, prev.as_deref()); - // b is unchanged so should NOT be unloaded or reloaded - assert!( - !output.contains("functions -e b"), - "unchanged alias b should not be unloaded, got: {output}" - ); - assert!( - !output.contains("alias b \"make build\""), - "unchanged alias b should not be reloaded, got: {output}" - ); - // t is newly added - assert!(output.contains("alias t \"make test\"")); - // Env var now contains both with hashes - let b_hash = compute_short_hash(b"make build"); - let t_hash = compute_short_hash(b"make test"); - assert!( - output.contains(&format!("b|{b_hash},t|{t_hash}")), - "expected b|hash,t|hash in env var, got: {output}" - ); - } - - #[test] - fn test_hook_picks_up_removed_alias() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\nt = \"make test\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, None); - assert!(output.contains("alias b")); - assert!(output.contains("alias t")); - - // Extract prev from first run - let prev = extract_prev_aliases(&output, &Shell::Fish); - - t.update_aliases("[aliases]\nb = \"make build\"\n"); - - let (output, _) = t.run(&Shell::Fish, &cwd, prev.as_deref()); - // b is unchanged: should NOT be unloaded or reloaded - assert!( - !output.contains("functions -e b"), - "unchanged alias b should not be unloaded, got: {output}" - ); - assert!( - !output.contains("alias b \"make build\""), - "unchanged alias b should not be reloaded, got: {output}" - ); - // t is removed: should be unloaded - assert!( - output.contains("functions -e t"), - "removed alias t should be unloaded, got: {output}" - ); - assert!(!output.contains("alias t \"make test\"")); - // Env var only contains b now - let b_hash = compute_short_hash(b"make build"); - assert!( - output.contains(&format!("\"b|{b_hash}\"")), - "expected b|hash in env var, got: {output}" - ); - } - - #[test] - fn test_hook_bash_output() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Bash, &cwd, Some("old")); - assert!(output.contains("unset -f old")); - assert!(output.contains("alias b=\"make build\"")); - assert!(output.contains("export _AM_PROJECT_ALIASES=")); - } - - #[test] - fn test_hook_loads_aliases_from_parent_directory() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\nt = \"make test\"\n") - .with_subdir("src/deep") - .with_security_trusted() - .setup(); - - let sub = t.subdir("src/deep"); - let (output, _) = t.run(&Shell::Fish, &sub, None); - assert!( - output.contains("alias b \"make build\""), - "should load aliases from parent .aliases, got: {output}" - ); - assert!(output.contains("alias t \"make test\"")); - assert!(output.contains(env_vars::AM_PROJECT_ALIASES)); - } - - // ─── Trust-gated hook tests ───────────────────────────────────── - - #[test] - fn test_hook_trusted_shows_load_message() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, changed) = t.run(&Shell::Fish, &cwd, None); - assert!(!changed); - assert!(output.contains("alias b \"make build\"")); - assert!(output.contains("am: loaded .aliases")); - } - - #[test] - fn test_hook_unknown_shows_warning() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .setup(); - - let cwd = t.root(); - let (output, changed) = t.run(&Shell::Fish, &cwd, None); - assert!(!changed); - assert!(!output.contains("alias b")); - assert!(output.contains("am: .aliases found but not trusted")); - assert!(output.contains("am trust")); - } - - #[test] - fn test_hook_untrusted_silent() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_security_untrusted() - .setup(); - - let cwd = t.root(); - let (output, changed) = t.run(&Shell::Fish, &cwd, None); - assert!(!changed); - assert!(!output.contains("alias b")); - assert!(!output.contains("am:")); - } - - #[test] - fn test_hook_tampered_shows_loud_warning() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_security_tampered() - .setup(); - - let cwd = t.root(); - let (output, changed) = t.run(&Shell::Fish, &cwd, None); - assert!(changed); - assert!(!output.contains("alias b")); - assert!(output.contains("modified since last trusted")); - } - - #[test] - fn test_hook_unload_shows_message() { - let mut t = TestBed::new().setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, Some("old1,old2")); - assert!(output.contains("functions -e old1")); - assert!(output.contains("functions -e old2")); - assert!(output.contains("am: unloaded .aliases")); - } - - // ─── Subdirectory behavior ────────────────────────────────────── - - #[test] - fn test_hook_subdirectory_no_warning_for_parent_aliases() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_subdir("src") - .setup(); - - let sub = t.subdir("src"); - let (output, _) = t.run(&Shell::Fish, &sub, None); - assert!( - !output.contains("am:"), - "should not show warning for parent .aliases, got: {output}" - ); - } - - #[test] - fn test_hook_subdirectory_trusted_loads_silently() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_subdir("src") - .with_security_trusted() - .setup(); - - let sub = t.subdir("src"); - let (output, _) = t.run(&Shell::Fish, &sub, None); - assert!(output.contains("alias b \"make build\"")); - assert!( - !output.contains("am: loaded"), - "should not show load message for parent .aliases, got: {output}" - ); - } - - #[test] - fn test_hook_picks_up_new_subcommand_added_to_existing_program() { - // Regression: when a second subcommand is added under the same program (e.g. c:t after - // c:l), the hook was incorrectly skipping the reload because the set of *program names* - // hadn't changed ("c" was already in _AM_PROJECT_ALIASES). The wrapper function must - // be regenerated whenever the file content changes. - let mut t = TestBed::new() - .with_aliases("[subcommands]\n\"c:l\" = [\"clippy\"]\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - - // First run: load c:l, c wrapper is emitted - let (output, _) = t.run(&Shell::Fish, &cwd, None); - assert!( - output.contains("function c"), - "first run should emit c wrapper" - ); - assert!(output.contains("clippy")); - - // Extract prev from first run for realistic change detection - let prev = extract_prev_aliases(&output, &Shell::Fish); - - // Add c:t — the .aliases file changes, but program name `c` stays the same - t.update_aliases("[subcommands]\n\"c:l\" = [\"clippy\"]\n\"c:t\" = [\"test\"]\n"); - - // Second run: prev has c|hash and c:l|hash, but file has new content - let (output, _) = t.run(&Shell::Fish, &cwd, prev.as_deref()); - assert!( - output.contains("function c"), - "hook must re-emit c wrapper after new subcommand added, got: {output}" - ); - assert!(output.contains("test"), "updated wrapper must include c:t"); - assert!( - output.contains("clippy"), - "updated wrapper must still include c:l" - ); - } - - #[test] - fn test_hook_with_project_subcommands() { - let mut t = TestBed::new() - .with_aliases( - "[aliases]\nb = \"make build\"\n\n[subcommands]\n\"jj:ab\" = [\"abandon\"]\n", - ) - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Bash, &cwd, None); - assert!(output.contains("alias b=\"make build\"")); - assert!(output.contains("jj() {")); - assert!(output.contains("ab) shift; command jj abandon")); - } - - // ─── Per-alias content hashing ───────────────────────────────── - - #[test] - fn test_parse_prev_aliases_new_format() { - let map = parse_prev_aliases(Some("b|abc1234,t|def5678")); - assert_eq!(map.len(), 2); - assert_eq!(map["b"], Some("abc1234".to_string())); - assert_eq!(map["t"], Some("def5678".to_string())); - } - - #[test] - fn test_parse_prev_aliases_old_format_backward_compat() { - let map = parse_prev_aliases(Some("b,t")); - assert_eq!(map.len(), 2); - assert_eq!(map["b"], None); - assert_eq!(map["t"], None); - } - - #[test] - fn test_parse_prev_aliases_empty() { - assert!(parse_prev_aliases(None).is_empty()); - assert!(parse_prev_aliases(Some("")).is_empty()); - } - - #[test] - fn test_parse_prev_aliases_mixed_format() { - let map = parse_prev_aliases(Some("b|abc1234,t,gs|fed9876")); - assert_eq!(map.len(), 3); - assert_eq!(map["b"], Some("abc1234".to_string())); - assert_eq!(map["t"], None); - assert_eq!(map["gs"], Some("fed9876".to_string())); - } - - #[test] - fn test_alias_content_hash_deterministic() { - let alias = crate::TomlAlias::Command("make build".to_string()); - let h1 = alias_content_hash(&alias); - let h2 = alias_content_hash(&alias); - assert_eq!(h1, h2); - assert_eq!(h1.len(), 7); - } - - #[test] - fn test_alias_content_hash_different_commands() { - let a = crate::TomlAlias::Command("make build".to_string()); - let b = crate::TomlAlias::Command("cargo build".to_string()); - assert_ne!(alias_content_hash(&a), alias_content_hash(&b)); - } - - #[test] - fn test_hook_reloads_when_alias_value_changes() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, None); - assert!(output.contains("alias b \"make build\"")); - - // Extract prev from first run - let prev = extract_prev_aliases(&output, &Shell::Fish); - - // Update alias value (same name, different command) and re-trust - t.update_aliases("[aliases]\nb = \"cargo build\"\n"); - - // Hook with prev — same name "b" but different command - let (output, _) = t.run(&Shell::Fish, &cwd, prev.as_deref()); - assert!( - output.contains("alias b \"cargo build\""), - "hook must reload when alias value changes, got: {output}" - ); - // Old value should be unloaded first - assert!( - output.contains("functions -e b"), - "changed alias should be unloaded before reload, got: {output}" - ); - } - - #[test] - fn test_hook_skips_unchanged_aliases() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\nt = \"make test\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, None); - - // Extract prev from first run - let prev = extract_prev_aliases(&output, &Shell::Fish); - - // Re-run with same content — should skip entirely - let (output, _) = t.run(&Shell::Fish, &cwd, prev.as_deref()); - assert!( - output.is_empty(), - "unchanged aliases should produce no output, got: {output}" - ); - } - - #[test] - fn test_hook_incremental_message_on_change() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\nt = \"make test\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, None); - // Fresh load shows full message - assert!(output.contains("am: loaded .aliases")); - - let prev = extract_prev_aliases(&output, &Shell::Fish); - - // Change t, add x, remove nothing - t.update_aliases("[aliases]\nb = \"make build\"\nt = \"make test --all\"\nx = \"exit\"\n"); - - let (output, _) = t.run(&Shell::Fish, &cwd, prev.as_deref()); - // Incremental message instead of full load - assert!( - output.contains("am: .aliases changed"), - "should show incremental change message, got: {output}" - ); - assert!( - !output.contains("am: loaded .aliases"), - "should not show full load message on incremental change, got: {output}" - ); - } - - #[test] - fn test_hook_backward_compat_old_format_triggers_full_reload() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - // Old format: no hashes - let (output, _) = t.run(&Shell::Fish, &cwd, Some("b")); - // Should treat b as "changed" (prev hash is None) and reload - assert!( - output.contains("alias b \"make build\""), - "backward compat: old format should trigger reload, got: {output}" - ); - } - - #[test] - fn test_extract_prev_aliases_fish() { - let output = "set -gx _AM_PROJECT_ALIASES \"b|abc1234,t|def5678\""; - let prev = extract_prev_aliases(output, &Shell::Fish); - assert_eq!(prev, Some("b|abc1234,t|def5678".to_string())); - } - - #[test] - fn test_extract_prev_aliases_bash() { - let output = "export _AM_PROJECT_ALIASES=\"b|abc1234,t|def5678\""; - let prev = extract_prev_aliases(output, &Shell::Bash); - assert_eq!(prev, Some("b|abc1234,t|def5678".to_string())); - } - - // ─── Shadow restoration tests ────────────────────────────────── - - #[test] - fn test_hook_restores_shadowed_profile_alias_on_leave() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nt = \"cargo test --release\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - - // Build a profile alias set that also has "t" - let mut profile_aliases = AliasSet::default(); - profile_aliases.insert("t".into(), crate::TomlAlias::Command("cargo test".into())); - - // First: load project aliases - let (output, _) = - t.run_with_profile_aliases(&Shell::Fish, &cwd, None, &profile_aliases); - assert!(output.contains("alias t \"cargo test --release\"")); - let prev = extract_prev_aliases(&output, &Shell::Fish); - - // Now simulate cd away (no .aliases in new dir) - let empty_dir = tempfile::tempdir().unwrap(); - let (output, _) = t.run_with_profile_aliases( - &Shell::Fish, - empty_dir.path(), - prev.as_deref(), - &profile_aliases, - ); - - // Project alias "t" should be unloaded - assert!( - output.contains("functions -e t"), - "should unload project alias t, got: {output}" - ); - // Profile alias "t" should be restored - assert!( - output.contains("alias t \"cargo test\""), - "should restore profile alias t, got: {output}" - ); - } - - #[test] - fn test_hook_restores_shadowed_alias_on_untrusted() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nt = \"cargo test --release\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let mut profile_aliases = AliasSet::default(); - profile_aliases.insert("t".into(), crate::TomlAlias::Command("cargo test".into())); - - // Load project aliases - let (output, _) = - t.run_with_profile_aliases(&Shell::Fish, &cwd, None, &profile_aliases); - let prev = extract_prev_aliases(&output, &Shell::Fish); - - // Tamper with the file (change without re-trusting) - std::fs::write(t.dir.path().join(".aliases"), "[aliases]\nt = \"hacked\"\n").unwrap(); - - // Hook detects tamper -> unloads project aliases -> should restore profile alias - let (output, _) = t.run_with_profile_aliases( - &Shell::Fish, - &cwd, - prev.as_deref(), - &profile_aliases, - ); - assert!( - output.contains("functions -e t"), - "should unload project alias t, got: {output}" - ); - assert!( - output.contains("alias t \"cargo test\""), - "should restore profile alias t, got: {output}" - ); - } - - #[test] - fn test_hook_restores_shadowed_alias_on_incremental_remove() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\nt = \"cargo test --release\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let mut profile_aliases = AliasSet::default(); - profile_aliases.insert("t".into(), crate::TomlAlias::Command("cargo test".into())); - - // Load both project aliases - let (output, _) = - t.run_with_profile_aliases(&Shell::Fish, &cwd, None, &profile_aliases); - let prev = extract_prev_aliases(&output, &Shell::Fish); - - // Remove "t" from project (keep "b") - t.update_aliases("[aliases]\nb = \"make build\"\n"); - - let (output, _) = t.run_with_profile_aliases( - &Shell::Fish, - &cwd, - prev.as_deref(), - &profile_aliases, - ); - - // "t" was removed from project -> should be unloaded then restored from profile - assert!( - output.contains("functions -e t"), - "removed alias t should be unloaded, got: {output}" - ); - assert!( - output.contains("alias t \"cargo test\""), - "profile alias t should be restored after project removal, got: {output}" - ); - // "b" should NOT be touched (unchanged) - assert!( - !output.contains("functions -e b"), - "unchanged alias b should not be unloaded, got: {output}" - ); - } - - #[test] - fn test_hook_no_restore_when_no_profile_shadow() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nt = \"cargo test --release\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - - // Empty profile — no shadow to restore - let profile_aliases = AliasSet::default(); - - let (output, _) = - t.run_with_profile_aliases(&Shell::Fish, &cwd, None, &profile_aliases); - let prev = extract_prev_aliases(&output, &Shell::Fish); - - // cd away - let empty_dir = tempfile::tempdir().unwrap(); - let (output, _) = t.run_with_profile_aliases( - &Shell::Fish, - empty_dir.path(), - prev.as_deref(), - &profile_aliases, - ); - - assert!( - output.contains("functions -e t"), - "should unload project alias t, got: {output}" - ); - // No restore — there was no profile alias to bring back - assert!( - !output.contains("alias t"), - "should not restore any alias when profile has none, got: {output}" - ); - } -} diff --git a/crates/am/src/init.rs b/crates/am/src/init.rs index e3cb9bee..29508d6c 100644 --- a/crates/am/src/init.rs +++ b/crates/am/src/init.rs @@ -1,6 +1,5 @@ -use crate::env_vars; use crate::shell::{Shell, ShellContext}; -use crate::subcommand::{group_by_program, SubcommandSet}; +use crate::subcommand::SubcommandSet; use crate::AliasSet; const WRAPPER_BASH: &str = include_str!("shell_wrappers/wrapper.bash"); @@ -48,14 +47,10 @@ pub fn generate_init( let mut output = precedence::render_diff(&diff, shell_impl.as_ref()); - // Clean up any legacy tracking var from older installs. + // Wrapper function + cd hook + completions. if !output.is_empty() { output.push('\n'); } - output.push_str(&shell_impl.unset_env(env_vars::AM_PROFILE_ALIASES_LEGACY)); - - // Wrapper function + cd hook + completions. - output.push('\n'); output.push_str(&am_wrapper(ctx.shell)); output.push('\n'); output.push_str(&cd_hook_setup(ctx.shell)); @@ -65,117 +60,6 @@ pub fn generate_init( output } -/// Like [`generate_init`] but prepends force-cleanup lines for `prev_names`. -/// Each name is unloaded using all possible shell forms before the normal init runs. -/// Intended for testing; production code reads prev_names from env vars in `update.rs`. -pub fn generate_force_init( - ctx: &ShellContext, - global_aliases: &AliasSet, - profile_aliases: &AliasSet, - subcommands: &SubcommandSet, - prev_names: &[String], -) -> String { - let shell_impl = ctx - .shell - .clone() - .as_shell(ctx.cfg, Default::default(), Default::default()); - let mut output = String::new(); - for name in prev_names { - output.push_str(&shell_impl.force_unalias(name)); - output.push('\n'); - } - // Clear project-alias tracking so __am_hook reloads them fresh. - output.push_str(&shell_impl.unset_env(crate::env_vars::AM_PROJECT_ALIASES)); - output.push('\n'); - output.push_str(&shell_impl.unset_env(crate::env_vars::AM_PROJECT_PATH)); - output.push('\n'); - output.push_str(&generate_init( - ctx, - global_aliases, - profile_aliases, - subcommands, - )); - output -} - -/// Generate shell code to reload all aliases (global + profile) after a mutation. -/// Unloads old aliases, loads new ones, updates the tracking env var. -pub fn generate_reload( - ctx: &ShellContext, - global_aliases: &AliasSet, - profile_aliases: &AliasSet, - subcommands: &SubcommandSet, - previous_aliases: Option<&str>, -) -> String { - let shell_impl = ctx.shell.clone().as_shell( - ctx.cfg, - ctx.external_functions.clone(), - ctx.external_aliases.clone(), - ); - let mut lines: Vec = Vec::new(); - - // Unload all previously tracked aliases - let prev: Vec<&str> = previous_aliases - .filter(|s| !s.is_empty()) - .map(|s| s.split(',').collect()) - .unwrap_or_default(); - - for alias_name in &prev { - lines.push(shell_impl.unalias(alias_name)); - } - - // Determine which program names have subcommand wrappers - let subcmd_groups = group_by_program(subcommands); - let programs_with_wrappers: std::collections::BTreeSet<&str> = - subcmd_groups.keys().map(|s| s.as_str()).collect(); - - // Load global + profile aliases (skip those absorbed by subcommand wrappers) - let mut all_names: Vec = Vec::new(); - - for (alias_name, alias_value) in global_aliases.iter() { - let name = alias_name.as_ref(); - if !programs_with_wrappers.contains(name) { - lines.push(shell_impl.alias(&alias_value.as_entry(name))); - } - all_names.push(name.to_string()); - } - - for (alias_name, alias_value) in profile_aliases.iter() { - let name = alias_name.as_ref(); - if !programs_with_wrappers.contains(name) { - lines.push(shell_impl.alias(&alias_value.as_entry(name))); - } - all_names.push(name.to_string()); - } - - // Emit subcommand wrappers - for (program, entries) in &subcmd_groups { - // Determine base command: alias value if regular alias exists, else "command " - let all_aliases = global_aliases.iter().chain(profile_aliases.iter()); - let base_cmd = all_aliases - .filter(|(n, _)| n.as_ref() == program.as_str()) - .map(|(_, v)| v.command().to_string()) - .last() - .unwrap_or_else(|| format!("command {program}")); - - lines.push(shell_impl.subcommand_wrapper(program, &base_cmd, entries)); - all_names.push(program.to_string()); - } - - // Update tracking - if all_names.is_empty() { - if !prev.is_empty() { - lines.push(shell_impl.unset_env(env_vars::AM_ALIASES)); - } - } else { - all_names.sort(); - all_names.dedup(); - lines.push(shell_impl.set_env(env_vars::AM_ALIASES, &all_names.join(","))); - } - - lines.join("\n") -} - fn shell_script(template: &str, shell: &Shell) -> String { template.replace("__SHELL__", &shell.to_string()) } @@ -239,6 +123,7 @@ fn powershell_completions() -> String { mod tests { use super::*; use crate::config::ShellsTomlConfig; + use crate::env_vars; use crate::shell::ShellContext; use crate::subcommand::SubcommandSet; use crate::{AliasName, TomlAlias}; @@ -377,64 +262,6 @@ mod tests { assert!(!output.contains(env_vars::AM_ALIASES)); } - #[test] - fn test_reload_unloads_old_and_loads_new() { - let aliases = test_aliases(); - let output = generate_reload( - &default_ctx(&Shell::Fish), - &AliasSet::default(), - &aliases, - &SubcommandSet::new(), - Some("old1,old2"), - ); - assert!(output.contains("functions -e old1")); - assert!(output.contains("functions -e old2")); - assert!(output.contains("alias gs \"git status\"")); - assert!(output.contains("alias ll \"ls -lha\"")); - assert!(output.contains(env_vars::AM_ALIASES)); - } - - #[test] - fn test_reload_zsh_unloads_with_unset_f() { - let aliases = test_aliases(); - let output = generate_reload( - &default_ctx(&Shell::Zsh), - &AliasSet::default(), - &aliases, - &SubcommandSet::new(), - Some("old1"), - ); - assert!(output.contains("unset -f old1")); - assert!(output.contains("alias gs=\"git status\"")); - } - - #[test] - fn test_reload_no_previous() { - let aliases = test_aliases(); - let output = generate_reload( - &default_ctx(&Shell::Fish), - &AliasSet::default(), - &aliases, - &SubcommandSet::new(), - None, - ); - assert!(!output.contains("functions -e")); - assert!(output.contains("alias gs")); - } - - #[test] - fn test_reload_to_empty_clears_tracking() { - let output = generate_reload( - &default_ctx(&Shell::Fish), - &AliasSet::default(), - &AliasSet::default(), - &SubcommandSet::new(), - Some("old1"), - ); - assert!(output.contains("functions -e old1")); - assert!(output.contains("set -e _AM_ALIASES")); - } - #[test] fn test_init_includes_global_aliases() { let mut globals = AliasSet::default(); @@ -473,24 +300,6 @@ mod tests { ); } - #[test] - fn test_reload_includes_globals() { - let mut globals = AliasSet::default(); - globals.insert( - "ll".into(), - crate::TomlAlias::Command("ls -lha".to_string()), - ); - let output = generate_reload( - &default_ctx(&Shell::Fish), - &globals, - &AliasSet::default(), - &SubcommandSet::new(), - Some("old"), - ); - assert!(output.contains("functions -e old")); - assert!(output.contains("alias ll \"ls -lha\"")); - } - #[test] fn test_bash_init_contains_aliases() { let aliases = test_aliases(); @@ -532,20 +341,6 @@ mod tests { assert!(output.contains("am sync bash")); } - #[test] - fn test_reload_bash_unloads_with_unset_f() { - let aliases = test_aliases(); - let output = generate_reload( - &default_ctx(&Shell::Bash), - &AliasSet::default(), - &aliases, - &SubcommandSet::new(), - Some("old1"), - ); - assert!(output.contains("unset -f old1")); - assert!(output.contains("alias gs=\"git status\"")); - } - #[test] fn test_bash_init_contains_subcommand_wrapper() { let subs = test_subcommands(); @@ -643,27 +438,4 @@ mod tests { ); } - #[test] - fn test_fish_reload_with_abbr_unloads_via_abbr_erase() { - use crate::config::{FishConfig, ShellsTomlConfig}; - let cfg = ShellsTomlConfig { - fish: Some(FishConfig { use_abbr: true }), - }; - let cwd = std::path::Path::new("/tmp"); - let ctx = ShellContext { - shell: &Shell::Fish, - cfg: &cfg, - cwd, - external_functions: Default::default(), - external_aliases: Default::default(), - }; - let output = generate_reload( - &ctx, - &AliasSet::default(), - &AliasSet::default(), - &SubcommandSet::new(), - Some("old1"), - ); - assert!(output.contains("abbr --erase old1")); - } } diff --git a/crates/am/src/lib.rs b/crates/am/src/lib.rs index 711bdc7a..425e23a6 100644 --- a/crates/am/src/lib.rs +++ b/crates/am/src/lib.rs @@ -8,7 +8,6 @@ pub mod display; pub mod effects; pub mod env_vars; pub mod exchange; -pub mod hook; pub mod import_export; pub mod init; pub mod messages; diff --git a/crates/am/src/messages.rs b/crates/am/src/messages.rs index 1a719d9e..5ae2d0cc 100644 --- a/crates/am/src/messages.rs +++ b/crates/am/src/messages.rs @@ -35,8 +35,6 @@ pub enum Message { raw: bool, }, InitShell(Shell, bool), - Hook(Shell, bool), - Reload(Shell), Sync(Shell, bool), ToggleProfiles(Vec), diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs index ba1d4388..08bf77c2 100644 --- a/crates/am/src/update.rs +++ b/crates/am/src/update.rs @@ -3,7 +3,7 @@ pub use crate::app_model::AppModel; use crate::display::render_listing; use crate::effects::Effect; use crate::env_vars; -use crate::init::{generate_init, generate_reload}; +use crate::init::generate_init; use crate::precedence::{self, Precedence}; use crate::profile::AliasCollection; use crate::project::ProjectAliases; @@ -593,13 +593,11 @@ pub fn update(model: &mut AppModel, message: Message) -> Result = std::collections::BTreeSet::new(); - for raw in prev_global.split(',').chain(prev_legacy_project.split(',')) { + for raw in prev_global.split(',') { let name = raw.split_once('|').map_or(raw, |(n, _)| n); if !name.is_empty() && !name.contains(':') { names.insert(name.to_string()); @@ -635,8 +633,6 @@ pub fn update(model: &mut AppModel, message: Message) -> Result Result { - let resolved = model - .profile_config() - .resolve_active_aliases(&model.session.active_profiles); - let resolved_subs = model - .profile_config() - .resolve_active_subcommands(&model.session.active_profiles); - let mut all_subs = model.config.subcommands.clone(); - for (k, v) in resolved_subs { - all_subs.insert(k, v); - } - let prev = std::env::var(env_vars::AM_ALIASES).ok(); - let ctx = ShellContext { - shell: &shell, - cfg: &model.config.shell, - cwd: &model.cwd, - external_functions: Default::default(), - external_aliases: Default::default(), - }; - let output = generate_reload( - &ctx, - &model.config.aliases, - &resolved, - &all_subs, - prev.as_deref(), - ); - if !output.is_empty() { - print!("{output}"); - } - Ok(UpdateResult::done()) - } - Message::Hook(shell, quiet) => { - let prev = std::env::var(env_vars::AM_PROJECT_ALIASES).ok(); - let prev_project_path = std::env::var(env_vars::AM_PROJECT_PATH).ok(); - let shell_cfg = model.config.shell.clone(); - let cwd = model.cwd.clone(); - let ctx = ShellContext { - shell: &shell, - cfg: &shell_cfg, - cwd: &cwd, - external_functions: Default::default(), - external_aliases: Default::default(), - }; - - // Resolve global + active profile aliases for shadow restoration - let resolved_profile = model - .profile_config() - .resolve_active_aliases(&model.session.active_profiles); - let mut all_profile_aliases = model.config.aliases.clone(); - for (name, alias) in resolved_profile.iter() { - all_profile_aliases.insert(name.clone(), alias.clone()); - } - - let (output, security_changed) = crate::hook::generate_hook_with_security( - &ctx, - prev.as_deref(), - prev_project_path.as_deref(), - model.security_config_mut(), - quiet, - &all_profile_aliases, - ) - .map_err(|e| UpdateError::Other(e.to_string()))?; - if !output.is_empty() { - print!("{output}"); - } - if security_changed { - Ok(UpdateResult::effect(Effect::SaveSecurity)) - } else { - Ok(UpdateResult::done()) - } - } Message::Sync(shell, quiet) => { let prev_aliases = std::env::var(env_vars::AM_ALIASES).ok(); let prev_subs = std::env::var(env_vars::AM_SUBCOMMANDS).ok(); - let legacy_project = std::env::var(env_vars::AM_PROJECT_ALIASES).ok(); - let merged_prev_aliases = match (prev_aliases.as_deref(), legacy_project.as_deref()) { - (None, None) => None, - (Some(a), None) => Some(a.to_string()), - (None, Some(b)) => Some(b.to_string()), - (Some(a), Some(b)) => Some(format!("{a},{b}")), - }; let prev_project_path = std::env::var(env_vars::AM_PROJECT_PATH).ok(); let shell_cfg = model.config.shell.clone(); @@ -791,14 +709,14 @@ pub fn update(model: &mut AppModel, message: Message) -> Result Result = @@ -246,126 +243,6 @@ fn snapshot_init_bash_with_kubectl_subcommands() { insta::assert_snapshot!(output); } -// ═══════════════════════════════════════════════════════════════════════ -// Reload snapshots -// ═══════════════════════════════════════════════════════════════════════ - -#[test] -fn snapshot_reload_fish_switch_profile() { - let config = git_conventional_config(); - let resolved = config.resolve_active_aliases(&["git", "git-conventional"]); - let output = generate_reload( - &default_ctx(&Shell::Fish), - &AliasSet::default(), - &resolved, - &SubcommandSet::new(), - Some("gs,cm"), - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_reload_zsh_switch_profile() { - let config = git_conventional_config(); - let resolved = config.resolve_active_aliases(&["git", "git-conventional"]); - let output = generate_reload( - &default_ctx(&Shell::Zsh), - &AliasSet::default(), - &resolved, - &SubcommandSet::new(), - Some("gs,cm"), - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_reload_powershell_switch_profile() { - let config = git_conventional_config(); - let resolved = config.resolve_active_aliases(&["git-conventional"]); - let output = generate_reload( - &default_ctx(&Shell::Powershell), - &AliasSet::default(), - &resolved, - &SubcommandSet::new(), - Some("gs,cm"), - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_reload_bash_switch_profile() { - let config = git_conventional_config(); - let resolved = config.resolve_active_aliases(&["git", "git-conventional"]); - let output = generate_reload( - &default_ctx(&Shell::Bash), - &AliasSet::default(), - &resolved, - &SubcommandSet::new(), - Some("gs,cm"), - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_reload_fish_after_global_add() { - // Simulates: user had profile aliases loaded, then adds a global alias - let globals = aliases(&[("ll", "ls -lha")]); - let config = git_conventional_config(); - let resolved = config.resolve_active_aliases(&["git", "git-conventional"]); - let output = generate_reload( - &default_ctx(&Shell::Fish), - &globals, - &resolved, - &SubcommandSet::new(), - Some("cm,cmf,gs"), // previously tracked aliases - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_reload_fish_globals_only_no_profile() { - // No active profile, only globals - let globals = aliases(&[("ll", "ls -lha"), ("gs", "git status")]); - let output = generate_reload( - &default_ctx(&Shell::Fish), - &globals, - &AliasSet::default(), - &SubcommandSet::new(), - Some("old"), - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_reload_zsh_after_global_add() { - let globals = aliases(&[("ll", "ls -lha")]); - let config = git_conventional_config(); - let resolved = config.resolve_active_aliases(&["git", "git-conventional"]); - let output = generate_reload( - &default_ctx(&Shell::Zsh), - &globals, - &resolved, - &SubcommandSet::new(), - Some("cm,cmf,gs"), - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_reload_bash_after_global_add() { - let globals = aliases(&[("ll", "ls -lha")]); - let config = git_conventional_config(); - let resolved = config.resolve_active_aliases(&["git", "git-conventional"]); - let output = generate_reload( - &default_ctx(&Shell::Bash), - &globals, - &resolved, - &SubcommandSet::new(), - Some("cm,cmf,gs"), - ); - insta::assert_snapshot!(output); -} - #[test] fn snapshot_init_fish_globals_and_multi_profile() { // Full scenario: globals + multiple active profiles @@ -381,255 +258,6 @@ fn snapshot_init_fish_globals_and_multi_profile() { insta::assert_snapshot!(output); } -#[test] -fn snapshot_reload_after_profile_removed() { - // Scenario: rust profile had its own aliases, then was removed from active set - // Now only git's aliases should remain - let config = profiles(indoc! {r#" - [[profiles]] - name = "git" - [profiles.aliases] - gs = "git status" - cm = "git commit -sm" - - [[profiles]] - name = "rust" - [profiles.aliases] - ct = "cargo test" - "#}); - // rust is no longer in the active set - let resolved = config.resolve_active_aliases(&["git"]); - // Previously tracked: cm,ct,gs (git's + rust's aliases were all loaded) - let output = generate_reload( - &default_ctx(&Shell::Fish), - &AliasSet::default(), - &resolved, - &SubcommandSet::new(), - Some("cm,ct,gs"), - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_reload_after_parent_profile_removed() { - // Scenario: git was removed, git-conventional is now standalone - let config = profiles(indoc! {r#" - [[profiles]] - name = "git-conventional" - [profiles.aliases] - cmf = "cm feat: {{@}}" - "#}); - // git was removed, only git-conventional active - let resolved = config.resolve_active_aliases(&["git-conventional"]); - // Previously tracked: cm,cmf,gs (git's + git-conventional's were loaded) - let output = generate_reload( - &default_ctx(&Shell::Fish), - &AliasSet::default(), - &resolved, - &SubcommandSet::new(), - Some("cm,cmf,gs"), - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_reload_after_active_set_changed() { - // Scenario: previously had git+rust active, now changed to base+rust - let config = profiles(indoc! {r#" - [[profiles]] - name = "base" - [profiles.aliases] - ll = "ls -lha" - - [[profiles]] - name = "git" - [profiles.aliases] - gs = "git status" - - [[profiles]] - name = "rust" - [profiles.aliases] - ct = "cargo test" - "#}); - let resolved = config.resolve_active_aliases(&["base", "rust"]); - // Previously tracked: ct,gs (from rust + git) - // Now should have: ct,ll (from rust + base) - let output = generate_reload( - &default_ctx(&Shell::Fish), - &AliasSet::default(), - &resolved, - &SubcommandSet::new(), - Some("ct,gs"), - ); - insta::assert_snapshot!(output); -} - -// ═══════════════════════════════════════════════════════════════════════ -// Hook snapshots -// ═══════════════════════════════════════════════════════════════════════ - -#[test] -fn snapshot_hook_fish_with_aliases() { - let dir = tempfile::tempdir().unwrap(); - let aliases_path = dir.path().join(".aliases"); - fs::write( - &aliases_path, - indoc! {r#" - [aliases] - t = "cargo test" - b = "cargo build" - "#}, - ) - .unwrap(); - - let mut security = SecurityConfig::default(); - let hash = compute_file_hash(&aliases_path).unwrap(); - security.trust(&aliases_path, &hash); - - let ctx = ShellContext { - shell: &Shell::Fish, - cfg: &DEFAULT_CFG, - cwd: dir.path(), - external_functions: Default::default(), - external_aliases: Default::default(), - }; - let (output, _) = generate_hook_with_security(&ctx, None, None, &mut security, false, &AliasSet::default()).unwrap(); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_hook_zsh_with_aliases() { - let dir = tempfile::tempdir().unwrap(); - let aliases_path = dir.path().join(".aliases"); - fs::write( - &aliases_path, - indoc! {r#" - [aliases] - t = "cargo test" - b = "cargo build" - "#}, - ) - .unwrap(); - - let mut security = SecurityConfig::default(); - let hash = compute_file_hash(&aliases_path).unwrap(); - security.trust(&aliases_path, &hash); - - let ctx = ShellContext { - shell: &Shell::Zsh, - cfg: &DEFAULT_CFG, - cwd: dir.path(), - external_functions: Default::default(), - external_aliases: Default::default(), - }; - let (output, _) = generate_hook_with_security(&ctx, None, None, &mut security, false, &AliasSet::default()).unwrap(); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_hook_powershell_with_aliases() { - let dir = tempfile::tempdir().unwrap(); - let aliases_path = dir.path().join(".aliases"); - fs::write( - &aliases_path, - indoc! {r#" - [aliases] - t = "cargo test" - b = "cargo build" - "#}, - ) - .unwrap(); - - let mut security = SecurityConfig::default(); - let hash = compute_file_hash(&aliases_path).unwrap(); - security.trust(&aliases_path, &hash); - - let ctx = ShellContext { - shell: &Shell::Powershell, - cfg: &DEFAULT_CFG, - cwd: dir.path(), - external_functions: Default::default(), - external_aliases: Default::default(), - }; - let (output, _) = generate_hook_with_security(&ctx, None, None, &mut security, false, &AliasSet::default()).unwrap(); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_hook_bash_with_aliases() { - let dir = tempfile::tempdir().unwrap(); - let aliases_path = dir.path().join(".aliases"); - fs::write( - &aliases_path, - indoc! {r#" - [aliases] - t = "cargo test" - b = "cargo build" - "#}, - ) - .unwrap(); - - let mut security = SecurityConfig::default(); - let hash = compute_file_hash(&aliases_path).unwrap(); - security.trust(&aliases_path, &hash); - - let ctx = ShellContext { - shell: &Shell::Bash, - cfg: &DEFAULT_CFG, - cwd: dir.path(), - external_functions: Default::default(), - external_aliases: Default::default(), - }; - let (output, _) = generate_hook_with_security(&ctx, None, None, &mut security, false, &AliasSet::default()).unwrap(); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_hook_fish_transition() { - let dir = tempfile::tempdir().unwrap(); - let aliases_path = dir.path().join(".aliases"); - fs::write( - &aliases_path, - indoc! {r#" - [aliases] - t = "make test" - "#}, - ) - .unwrap(); - - let mut security = SecurityConfig::default(); - let hash = compute_file_hash(&aliases_path).unwrap(); - security.trust(&aliases_path, &hash); - - let ctx = ShellContext { - shell: &Shell::Fish, - cfg: &DEFAULT_CFG, - cwd: dir.path(), - external_functions: Default::default(), - external_aliases: Default::default(), - }; - let (output, _) = - generate_hook_with_security(&ctx, Some("old_a,old_b"), None, &mut security, false, &AliasSet::default()).unwrap(); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_hook_fish_leaving_project() { - let dir = tempfile::tempdir().unwrap(); - // No .aliases file - let mut security = SecurityConfig::default(); - let ctx = ShellContext { - shell: &Shell::Fish, - cfg: &DEFAULT_CFG, - cwd: dir.path(), - external_functions: Default::default(), - external_aliases: Default::default(), - }; - let (output, _) = - generate_hook_with_security(&ctx, Some("old_a,old_b"), None, &mut security, false, &AliasSet::default()).unwrap(); - insta::assert_snapshot!(output); -} - // ═══════════════════════════════════════════════════════════════════════ // Display snapshots // ═══════════════════════════════════════════════════════════════════════ @@ -1043,76 +671,6 @@ fn snapshot_share_help() { insta::assert_snapshot!(output); } -fn abbr_ctx(shell: &Shell) -> ShellContext<'_> { - static ABBR_CFG: std::sync::LazyLock = - std::sync::LazyLock::new(|| ShellsTomlConfig { - fish: Some(FishConfig { use_abbr: true }), - }); - ShellContext { - shell, - cfg: &ABBR_CFG, - cwd: std::path::Path::new("/tmp"), - external_functions: Default::default(), - external_aliases: Default::default(), - } -} - -#[test] -fn snapshot_init_fish_force_with_tracked_aliases() { - // Simulates: _AM_ALIASES=gs,ll is set, user runs am init --force fish. - // force_unalias must emit both cleanup forms for each tracked name, - // followed by the normal init output. - let prev_names = vec!["gs".to_string(), "ll".to_string()]; - let output = generate_force_init( - &default_ctx(&Shell::Fish), - &aliases(&[("gs", "git status"), ("ll", "ls -lha")]), - &AliasSet::default(), - &SubcommandSet::new(), - &prev_names, - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_init_fish_abbr_force_with_tracked_aliases() { - // Same but with use_abbr = true — cleanup must still emit both forms. - let prev_names = vec!["gs".to_string()]; - let output = generate_force_init( - &abbr_ctx(&Shell::Fish), - &aliases(&[("gs", "git status")]), - &AliasSet::default(), - &SubcommandSet::new(), - &prev_names, - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_init_fish_force_no_previous() { - // --force with no tracked aliases: no cleanup lines, just normal init. - let output = generate_force_init( - &default_ctx(&Shell::Fish), - &aliases(&[("gs", "git status")]), - &AliasSet::default(), - &SubcommandSet::new(), - &[], - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_init_bash_force_with_tracked_aliases() { - let prev_names = vec!["gs".to_string()]; - let output = generate_force_init( - &default_ctx(&Shell::Bash), - &aliases(&[("gs", "git status")]), - &AliasSet::default(), - &SubcommandSet::new(), - &prev_names, - ); - insta::assert_snapshot!(output); -} - #[test] fn sync_fresh_load_emits_aliases_and_env_var() { use amoxide::precedence::{render_diff, Precedence}; @@ -1167,7 +725,6 @@ fn init_force_unloads_introspected_names_with_hash_suffix_stripped() { // Simulate a prior session where _AM_ALIASES held name|hash entries. // The force init must emit `unalias name` (no `|hash` suffix). std::env::set_var("_AM_ALIASES", "b|abc1234,t|def5678"); - std::env::set_var("_AM_PROJECT_ALIASES", "p|9999999"); let dir = tempfile::tempdir().unwrap(); let mut model = AppModel::load_from(dir.path().to_path_buf()); @@ -1178,5 +735,4 @@ fn init_force_unloads_introspected_names_with_hash_suffix_stripped() { assert!(res.is_ok()); std::env::remove_var("_AM_ALIASES"); - std::env::remove_var("_AM_PROJECT_ALIASES"); } diff --git a/crates/am/tests/snapshots/snapshots__snapshot_hook_bash_with_aliases.snap b/crates/am/tests/snapshots/snapshots__snapshot_hook_bash_with_aliases.snap deleted file mode 100644 index cf55a960..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_hook_bash_with_aliases.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -assertion_line: 584 -expression: output ---- -printf '%s\n' 'am: loaded .aliases' -printf '%s\n' ' b → cargo build' -printf '%s\n' ' t → cargo test' -alias b="cargo build" -alias t="cargo test" -export _AM_PROJECT_ALIASES="b|b58de66,t|ab61de4" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_hook_fish_leaving_project.snap b/crates/am/tests/snapshots/snapshots__snapshot_hook_fish_leaving_project.snap deleted file mode 100644 index 6306434d..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_hook_fish_leaving_project.snap +++ /dev/null @@ -1,8 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -functions -e old_a -functions -e old_b -echo 'am: unloaded .aliases: old_a, old_b' -set -e _AM_PROJECT_ALIASES diff --git a/crates/am/tests/snapshots/snapshots__snapshot_hook_fish_transition.snap b/crates/am/tests/snapshots/snapshots__snapshot_hook_fish_transition.snap deleted file mode 100644 index e844994e..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_hook_fish_transition.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -assertion_line: 613 -expression: output ---- -functions -e old_a -functions -e old_b -echo 'am: .aliases changed (1 added, 2 removed)' -alias t "make test" -set -gx _AM_PROJECT_ALIASES "t|7f7c42c" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_hook_fish_with_aliases.snap b/crates/am/tests/snapshots/snapshots__snapshot_hook_fish_with_aliases.snap deleted file mode 100644 index ee0788a5..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_hook_fish_with_aliases.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -assertion_line: 497 -expression: output ---- -echo 'am: loaded .aliases' -echo ' b → cargo build' -echo ' t → cargo test' -alias b "cargo build" -alias t "cargo test" -set -gx _AM_PROJECT_ALIASES "b|b58de66,t|ab61de4" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_hook_powershell_with_aliases.snap b/crates/am/tests/snapshots/snapshots__snapshot_hook_powershell_with_aliases.snap deleted file mode 100644 index c7a9a1ae..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_hook_powershell_with_aliases.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -assertion_line: 555 -expression: output ---- -Write-Host 'am: loaded .aliases' -Write-Host ' b → cargo build' -Write-Host ' t → cargo test' -function global:b { cargo build @args } -function global:t { cargo test @args } -$env:_AM_PROJECT_ALIASES = "b|b58de66,t|ab61de4" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_hook_zsh_with_aliases.snap b/crates/am/tests/snapshots/snapshots__snapshot_hook_zsh_with_aliases.snap deleted file mode 100644 index de331c60..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_hook_zsh_with_aliases.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -assertion_line: 526 -expression: output ---- -printf '%s\n' 'am: loaded .aliases' -printf '%s\n' ' b → cargo build' -printf '%s\n' ' t → cargo test' -alias b="cargo build" -alias t="cargo test" -export _AM_PROJECT_ALIASES="b|b58de66,t|ab61de4" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_force_with_tracked_aliases.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_force_with_tracked_aliases.snap deleted file mode 100644 index f2bacd6e..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_force_with_tracked_aliases.snap +++ /dev/null @@ -1,1019 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -unalias gs 2>/dev/null; unset -f gs 2>/dev/null -unset _AM_PROJECT_ALIASES -unset _AM_PROJECT_PATH -alias gs="git status" -export _AM_ALIASES="gs" -unset _AM_PROFILE_ALIASES - -am() { - command am "$@" - local am_status=$? - if [[ $am_status -ne 0 ]]; then return $am_status; fi - case "$1" in - tui|t) eval "$(command am reload bash)"; eval "$(command am hook bash)"; return ;; - esac - case "$1" in - use|u) eval "$(command am reload bash)"; return ;; - esac - case "$1:$2" in - profile:use|p:use|profile:u|p:u|profile:add|p:add|profile:a|p:a|profile:remove|p:remove|profile:r|p:r) eval "$(command am reload bash)" ;; - esac - case "$1" in - add|a|remove|r) - case "$*" in - *\ -l\ *|*\ --local\ *|*\ -l|*\ --local) eval "$(command am hook bash)" ;; - *) eval "$(command am reload bash)" ;; - esac ;; - trust) eval "$(command am hook bash)" ;; - untrust) eval "$(command am hook --quiet bash)" ;; - esac -} - - -# am cd hook: track directory changes and reload project aliases -__am_hook() { - local previous_exit_status=$? - if [[ "${__am_prev_dir:-}" != "$PWD" ]]; then - __am_prev_dir="$PWD" - eval "$(command am hook bash)" - fi - return $previous_exit_status -} -if [[ "$(declare -p PROMPT_COMMAND 2>&1)" == "declare -a"* ]]; then - case " ${PROMPT_COMMAND[*]} " in - *" __am_hook "*) ;; - *) PROMPT_COMMAND=(__am_hook "${PROMPT_COMMAND[@]}") ;; - esac -else - case ";${PROMPT_COMMAND:-};" in - *";__am_hook;"*) ;; - *) PROMPT_COMMAND="__am_hook${PROMPT_COMMAND:+;$PROMPT_COMMAND}" ;; - esac -fi -__am_hook - - -_am() { - local i cur prev opts cmd - COMPREPLY=() - if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then - cur="$2" - else - cur="${COMP_WORDS[COMP_CWORD]}" - fi - prev="$3" - cmd="" - opts="" - - for i in "${COMP_WORDS[@]:0:COMP_CWORD}" - do - case "${cmd},${i}" in - ",$1") - cmd="am" - ;; - am,add) - cmd="am__subcmd__add" - ;; - am,export) - cmd="am__subcmd__export" - ;; - am,help) - cmd="am__subcmd__help" - ;; - am,hook) - cmd="am__subcmd__hook" - ;; - am,import) - cmd="am__subcmd__import" - ;; - am,init) - cmd="am__subcmd__init" - ;; - am,ls) - cmd="am__subcmd__ls" - ;; - am,profile) - cmd="am__subcmd__profile" - ;; - am,reload) - cmd="am__subcmd__reload" - ;; - am,remove) - cmd="am__subcmd__remove" - ;; - am,setup) - cmd="am__subcmd__setup" - ;; - am,share) - cmd="am__subcmd__share" - ;; - am,status) - cmd="am__subcmd__status" - ;; - am,sync) - cmd="am__subcmd__sync" - ;; - am,trust) - cmd="am__subcmd__trust" - ;; - am,tui) - cmd="am__subcmd__tui" - ;; - am,untrust) - cmd="am__subcmd__untrust" - ;; - am,use) - cmd="am__subcmd__use" - ;; - am__subcmd__help,add) - cmd="am__subcmd__help__subcmd__add" - ;; - am__subcmd__help,export) - cmd="am__subcmd__help__subcmd__export" - ;; - am__subcmd__help,help) - cmd="am__subcmd__help__subcmd__help" - ;; - am__subcmd__help,hook) - cmd="am__subcmd__help__subcmd__hook" - ;; - am__subcmd__help,import) - cmd="am__subcmd__help__subcmd__import" - ;; - am__subcmd__help,init) - cmd="am__subcmd__help__subcmd__init" - ;; - am__subcmd__help,ls) - cmd="am__subcmd__help__subcmd__ls" - ;; - am__subcmd__help,profile) - cmd="am__subcmd__help__subcmd__profile" - ;; - am__subcmd__help,reload) - cmd="am__subcmd__help__subcmd__reload" - ;; - am__subcmd__help,remove) - cmd="am__subcmd__help__subcmd__remove" - ;; - am__subcmd__help,setup) - cmd="am__subcmd__help__subcmd__setup" - ;; - am__subcmd__help,share) - cmd="am__subcmd__help__subcmd__share" - ;; - am__subcmd__help,status) - cmd="am__subcmd__help__subcmd__status" - ;; - am__subcmd__help,sync) - cmd="am__subcmd__help__subcmd__sync" - ;; - am__subcmd__help,trust) - cmd="am__subcmd__help__subcmd__trust" - ;; - am__subcmd__help,tui) - cmd="am__subcmd__help__subcmd__tui" - ;; - am__subcmd__help,untrust) - cmd="am__subcmd__help__subcmd__untrust" - ;; - am__subcmd__help,use) - cmd="am__subcmd__help__subcmd__use" - ;; - am__subcmd__help__subcmd__profile,add) - cmd="am__subcmd__help__subcmd__profile__subcmd__add" - ;; - am__subcmd__help__subcmd__profile,list) - cmd="am__subcmd__help__subcmd__profile__subcmd__list" - ;; - am__subcmd__help__subcmd__profile,remove) - cmd="am__subcmd__help__subcmd__profile__subcmd__remove" - ;; - am__subcmd__help__subcmd__profile,use) - cmd="am__subcmd__help__subcmd__profile__subcmd__use" - ;; - am__subcmd__profile,add) - cmd="am__subcmd__profile__subcmd__add" - ;; - am__subcmd__profile,help) - cmd="am__subcmd__profile__subcmd__help" - ;; - am__subcmd__profile,list) - cmd="am__subcmd__profile__subcmd__list" - ;; - am__subcmd__profile,remove) - cmd="am__subcmd__profile__subcmd__remove" - ;; - am__subcmd__profile,use) - cmd="am__subcmd__profile__subcmd__use" - ;; - am__subcmd__profile__subcmd__help,add) - cmd="am__subcmd__profile__subcmd__help__subcmd__add" - ;; - am__subcmd__profile__subcmd__help,help) - cmd="am__subcmd__profile__subcmd__help__subcmd__help" - ;; - am__subcmd__profile__subcmd__help,list) - cmd="am__subcmd__profile__subcmd__help__subcmd__list" - ;; - am__subcmd__profile__subcmd__help,remove) - cmd="am__subcmd__profile__subcmd__help__subcmd__remove" - ;; - am__subcmd__profile__subcmd__help,use) - cmd="am__subcmd__profile__subcmd__help__subcmd__use" - ;; - *) - ;; - esac - done - - case "${cmd}" in - am) - opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__add) - opts="-p -l -g -h -V --profile --local --global --raw --sub --help --version [COMMAND]..." - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --profile) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -p) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --sub) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__export) - opts="-l -g -p -b -h -V --local --global --profile --all --base64 --help --version" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --profile) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -p) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help) - opts="add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__add) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__export) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__help) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__hook) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__import) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__init) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__ls) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__profile) - opts="add use remove list" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__profile__subcmd__add) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__profile__subcmd__list) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__profile__subcmd__remove) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__profile__subcmd__use) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__reload) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__remove) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__setup) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__share) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__status) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__sync) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__trust) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__tui) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__untrust) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__use) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__hook) - opts="-q -h -V --quiet --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__import) - opts="-l -g -p -b -y -h -V --local --global --profile --all --base64 --yes --trust --help --version " - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --profile) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -p) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__init) - opts="-f -h -V --force --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__ls) - opts="-u -h -V --used --help --version" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile) - opts="-h -V --help --version add use remove list help" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__add) - opts="-h -V --help --version " - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__help) - opts="add use remove list help" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__help__subcmd__add) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__help__subcmd__help) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__help__subcmd__list) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__help__subcmd__remove) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__help__subcmd__use) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__list) - opts="-u -h -V --used --help --version" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__remove) - opts="-f -h -V --force --help --version " - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__use) - opts="-n -i -h -V --priority --inverse --help --version [NAMES]..." - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --priority) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -n) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__reload) - opts="-h -V --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__remove) - opts="-p -l -g -h -V --profile --local --global --sub --help --version " - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --profile) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -p) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --sub) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__setup) - opts="-h -V --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__share) - opts="-l -g -p -h -V --local --global --profile --all --termbin --paste-rs --help --version" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --profile) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -p) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__status) - opts="-h -V --help --version" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__sync) - opts="-q -h -V --quiet --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__trust) - opts="-h -V --help --version" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__tui) - opts="-h -V --help --version" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__untrust) - opts="-f -h -V --forget --help --version" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__use) - opts="-n -i -h -V --priority --inverse --help --version [NAMES]..." - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --priority) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -n) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - esac -} - -if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then - complete -F _am -o nosort -o bashdefault -o default am -else - complete -F _am -o bashdefault -o default am -fi diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_abbr_force_with_tracked_aliases.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_abbr_force_with_tracked_aliases.snap deleted file mode 100644 index 902ea7b0..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_abbr_force_with_tracked_aliases.snap +++ /dev/null @@ -1,214 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -functions -e gs -abbr --query gs; and abbr --erase gs -set -e _AM_PROJECT_ALIASES -set -e _AM_PROJECT_PATH -abbr --add gs "git status" -set -gx _AM_ALIASES "gs" -set -e _AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations -function am --wraps=am - command am $argv - set -l am_status $status - if test $am_status -ne 0 - return $am_status - end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload fish | source - command am hook fish | source - return - end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload fish | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload fish | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook fish | source - else - # profile/global alias change → reload - command am reload fish | source - end - else if test "$argv[1]" = trust - command am hook fish | source - else if test "$argv[1]" = untrust - command am hook --quiet fish | source - end -end - -# am cd hook -function __am_hook --on-variable PWD - am hook fish | source -end -__am_hook - -# Print an optspec for argparse to handle cmd's options that are independent of any subcommand. -function __fish_am_global_optspecs - string join \n h/help V/version -end - -function __fish_am_needs_command - # Figure out if the current invocation already has a command. - set -l cmd (commandline -opc) - set -e cmd[1] - argparse -s (__fish_am_global_optspecs) -- $cmd 2>/dev/null - or return - if set -q argv[1] - # Also print the command, so this can be used to figure out what it is. - echo $argv[1] - return 1 - end - return 0 -end - -function __fish_am_using_subcommand - set -l cmd (__fish_am_needs_command) - test -z "$cmd" - and return 1 - contains -- $cmd[1] $argv -end - -complete -c am -n "__fish_am_needs_command" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_needs_command" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_needs_command" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_needs_command" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_needs_command" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_needs_command" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_needs_command" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_needs_command" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_needs_command" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_needs_command" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_needs_command" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' -complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r -complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r -complete -c am -n "__fish_am_using_subcommand add" -s l -l local -d 'Add to the project\'s .aliases file instead of a profile' -complete -c am -n "__fish_am_using_subcommand add" -s g -l global -d 'Add as a global alias (always loaded, independent of profile)' -complete -c am -n "__fish_am_using_subcommand add" -l raw -d 'Disable {{N}} template detection (treat command as literal)' -complete -c am -n "__fish_am_using_subcommand add" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand add" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand remove" -s p -l profile -d 'Profile to remove the alias from (defaults to active profile)' -r -complete -c am -n "__fish_am_using_subcommand remove" -l sub -d 'Subcommand path segments to complete the key (e.g. --sub b --sub l removes jj:b:l)' -r -complete -c am -n "__fish_am_using_subcommand remove" -s l -l local -d 'Remove from the project\'s .aliases file instead of a profile' -complete -c am -n "__fish_am_using_subcommand remove" -s g -l global -d 'Remove a global alias' -complete -c am -n "__fish_am_using_subcommand remove" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand remove" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand ls" -s u -l used -d 'Show only active profiles and loaded project aliases' -complete -c am -n "__fish_am_using_subcommand ls" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand ls" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand status" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand status" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "add" -d 'Add a new profile' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "remove" -d 'Remove a profile' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "list" -d 'List all profiles' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from add" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from add" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s n -l priority -d 'Activate at specific priority position (1-based). Repositions if already active' -r -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s i -l inverse -d 'Reverse the processing order (first listed = highest priority)' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from remove" -s f -l force -d 'Skip confirmation prompt' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from remove" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from remove" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from list" -s u -l used -d 'Show only active profiles and loaded project aliases' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from list" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "add" -d 'Add a new profile' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "remove" -d 'Remove a profile' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "list" -d 'List all profiles' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand init" -s f -l force -d 'Force re-initialisation: unload all previously tracked aliases (both alias and function forms) before re-loading. Use after config changes such as toggling `use_abbr`' -complete -c am -n "__fish_am_using_subcommand init" -s h -l help -d 'Print help (see more with \'--help\')' -complete -c am -n "__fish_am_using_subcommand init" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand setup" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand setup" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand use" -s n -l priority -d 'Activate at specific priority position (1-based). Repositions if already active' -r -complete -c am -n "__fish_am_using_subcommand use" -s i -l inverse -d 'Reverse the processing order (first listed = highest priority)' -complete -c am -n "__fish_am_using_subcommand use" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand use" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand tui" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand tui" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand export" -s p -l profile -d 'Operate on specific profile(s) — can be repeated' -r -complete -c am -n "__fish_am_using_subcommand export" -s l -l local -d 'Operate on project-local aliases' -complete -c am -n "__fish_am_using_subcommand export" -s g -l global -d 'Operate on global aliases' -complete -c am -n "__fish_am_using_subcommand export" -l all -d 'Operate on everything (global + all profiles + local)' -complete -c am -n "__fish_am_using_subcommand export" -s b -l base64 -d 'Encode output as base64' -complete -c am -n "__fish_am_using_subcommand export" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand export" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand import" -s p -l profile -d 'Operate on specific profile(s) — can be repeated' -r -complete -c am -n "__fish_am_using_subcommand import" -s l -l local -d 'Operate on project-local aliases' -complete -c am -n "__fish_am_using_subcommand import" -s g -l global -d 'Operate on global aliases' -complete -c am -n "__fish_am_using_subcommand import" -l all -d 'Operate on everything (global + all profiles + local)' -complete -c am -n "__fish_am_using_subcommand import" -s b -l base64 -d 'Decode base64 input before parsing' -complete -c am -n "__fish_am_using_subcommand import" -s y -l yes -d 'Skip all confirmation prompts' -complete -c am -n "__fish_am_using_subcommand import" -l trust -d 'DANGER: Skip safety checks for suspicious content (escape sequences). Only use for your own exports. Never trust external input blindly — it can carry invisible escape sequences that hide malicious commands' -complete -c am -n "__fish_am_using_subcommand import" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand import" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand share" -s p -l profile -d 'Operate on specific profile(s) — can be repeated' -r -complete -c am -n "__fish_am_using_subcommand share" -s l -l local -d 'Operate on project-local aliases' -complete -c am -n "__fish_am_using_subcommand share" -s g -l global -d 'Operate on global aliases' -complete -c am -n "__fish_am_using_subcommand share" -l all -d 'Operate on everything (global + all profiles + local)' -complete -c am -n "__fish_am_using_subcommand share" -l termbin -d 'Generate command for termbin.com (netcat)' -complete -c am -n "__fish_am_using_subcommand share" -l paste-rs -d 'Generate command for paste.rs (curl)' -complete -c am -n "__fish_am_using_subcommand share" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand share" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand trust" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' -complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "list" -d 'List all profiles' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_no_previous.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_no_previous.snap deleted file mode 100644 index c3a5fec0..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_no_previous.snap +++ /dev/null @@ -1,212 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -set -e _AM_PROJECT_ALIASES -set -e _AM_PROJECT_PATH -alias gs "git status" -set -gx _AM_ALIASES "gs" -set -e _AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations -function am --wraps=am - command am $argv - set -l am_status $status - if test $am_status -ne 0 - return $am_status - end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload fish | source - command am hook fish | source - return - end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload fish | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload fish | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook fish | source - else - # profile/global alias change → reload - command am reload fish | source - end - else if test "$argv[1]" = trust - command am hook fish | source - else if test "$argv[1]" = untrust - command am hook --quiet fish | source - end -end - -# am cd hook -function __am_hook --on-variable PWD - am hook fish | source -end -__am_hook - -# Print an optspec for argparse to handle cmd's options that are independent of any subcommand. -function __fish_am_global_optspecs - string join \n h/help V/version -end - -function __fish_am_needs_command - # Figure out if the current invocation already has a command. - set -l cmd (commandline -opc) - set -e cmd[1] - argparse -s (__fish_am_global_optspecs) -- $cmd 2>/dev/null - or return - if set -q argv[1] - # Also print the command, so this can be used to figure out what it is. - echo $argv[1] - return 1 - end - return 0 -end - -function __fish_am_using_subcommand - set -l cmd (__fish_am_needs_command) - test -z "$cmd" - and return 1 - contains -- $cmd[1] $argv -end - -complete -c am -n "__fish_am_needs_command" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_needs_command" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_needs_command" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_needs_command" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_needs_command" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_needs_command" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_needs_command" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_needs_command" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_needs_command" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_needs_command" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_needs_command" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' -complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r -complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r -complete -c am -n "__fish_am_using_subcommand add" -s l -l local -d 'Add to the project\'s .aliases file instead of a profile' -complete -c am -n "__fish_am_using_subcommand add" -s g -l global -d 'Add as a global alias (always loaded, independent of profile)' -complete -c am -n "__fish_am_using_subcommand add" -l raw -d 'Disable {{N}} template detection (treat command as literal)' -complete -c am -n "__fish_am_using_subcommand add" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand add" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand remove" -s p -l profile -d 'Profile to remove the alias from (defaults to active profile)' -r -complete -c am -n "__fish_am_using_subcommand remove" -l sub -d 'Subcommand path segments to complete the key (e.g. --sub b --sub l removes jj:b:l)' -r -complete -c am -n "__fish_am_using_subcommand remove" -s l -l local -d 'Remove from the project\'s .aliases file instead of a profile' -complete -c am -n "__fish_am_using_subcommand remove" -s g -l global -d 'Remove a global alias' -complete -c am -n "__fish_am_using_subcommand remove" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand remove" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand ls" -s u -l used -d 'Show only active profiles and loaded project aliases' -complete -c am -n "__fish_am_using_subcommand ls" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand ls" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand status" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand status" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "add" -d 'Add a new profile' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "remove" -d 'Remove a profile' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "list" -d 'List all profiles' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from add" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from add" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s n -l priority -d 'Activate at specific priority position (1-based). Repositions if already active' -r -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s i -l inverse -d 'Reverse the processing order (first listed = highest priority)' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from remove" -s f -l force -d 'Skip confirmation prompt' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from remove" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from remove" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from list" -s u -l used -d 'Show only active profiles and loaded project aliases' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from list" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "add" -d 'Add a new profile' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "remove" -d 'Remove a profile' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "list" -d 'List all profiles' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand init" -s f -l force -d 'Force re-initialisation: unload all previously tracked aliases (both alias and function forms) before re-loading. Use after config changes such as toggling `use_abbr`' -complete -c am -n "__fish_am_using_subcommand init" -s h -l help -d 'Print help (see more with \'--help\')' -complete -c am -n "__fish_am_using_subcommand init" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand setup" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand setup" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand use" -s n -l priority -d 'Activate at specific priority position (1-based). Repositions if already active' -r -complete -c am -n "__fish_am_using_subcommand use" -s i -l inverse -d 'Reverse the processing order (first listed = highest priority)' -complete -c am -n "__fish_am_using_subcommand use" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand use" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand tui" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand tui" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand export" -s p -l profile -d 'Operate on specific profile(s) — can be repeated' -r -complete -c am -n "__fish_am_using_subcommand export" -s l -l local -d 'Operate on project-local aliases' -complete -c am -n "__fish_am_using_subcommand export" -s g -l global -d 'Operate on global aliases' -complete -c am -n "__fish_am_using_subcommand export" -l all -d 'Operate on everything (global + all profiles + local)' -complete -c am -n "__fish_am_using_subcommand export" -s b -l base64 -d 'Encode output as base64' -complete -c am -n "__fish_am_using_subcommand export" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand export" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand import" -s p -l profile -d 'Operate on specific profile(s) — can be repeated' -r -complete -c am -n "__fish_am_using_subcommand import" -s l -l local -d 'Operate on project-local aliases' -complete -c am -n "__fish_am_using_subcommand import" -s g -l global -d 'Operate on global aliases' -complete -c am -n "__fish_am_using_subcommand import" -l all -d 'Operate on everything (global + all profiles + local)' -complete -c am -n "__fish_am_using_subcommand import" -s b -l base64 -d 'Decode base64 input before parsing' -complete -c am -n "__fish_am_using_subcommand import" -s y -l yes -d 'Skip all confirmation prompts' -complete -c am -n "__fish_am_using_subcommand import" -l trust -d 'DANGER: Skip safety checks for suspicious content (escape sequences). Only use for your own exports. Never trust external input blindly — it can carry invisible escape sequences that hide malicious commands' -complete -c am -n "__fish_am_using_subcommand import" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand import" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand share" -s p -l profile -d 'Operate on specific profile(s) — can be repeated' -r -complete -c am -n "__fish_am_using_subcommand share" -s l -l local -d 'Operate on project-local aliases' -complete -c am -n "__fish_am_using_subcommand share" -s g -l global -d 'Operate on global aliases' -complete -c am -n "__fish_am_using_subcommand share" -l all -d 'Operate on everything (global + all profiles + local)' -complete -c am -n "__fish_am_using_subcommand share" -l termbin -d 'Generate command for termbin.com (netcat)' -complete -c am -n "__fish_am_using_subcommand share" -l paste-rs -d 'Generate command for paste.rs (curl)' -complete -c am -n "__fish_am_using_subcommand share" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand share" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand trust" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' -complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "list" -d 'List all profiles' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_with_tracked_aliases.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_with_tracked_aliases.snap deleted file mode 100644 index a271b414..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_with_tracked_aliases.snap +++ /dev/null @@ -1,217 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -functions -e gs -abbr --query gs; and abbr --erase gs -functions -e ll -abbr --query ll; and abbr --erase ll -set -e _AM_PROJECT_ALIASES -set -e _AM_PROJECT_PATH -alias gs "git status" -alias ll "ls -lha" -set -gx _AM_ALIASES "gs,ll" -set -e _AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations -function am --wraps=am - command am $argv - set -l am_status $status - if test $am_status -ne 0 - return $am_status - end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload fish | source - command am hook fish | source - return - end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload fish | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload fish | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook fish | source - else - # profile/global alias change → reload - command am reload fish | source - end - else if test "$argv[1]" = trust - command am hook fish | source - else if test "$argv[1]" = untrust - command am hook --quiet fish | source - end -end - -# am cd hook -function __am_hook --on-variable PWD - am hook fish | source -end -__am_hook - -# Print an optspec for argparse to handle cmd's options that are independent of any subcommand. -function __fish_am_global_optspecs - string join \n h/help V/version -end - -function __fish_am_needs_command - # Figure out if the current invocation already has a command. - set -l cmd (commandline -opc) - set -e cmd[1] - argparse -s (__fish_am_global_optspecs) -- $cmd 2>/dev/null - or return - if set -q argv[1] - # Also print the command, so this can be used to figure out what it is. - echo $argv[1] - return 1 - end - return 0 -end - -function __fish_am_using_subcommand - set -l cmd (__fish_am_needs_command) - test -z "$cmd" - and return 1 - contains -- $cmd[1] $argv -end - -complete -c am -n "__fish_am_needs_command" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_needs_command" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_needs_command" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_needs_command" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_needs_command" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_needs_command" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_needs_command" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_needs_command" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_needs_command" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_needs_command" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_needs_command" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' -complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r -complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r -complete -c am -n "__fish_am_using_subcommand add" -s l -l local -d 'Add to the project\'s .aliases file instead of a profile' -complete -c am -n "__fish_am_using_subcommand add" -s g -l global -d 'Add as a global alias (always loaded, independent of profile)' -complete -c am -n "__fish_am_using_subcommand add" -l raw -d 'Disable {{N}} template detection (treat command as literal)' -complete -c am -n "__fish_am_using_subcommand add" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand add" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand remove" -s p -l profile -d 'Profile to remove the alias from (defaults to active profile)' -r -complete -c am -n "__fish_am_using_subcommand remove" -l sub -d 'Subcommand path segments to complete the key (e.g. --sub b --sub l removes jj:b:l)' -r -complete -c am -n "__fish_am_using_subcommand remove" -s l -l local -d 'Remove from the project\'s .aliases file instead of a profile' -complete -c am -n "__fish_am_using_subcommand remove" -s g -l global -d 'Remove a global alias' -complete -c am -n "__fish_am_using_subcommand remove" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand remove" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand ls" -s u -l used -d 'Show only active profiles and loaded project aliases' -complete -c am -n "__fish_am_using_subcommand ls" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand ls" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand status" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand status" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "add" -d 'Add a new profile' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "remove" -d 'Remove a profile' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "list" -d 'List all profiles' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from add" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from add" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s n -l priority -d 'Activate at specific priority position (1-based). Repositions if already active' -r -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s i -l inverse -d 'Reverse the processing order (first listed = highest priority)' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from remove" -s f -l force -d 'Skip confirmation prompt' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from remove" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from remove" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from list" -s u -l used -d 'Show only active profiles and loaded project aliases' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from list" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "add" -d 'Add a new profile' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "remove" -d 'Remove a profile' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "list" -d 'List all profiles' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand init" -s f -l force -d 'Force re-initialisation: unload all previously tracked aliases (both alias and function forms) before re-loading. Use after config changes such as toggling `use_abbr`' -complete -c am -n "__fish_am_using_subcommand init" -s h -l help -d 'Print help (see more with \'--help\')' -complete -c am -n "__fish_am_using_subcommand init" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand setup" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand setup" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand use" -s n -l priority -d 'Activate at specific priority position (1-based). Repositions if already active' -r -complete -c am -n "__fish_am_using_subcommand use" -s i -l inverse -d 'Reverse the processing order (first listed = highest priority)' -complete -c am -n "__fish_am_using_subcommand use" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand use" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand tui" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand tui" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand export" -s p -l profile -d 'Operate on specific profile(s) — can be repeated' -r -complete -c am -n "__fish_am_using_subcommand export" -s l -l local -d 'Operate on project-local aliases' -complete -c am -n "__fish_am_using_subcommand export" -s g -l global -d 'Operate on global aliases' -complete -c am -n "__fish_am_using_subcommand export" -l all -d 'Operate on everything (global + all profiles + local)' -complete -c am -n "__fish_am_using_subcommand export" -s b -l base64 -d 'Encode output as base64' -complete -c am -n "__fish_am_using_subcommand export" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand export" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand import" -s p -l profile -d 'Operate on specific profile(s) — can be repeated' -r -complete -c am -n "__fish_am_using_subcommand import" -s l -l local -d 'Operate on project-local aliases' -complete -c am -n "__fish_am_using_subcommand import" -s g -l global -d 'Operate on global aliases' -complete -c am -n "__fish_am_using_subcommand import" -l all -d 'Operate on everything (global + all profiles + local)' -complete -c am -n "__fish_am_using_subcommand import" -s b -l base64 -d 'Decode base64 input before parsing' -complete -c am -n "__fish_am_using_subcommand import" -s y -l yes -d 'Skip all confirmation prompts' -complete -c am -n "__fish_am_using_subcommand import" -l trust -d 'DANGER: Skip safety checks for suspicious content (escape sequences). Only use for your own exports. Never trust external input blindly — it can carry invisible escape sequences that hide malicious commands' -complete -c am -n "__fish_am_using_subcommand import" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand import" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand share" -s p -l profile -d 'Operate on specific profile(s) — can be repeated' -r -complete -c am -n "__fish_am_using_subcommand share" -s l -l local -d 'Operate on project-local aliases' -complete -c am -n "__fish_am_using_subcommand share" -s g -l global -d 'Operate on global aliases' -complete -c am -n "__fish_am_using_subcommand share" -l all -d 'Operate on everything (global + all profiles + local)' -complete -c am -n "__fish_am_using_subcommand share" -l termbin -d 'Generate command for termbin.com (netcat)' -complete -c am -n "__fish_am_using_subcommand share" -l paste-rs -d 'Generate command for paste.rs (curl)' -complete -c am -n "__fish_am_using_subcommand share" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand share" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand trust" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' -complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "list" -d 'List all profiles' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_after_active_set_changed.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_after_active_set_changed.snap deleted file mode 100644 index 5c9767bb..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_after_active_set_changed.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -functions -e ct -functions -e gs -alias ct "cargo test" -alias ll "ls -lha" -set -gx _AM_ALIASES "ct,ll" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_after_parent_profile_removed.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_after_parent_profile_removed.snap deleted file mode 100644 index b6e82c3d..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_after_parent_profile_removed.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -functions -e cm -functions -e cmf -functions -e gs -function cmf - cm feat: $argv -end -set -gx _AM_ALIASES "cmf" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_after_profile_removed.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_after_profile_removed.snap deleted file mode 100644 index 3b4c6b93..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_after_profile_removed.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -functions -e cm -functions -e ct -functions -e gs -alias cm "git commit -sm" -alias gs "git status" -set -gx _AM_ALIASES "cm,gs" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_bash_after_global_add.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_bash_after_global_add.snap deleted file mode 100644 index bc346a2f..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_bash_after_global_add.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -unalias cm 2>/dev/null; unset -f cm 2>/dev/null -unalias cmf 2>/dev/null; unset -f cmf 2>/dev/null -unalias gs 2>/dev/null; unset -f gs 2>/dev/null -alias ll="ls -lha" -alias cm="git commit -sm" -cmf() { cm feat: "$@"; } -alias gs="git status" -export _AM_ALIASES="cm,cmf,gs,ll" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_bash_switch_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_bash_switch_profile.snap deleted file mode 100644 index a67b370f..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_bash_switch_profile.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -unalias gs 2>/dev/null; unset -f gs 2>/dev/null -unalias cm 2>/dev/null; unset -f cm 2>/dev/null -alias cm="git commit -sm" -cmf() { cm feat: "$@"; } -alias gs="git status" -export _AM_ALIASES="cm,cmf,gs" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_fish_after_global_add.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_fish_after_global_add.snap deleted file mode 100644 index 6a0b444c..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_fish_after_global_add.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -functions -e cm -functions -e cmf -functions -e gs -alias ll "ls -lha" -alias cm "git commit -sm" -function cmf - cm feat: $argv -end -alias gs "git status" -set -gx _AM_ALIASES "cm,cmf,gs,ll" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_fish_globals_only_no_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_fish_globals_only_no_profile.snap deleted file mode 100644 index 0913485e..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_fish_globals_only_no_profile.snap +++ /dev/null @@ -1,8 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -functions -e old -alias gs "git status" -alias ll "ls -lha" -set -gx _AM_ALIASES "gs,ll" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_fish_switch_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_fish_switch_profile.snap deleted file mode 100644 index 87a263f9..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_fish_switch_profile.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -functions -e gs -functions -e cm -alias cm "git commit -sm" -function cmf - cm feat: $argv -end -alias gs "git status" -set -gx _AM_ALIASES "cm,cmf,gs" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_powershell_switch_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_powershell_switch_profile.snap deleted file mode 100644 index ef9610c5..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_powershell_switch_profile.snap +++ /dev/null @@ -1,8 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -if (Test-Path Function:\gs) { Remove-Item Function:\gs } -if (Test-Path Function:\cm) { Remove-Item Function:\cm } -function global:cmf { cm feat: $args } -$env:_AM_ALIASES = "cmf" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_zsh_after_global_add.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_zsh_after_global_add.snap deleted file mode 100644 index bc346a2f..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_zsh_after_global_add.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -unalias cm 2>/dev/null; unset -f cm 2>/dev/null -unalias cmf 2>/dev/null; unset -f cmf 2>/dev/null -unalias gs 2>/dev/null; unset -f gs 2>/dev/null -alias ll="ls -lha" -alias cm="git commit -sm" -cmf() { cm feat: "$@"; } -alias gs="git status" -export _AM_ALIASES="cm,cmf,gs,ll" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_zsh_switch_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_zsh_switch_profile.snap deleted file mode 100644 index a67b370f..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_zsh_switch_profile.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -unalias gs 2>/dev/null; unset -f gs 2>/dev/null -unalias cm 2>/dev/null; unset -f cm 2>/dev/null -alias cm="git commit -sm" -cmf() { cm feat: "$@"; } -alias gs="git status" -export _AM_ALIASES="cm,cmf,gs" From 268fd56b30895029abf67db8d6bb43d68c29d78e Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 21:46:33 +0200 Subject: [PATCH 18/38] test: regenerate snapshots for Precedence Engine output - accept 10 updated init snapshots: name|hash format, 'am sync' wrapper text - add 8 new sync-scenario snapshots covering fresh load, incremental, transitions, shadow restoration, and subcommand wrappers across shells --- crates/am/tests/snapshots.rs | 117 ++++++++++++++++++ ...ts__snapshot_init_bash_simple_profile.snap | 104 ++-------------- ...ot_init_bash_with_kubectl_subcommands.snap | 105 ++-------------- ...pshots__snapshot_init_fish_deep_chain.snap | 87 +++++-------- ...t_init_fish_globals_and_multi_profile.snap | 89 +++++-------- ...ots__snapshot_init_fish_multi_profile.snap | 87 +++++-------- ...ts__snapshot_init_fish_simple_profile.snap | 87 +++++-------- ...hots__snapshot_init_fish_with_globals.snap | 89 +++++-------- ...hot_init_fish_with_simple_subcommands.snap | 88 +++++-------- ...apshot_init_powershell_simple_profile.snap | 88 +++---------- ...ots__snapshot_init_zsh_simple_profile.snap | 80 ++---------- ...hot_sync_bash_fresh_load_project_only.snap | 6 + ...nc_bash_subcommand_wrapper_fresh_load.snap | 12 ++ ...hot_sync_fish_fresh_load_project_only.snap | 7 ++ ...nc_fish_incremental_one_alias_updated.snap | 7 ++ ...aving_project_with_shadow_restoration.snap | 8 ++ ...t_sync_fish_transition_to_new_project.snap | 7 ++ ...nc_powershell_fresh_load_project_only.snap | 6 + ...shot_sync_zsh_fresh_load_project_only.snap | 6 + 19 files changed, 406 insertions(+), 674 deletions(-) create mode 100644 crates/am/tests/snapshots/snapshots__snapshot_sync_bash_fresh_load_project_only.snap create mode 100644 crates/am/tests/snapshots/snapshots__snapshot_sync_bash_subcommand_wrapper_fresh_load.snap create mode 100644 crates/am/tests/snapshots/snapshots__snapshot_sync_fish_fresh_load_project_only.snap create mode 100644 crates/am/tests/snapshots/snapshots__snapshot_sync_fish_incremental_one_alias_updated.snap create mode 100644 crates/am/tests/snapshots/snapshots__snapshot_sync_fish_leaving_project_with_shadow_restoration.snap create mode 100644 crates/am/tests/snapshots/snapshots__snapshot_sync_fish_transition_to_new_project.snap create mode 100644 crates/am/tests/snapshots/snapshots__snapshot_sync_powershell_fresh_load_project_only.snap create mode 100644 crates/am/tests/snapshots/snapshots__snapshot_sync_zsh_fresh_load_project_only.snap diff --git a/crates/am/tests/snapshots.rs b/crates/am/tests/snapshots.rs index 67396fde..9407b7c0 100644 --- a/crates/am/tests/snapshots.rs +++ b/crates/am/tests/snapshots.rs @@ -736,3 +736,120 @@ fn init_force_unloads_introspected_names_with_hash_suffix_stripped() { std::env::remove_var("_AM_ALIASES"); } + +// ═══════════════════════════════════════════════════════════════════════ +// Sync snapshots — cover behaviors that snapshot_hook_* and +// snapshot_reload_* used to exercise, now through the Precedence Engine. +// ═══════════════════════════════════════════════════════════════════════ + +#[test] +fn snapshot_sync_fish_fresh_load_project_only() { + use amoxide::precedence::{render_diff, Precedence}; + let project = aliases(&[("b", "cargo build"), ("t", "cargo test")]); + let shell = Shell::Fish.as_shell(&Default::default(), Default::default(), Default::default()); + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .resolve(); + let output = render_diff(&diff, shell.as_ref()); + insta::assert_snapshot!(output); +} + +#[test] +fn snapshot_sync_bash_fresh_load_project_only() { + use amoxide::precedence::{render_diff, Precedence}; + let project = aliases(&[("b", "cargo build")]); + let shell = Shell::Bash.as_shell(&Default::default(), Default::default(), Default::default()); + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .resolve(); + let output = render_diff(&diff, shell.as_ref()); + insta::assert_snapshot!(output); +} + +#[test] +fn snapshot_sync_zsh_fresh_load_project_only() { + use amoxide::precedence::{render_diff, Precedence}; + let project = aliases(&[("b", "cargo build")]); + let shell = Shell::Zsh.as_shell(&Default::default(), Default::default(), Default::default()); + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .resolve(); + let output = render_diff(&diff, shell.as_ref()); + insta::assert_snapshot!(output); +} + +#[test] +fn snapshot_sync_powershell_fresh_load_project_only() { + use amoxide::precedence::{render_diff, Precedence}; + let project = aliases(&[("b", "cargo build")]); + let shell = Shell::Powershell.as_shell(&Default::default(), Default::default(), Default::default()); + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .resolve(); + let output = render_diff(&diff, shell.as_ref()); + insta::assert_snapshot!(output); +} + +#[test] +fn snapshot_sync_fish_transition_to_new_project() { + use amoxide::precedence::{render_diff, Precedence}; + let project = aliases(&[("new1", "echo new")]); + let prev_hash = amoxide::trust::compute_short_hash(b"echo old"); + let prev = format!("old1|{prev_hash}"); + let shell = Shell::Fish.as_shell(&Default::default(), Default::default(), Default::default()); + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .with_shell_state_from_env(Some(&prev), None) + .resolve(); + let output = render_diff(&diff, shell.as_ref()); + insta::assert_snapshot!(output); +} + +#[test] +fn snapshot_sync_fish_leaving_project_with_shadow_restoration() { + use amoxide::precedence::{render_diff, Precedence}; + // Previously the project shadowed a profile alias `t`. Now we've left the + // project directory — effective `t` reverts to the profile value. The + // stored hash was the project's; new effective hash is the profile's. + // The engine must re-emit the profile `t` (shadow restoration). + let profile = aliases(&[("t", "cargo test"), ("ll", "ls -lha")]); + let project_hash = amoxide::trust::compute_short_hash(b"cargo test --release"); + let ll_hash = amoxide::trust::compute_short_hash(b"ls -lha"); + let prev = format!("t|{project_hash},b|aaa1111,ll|{ll_hash}"); + let shell = Shell::Fish.as_shell(&Default::default(), Default::default(), Default::default()); + let diff = Precedence::new() + .with_profiles(&profile, &SubcommandSet::new()) + .with_shell_state_from_env(Some(&prev), None) + .resolve(); + let output = render_diff(&diff, shell.as_ref()); + insta::assert_snapshot!(output); +} + +#[test] +fn snapshot_sync_fish_incremental_one_alias_updated() { + use amoxide::precedence::{render_diff, Precedence}; + let project = aliases(&[("b", "cargo build --release"), ("t", "cargo test")]); + let old_b_hash = amoxide::trust::compute_short_hash(b"cargo build"); + let t_hash = amoxide::trust::compute_short_hash(b"cargo test"); + let prev = format!("b|{old_b_hash},t|{t_hash}"); + let shell = Shell::Fish.as_shell(&Default::default(), Default::default(), Default::default()); + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .with_shell_state_from_env(Some(&prev), None) + .resolve(); + let output = render_diff(&diff, shell.as_ref()); + insta::assert_snapshot!(output); +} + +#[test] +fn snapshot_sync_bash_subcommand_wrapper_fresh_load() { + use amoxide::precedence::{render_diff, Precedence}; + let mut subs = SubcommandSet::new(); + subs.insert("jj:ab".into(), vec!["abandon".into()]); + let shell = Shell::Bash.as_shell(&Default::default(), Default::default(), Default::default()); + let diff = Precedence::new() + .with_project(&AliasSet::default(), &subs) + .resolve(); + let output = render_diff(&diff, shell.as_ref()); + insta::assert_snapshot!(output); +} diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_simple_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_simple_profile.snap index 9c59ac1b..1a2eb344 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_simple_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_simple_profile.snap @@ -4,40 +4,29 @@ expression: output --- alias gs="git status" alias ll="ls -lha" -export _AM_ALIASES="gs,ll" -unset _AM_PROFILE_ALIASES - +export _AM_ALIASES="gs|22db469,ll|619d266" am() { command am "$@" local am_status=$? if [[ $am_status -ne 0 ]]; then return $am_status; fi case "$1" in - tui|t) eval "$(command am reload bash)"; eval "$(command am hook bash)"; return ;; - esac - case "$1" in - use|u) eval "$(command am reload bash)"; return ;; - esac - case "$1:$2" in - profile:use|p:use|profile:u|p:u|profile:add|p:add|profile:a|p:a|profile:remove|p:remove|profile:r|p:r) eval "$(command am reload bash)" ;; - esac - case "$1" in - add|a|remove|r) - case "$*" in - *\ -l\ *|*\ --local\ *|*\ -l|*\ --local) eval "$(command am hook bash)" ;; - *) eval "$(command am reload bash)" ;; + add|a|remove|r|use|u|trust|tui|t) + eval "$(command am sync bash)" ;; + untrust) + eval "$(command am sync --quiet bash)" ;; + profile|p) + case "$2" in + use|u|add|a|remove|r) eval "$(command am sync bash)" ;; esac ;; - trust) eval "$(command am hook bash)" ;; - untrust) eval "$(command am hook --quiet bash)" ;; esac } - -# am cd hook: track directory changes and reload project aliases +# am cd hook: sync project aliases on directory change __am_hook() { local previous_exit_status=$? if [[ "${__am_prev_dir:-}" != "$PWD" ]]; then __am_prev_dir="$PWD" - eval "$(command am hook bash)" + eval "$(command am sync bash)" fi return $previous_exit_status } @@ -54,7 +43,6 @@ else fi __am_hook - _am() { local i cur prev opts cmd COMPREPLY=() @@ -82,9 +70,6 @@ _am() { am,help) cmd="am__subcmd__help" ;; - am,hook) - cmd="am__subcmd__hook" - ;; am,import) cmd="am__subcmd__import" ;; @@ -97,9 +82,6 @@ _am() { am,profile) cmd="am__subcmd__profile" ;; - am,reload) - cmd="am__subcmd__reload" - ;; am,remove) cmd="am__subcmd__remove" ;; @@ -136,9 +118,6 @@ _am() { am__subcmd__help,help) cmd="am__subcmd__help__subcmd__help" ;; - am__subcmd__help,hook) - cmd="am__subcmd__help__subcmd__hook" - ;; am__subcmd__help,import) cmd="am__subcmd__help__subcmd__import" ;; @@ -151,9 +130,6 @@ _am() { am__subcmd__help,profile) cmd="am__subcmd__help__subcmd__profile" ;; - am__subcmd__help,reload) - cmd="am__subcmd__help__subcmd__reload" - ;; am__subcmd__help,remove) cmd="am__subcmd__help__subcmd__remove" ;; @@ -230,7 +206,7 @@ _am() { case "${cmd}" in am) - opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" + opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -292,7 +268,7 @@ _am() { return 0 ;; am__subcmd__help) - opts="add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" + opts="add remove ls status profile init setup use tui export import share trust untrust sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -347,20 +323,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__hook) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__help__subcmd__import) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then @@ -473,20 +435,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__reload) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__help__subcmd__remove) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then @@ -613,20 +561,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__hook) - opts="-q -h -V --quiet --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__import) opts="-l -g -p -b -y -h -V --local --global --profile --all --base64 --yes --trust --help --version " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then @@ -839,20 +773,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__reload) - opts="-h -V --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__remove) opts="-p -l -g -h -V --profile --local --global --sub --help --version " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_with_kubectl_subcommands.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_with_kubectl_subcommands.snap index 01b049af..3b551157 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_with_kubectl_subcommands.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_with_kubectl_subcommands.snap @@ -32,40 +32,30 @@ kubectl() { *) command kubectl "$@" ;; esac } -export _AM_ALIASES="kubectl" -unset _AM_PROFILE_ALIASES - +export _AM_ALIASES="kubectl|e597aec" +export _AM_SUBCOMMANDS="kubectl:apply:f|ce5fb2d,kubectl:get:po|98de999,kubectl:get:svc|15a309b,kubectl:logs:f|1b79d5f,kubectl:rollout:status|da7ff14" am() { command am "$@" local am_status=$? if [[ $am_status -ne 0 ]]; then return $am_status; fi case "$1" in - tui|t) eval "$(command am reload bash)"; eval "$(command am hook bash)"; return ;; - esac - case "$1" in - use|u) eval "$(command am reload bash)"; return ;; - esac - case "$1:$2" in - profile:use|p:use|profile:u|p:u|profile:add|p:add|profile:a|p:a|profile:remove|p:remove|profile:r|p:r) eval "$(command am reload bash)" ;; - esac - case "$1" in - add|a|remove|r) - case "$*" in - *\ -l\ *|*\ --local\ *|*\ -l|*\ --local) eval "$(command am hook bash)" ;; - *) eval "$(command am reload bash)" ;; + add|a|remove|r|use|u|trust|tui|t) + eval "$(command am sync bash)" ;; + untrust) + eval "$(command am sync --quiet bash)" ;; + profile|p) + case "$2" in + use|u|add|a|remove|r) eval "$(command am sync bash)" ;; esac ;; - trust) eval "$(command am hook bash)" ;; - untrust) eval "$(command am hook --quiet bash)" ;; esac } - -# am cd hook: track directory changes and reload project aliases +# am cd hook: sync project aliases on directory change __am_hook() { local previous_exit_status=$? if [[ "${__am_prev_dir:-}" != "$PWD" ]]; then __am_prev_dir="$PWD" - eval "$(command am hook bash)" + eval "$(command am sync bash)" fi return $previous_exit_status } @@ -82,7 +72,6 @@ else fi __am_hook - _am() { local i cur prev opts cmd COMPREPLY=() @@ -110,9 +99,6 @@ _am() { am,help) cmd="am__subcmd__help" ;; - am,hook) - cmd="am__subcmd__hook" - ;; am,import) cmd="am__subcmd__import" ;; @@ -125,9 +111,6 @@ _am() { am,profile) cmd="am__subcmd__profile" ;; - am,reload) - cmd="am__subcmd__reload" - ;; am,remove) cmd="am__subcmd__remove" ;; @@ -164,9 +147,6 @@ _am() { am__subcmd__help,help) cmd="am__subcmd__help__subcmd__help" ;; - am__subcmd__help,hook) - cmd="am__subcmd__help__subcmd__hook" - ;; am__subcmd__help,import) cmd="am__subcmd__help__subcmd__import" ;; @@ -179,9 +159,6 @@ _am() { am__subcmd__help,profile) cmd="am__subcmd__help__subcmd__profile" ;; - am__subcmd__help,reload) - cmd="am__subcmd__help__subcmd__reload" - ;; am__subcmd__help,remove) cmd="am__subcmd__help__subcmd__remove" ;; @@ -258,7 +235,7 @@ _am() { case "${cmd}" in am) - opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" + opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -320,7 +297,7 @@ _am() { return 0 ;; am__subcmd__help) - opts="add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" + opts="add remove ls status profile init setup use tui export import share trust untrust sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -375,20 +352,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__hook) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__help__subcmd__import) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then @@ -501,20 +464,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__reload) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__help__subcmd__remove) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then @@ -641,20 +590,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__hook) - opts="-q -h -V --quiet --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__import) opts="-l -g -p -b -y -h -V --local --global --profile --all --base64 --yes --trust --help --version " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then @@ -867,20 +802,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__reload) - opts="-h -V --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__remove) opts="-p -l -g -h -V --profile --local --global --sub --help --version " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap index eeff4de3..fcfee91a 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap @@ -5,50 +5,30 @@ expression: output alias ct "cargo test" alias gs "git status" alias ll "ls -lha" -set -gx _AM_ALIASES "ct,gs,ll" -set -e _AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations +set -gx _AM_ALIASES "ct|ab61de4,gs|22db469,ll|619d266" +# am wrapper: sync after mutations function am --wraps=am command am $argv set -l am_status $status if test $am_status -ne 0 return $am_status end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload fish | source - command am hook fish | source - return - end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload fish | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload fish | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook fish | source - else - # profile/global alias change → reload - command am reload fish | source - end - else if test "$argv[1]" = trust - command am hook fish | source - else if test "$argv[1]" = untrust - command am hook --quiet fish | source + switch "$argv[1]" + case add a remove r use u trust tui t + command am sync fish | source + case untrust + command am sync --quiet fish | source + case profile p + switch "$argv[2]" + case use u add a remove r + command am sync fish | source + end end end # am cd hook function __am_hook --on-variable PWD - am hook fish | source + am sync fish | source end __am_hook @@ -94,8 +74,6 @@ complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases fr complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r @@ -180,32 +158,25 @@ complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print v complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap index 287bcf5f..8716c208 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap @@ -2,56 +2,36 @@ source: crates/am/tests/snapshots.rs expression: output --- -alias ll "ls -lha" alias cm "git commit -sm" function cmf cm feat: $argv end alias gs "git status" -set -gx _AM_ALIASES "cm,cmf,gs,ll" -set -e _AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations +alias ll "ls -lha" +set -gx _AM_ALIASES "cm|bed528f,cmf|9c99949,gs|22db469,ll|619d266" +# am wrapper: sync after mutations function am --wraps=am command am $argv set -l am_status $status if test $am_status -ne 0 return $am_status end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload fish | source - command am hook fish | source - return - end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload fish | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload fish | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook fish | source - else - # profile/global alias change → reload - command am reload fish | source - end - else if test "$argv[1]" = trust - command am hook fish | source - else if test "$argv[1]" = untrust - command am hook --quiet fish | source + switch "$argv[1]" + case add a remove r use u trust tui t + command am sync fish | source + case untrust + command am sync --quiet fish | source + case profile p + switch "$argv[2]" + case use u add a remove r + command am sync fish | source + end end end # am cd hook function __am_hook --on-variable PWD - am hook fish | source + am sync fish | source end __am_hook @@ -97,8 +77,6 @@ complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases fr complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r @@ -183,32 +161,25 @@ complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print v complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap index 24cd9bf5..63c351f6 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap @@ -7,50 +7,30 @@ function cmf cm feat: $argv end alias gs "git status" -set -gx _AM_ALIASES "cm,cmf,gs" -set -e _AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations +set -gx _AM_ALIASES "cm|bed528f,cmf|9c99949,gs|22db469" +# am wrapper: sync after mutations function am --wraps=am command am $argv set -l am_status $status if test $am_status -ne 0 return $am_status end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload fish | source - command am hook fish | source - return - end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload fish | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload fish | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook fish | source - else - # profile/global alias change → reload - command am reload fish | source - end - else if test "$argv[1]" = trust - command am hook fish | source - else if test "$argv[1]" = untrust - command am hook --quiet fish | source + switch "$argv[1]" + case add a remove r use u trust tui t + command am sync fish | source + case untrust + command am sync --quiet fish | source + case profile p + switch "$argv[2]" + case use u add a remove r + command am sync fish | source + end end end # am cd hook function __am_hook --on-variable PWD - am hook fish | source + am sync fish | source end __am_hook @@ -96,8 +76,6 @@ complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases fr complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r @@ -182,32 +160,25 @@ complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print v complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap index c220aee8..b38ff72b 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap @@ -4,50 +4,30 @@ expression: output --- alias gs "git status" alias ll "ls -lha" -set -gx _AM_ALIASES "gs,ll" -set -e _AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations +set -gx _AM_ALIASES "gs|22db469,ll|619d266" +# am wrapper: sync after mutations function am --wraps=am command am $argv set -l am_status $status if test $am_status -ne 0 return $am_status end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload fish | source - command am hook fish | source - return - end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload fish | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload fish | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook fish | source - else - # profile/global alias change → reload - command am reload fish | source - end - else if test "$argv[1]" = trust - command am hook fish | source - else if test "$argv[1]" = untrust - command am hook --quiet fish | source + switch "$argv[1]" + case add a remove r use u trust tui t + command am sync fish | source + case untrust + command am sync --quiet fish | source + case profile p + switch "$argv[2]" + case use u add a remove r + command am sync fish | source + end end end # am cd hook function __am_hook --on-variable PWD - am hook fish | source + am sync fish | source end __am_hook @@ -93,8 +73,6 @@ complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases fr complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r @@ -179,32 +157,25 @@ complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print v complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap index 34c9466e..7bb36783 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap @@ -2,52 +2,32 @@ source: crates/am/tests/snapshots.rs expression: output --- -alias ll "ls -lha" alias ct "cargo test" -set -gx _AM_ALIASES "ct,ll" -set -e _AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations +alias ll "ls -lha" +set -gx _AM_ALIASES "ct|ab61de4,ll|619d266" +# am wrapper: sync after mutations function am --wraps=am command am $argv set -l am_status $status if test $am_status -ne 0 return $am_status end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload fish | source - command am hook fish | source - return - end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload fish | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload fish | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook fish | source - else - # profile/global alias change → reload - command am reload fish | source - end - else if test "$argv[1]" = trust - command am hook fish | source - else if test "$argv[1]" = untrust - command am hook --quiet fish | source + switch "$argv[1]" + case add a remove r use u trust tui t + command am sync fish | source + case untrust + command am sync --quiet fish | source + case profile p + switch "$argv[2]" + case use u add a remove r + command am sync fish | source + end end end # am cd hook function __am_hook --on-variable PWD - am hook fish | source + am sync fish | source end __am_hook @@ -93,8 +73,6 @@ complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases fr complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r @@ -179,32 +157,25 @@ complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print v complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap index 0a497b3d..4a4a26cf 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap @@ -13,50 +13,31 @@ function jj --wraps=jj command jj $argv end end -set -gx _AM_ALIASES "gs,jj" -set -e _AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations +set -gx _AM_ALIASES "gs|22db469,jj|d8877a2" +set -gx _AM_SUBCOMMANDS "jj:ab|8296f9c,jj:new|22681a8" +# am wrapper: sync after mutations function am --wraps=am command am $argv set -l am_status $status if test $am_status -ne 0 return $am_status end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload fish | source - command am hook fish | source - return - end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload fish | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload fish | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook fish | source - else - # profile/global alias change → reload - command am reload fish | source - end - else if test "$argv[1]" = trust - command am hook fish | source - else if test "$argv[1]" = untrust - command am hook --quiet fish | source + switch "$argv[1]" + case add a remove r use u trust tui t + command am sync fish | source + case untrust + command am sync --quiet fish | source + case profile p + switch "$argv[2]" + case use u add a remove r + command am sync fish | source + end end end # am cd hook function __am_hook --on-variable PWD - am hook fish | source + am sync fish | source end __am_hook @@ -102,8 +83,6 @@ complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases fr complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r @@ -188,32 +167,25 @@ complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print v complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_powershell_simple_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_powershell_simple_profile.snap index da3bd6df..5a555787 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_powershell_simple_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_powershell_simple_profile.snap @@ -4,65 +4,42 @@ expression: output --- function global:gs { git status @args } function global:ll { ls -lha @args } -$env:_AM_ALIASES = "gs,ll" -Remove-Item -ErrorAction SilentlyContinue Env:_AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations +$env:_AM_ALIASES = "gs|22db469,ll|619d266" +# am wrapper: sync after mutations function am { $amBin = (Get-Command -CommandType Application am | Select-Object -First 1).Source & $amBin @args if ($LASTEXITCODE -ne 0) { return } - # tui — always reload - if ($args.Count -ge 1 -and $args[0] -in 'tui', 't') { - $out = (& $amBin reload powershell) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - $out = (& $amBin hook powershell) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - return - } - # top-level use — reload - if ($args.Count -ge 1 -and $args[0] -in 'use', 'u') { - $out = (& $amBin reload powershell) -join "`r`n" + if ($args.Count -lt 1) { return } + $first = $args[0] + $second = if ($args.Count -ge 2) { $args[1] } else { $null } + + $runSync = { + $out = (& $amBin sync powershell) -join "`r`n" if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - return - } - # profile mutation — reload - if ($args.Count -ge 1 -and $args[0] -in 'profile', 'p') { - if ($args.Count -ge 2 -and $args[1] -in 'use', 'u', 'add', 'a', 'remove', 'r') { - $out = (& $amBin reload powershell) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - } - } - # alias mutation — reload - elseif ($args.Count -ge 1 -and $args[0] -in 'add', 'a', 'remove', 'r') { - if ($args -contains '-l' -or $args -contains '--local') { - $out = (& $amBin hook powershell) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - } else { - $out = (& $amBin reload powershell) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - } } - # trust/untrust — reload project aliases - elseif ($args.Count -ge 1 -and $args[0] -eq 'trust') { - $out = (& $amBin hook powershell) -join "`r`n" + $runSyncQuiet = { + $out = (& $amBin sync --quiet powershell) -join "`r`n" if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } } - elseif ($args.Count -ge 1 -and $args[0] -eq 'untrust') { - $out = (& $amBin hook --quiet powershell) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } + + if ($first -in 'add', 'a', 'remove', 'r', 'use', 'u', 'trust', 'tui', 't') { + & $runSync + } elseif ($first -eq 'untrust') { + & $runSyncQuiet + } elseif ($first -in 'profile', 'p') { + if ($second -in 'use', 'u', 'add', 'a', 'remove', 'r') { & $runSync } } } - -# am cd hook: track directory changes and reload project aliases +# am cd hook: sync project aliases on directory change $env:__AM_LAST_DIR = $PWD.Path $__am_original_prompt = $function:prompt function global:prompt { if ($PWD.Path -ne $env:__AM_LAST_DIR) { $env:__AM_LAST_DIR = $PWD.Path $amBin = (Get-Command -CommandType Application am | Select-Object -First 1).Source - $hookCode = (& $amBin hook powershell) -join "`r`n" + $hookCode = (& $amBin sync powershell) -join "`r`n" if ($hookCode) { Invoke-Command -ScriptBlock ([scriptblock]::Create($hookCode)) -NoNewScope } } if ($__am_original_prompt) { & $__am_original_prompt } else { "PS $($PWD.Path)> " } @@ -70,7 +47,6 @@ function global:prompt { - Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) @@ -108,8 +84,6 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [System.Management.Automation.CompletionResult]::new('share', 'share', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Generate a share command for posting aliases to a pastebin service') [System.Management.Automation.CompletionResult]::new('trust', 'trust', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Review and trust the project .aliases file in the current directory') [System.Management.Automation.CompletionResult]::new('untrust', 'untrust', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Remove trust for the project .aliases file in the current directory') - [System.Management.Automation.CompletionResult]::new('hook', 'hook', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Internal: called by the cd hook to load/unload project aliases') - [System.Management.Automation.CompletionResult]::new('reload', 'reload', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Internal: called by the am wrapper to reload profile aliases after switching') [System.Management.Automation.CompletionResult]::new('sync', 'sync', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)') [System.Management.Automation.CompletionResult]::new('help', 'help', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)') break @@ -331,22 +305,6 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [System.Management.Automation.CompletionResult]::new('--version', '--version', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print version') break } - 'am;hook' { - [System.Management.Automation.CompletionResult]::new('-q', '-q', [System.Management.Automation.CompletionResultType]::ParameterName, 'Suppress info and warning messages (still unloads/loads aliases)') - [System.Management.Automation.CompletionResult]::new('--quiet', '--quiet', [System.Management.Automation.CompletionResultType]::ParameterName, 'Suppress info and warning messages (still unloads/loads aliases)') - [System.Management.Automation.CompletionResult]::new('-h', '-h', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print help') - [System.Management.Automation.CompletionResult]::new('--help', '--help', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print help') - [System.Management.Automation.CompletionResult]::new('-V', '-V ', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print version') - [System.Management.Automation.CompletionResult]::new('--version', '--version', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print version') - break - } - 'am;reload' { - [System.Management.Automation.CompletionResult]::new('-h', '-h', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print help') - [System.Management.Automation.CompletionResult]::new('--help', '--help', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print help') - [System.Management.Automation.CompletionResult]::new('-V', '-V ', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print version') - [System.Management.Automation.CompletionResult]::new('--version', '--version', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print version') - break - } 'am;sync' { [System.Management.Automation.CompletionResult]::new('-q', '-q', [System.Management.Automation.CompletionResultType]::ParameterName, 'Suppress info and warning messages (still unloads/loads aliases)') [System.Management.Automation.CompletionResult]::new('--quiet', '--quiet', [System.Management.Automation.CompletionResultType]::ParameterName, 'Suppress info and warning messages (still unloads/loads aliases)') @@ -371,8 +329,6 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [System.Management.Automation.CompletionResult]::new('share', 'share', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Generate a share command for posting aliases to a pastebin service') [System.Management.Automation.CompletionResult]::new('trust', 'trust', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Review and trust the project .aliases file in the current directory') [System.Management.Automation.CompletionResult]::new('untrust', 'untrust', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Remove trust for the project .aliases file in the current directory') - [System.Management.Automation.CompletionResult]::new('hook', 'hook', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Internal: called by the cd hook to load/unload project aliases') - [System.Management.Automation.CompletionResult]::new('reload', 'reload', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Internal: called by the am wrapper to reload profile aliases after switching') [System.Management.Automation.CompletionResult]::new('sync', 'sync', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)') [System.Management.Automation.CompletionResult]::new('help', 'help', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)') break @@ -435,12 +391,6 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { 'am;help;untrust' { break } - 'am;help;hook' { - break - } - 'am;help;reload' { - break - } 'am;help;sync' { break } diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_zsh_simple_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_zsh_simple_profile.snap index a7077c05..85d28a31 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_zsh_simple_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_zsh_simple_profile.snap @@ -4,35 +4,25 @@ expression: output --- alias gs="git status" alias ll="ls -lha" -export _AM_ALIASES="gs,ll" -unset _AM_PROFILE_ALIASES - +export _AM_ALIASES="gs|22db469,ll|619d266" am() { command am "$@" local am_status=$? if [[ $am_status -ne 0 ]]; then return $am_status; fi case "$1" in - tui|t) eval "$(command am reload zsh)"; eval "$(command am hook zsh)"; return ;; - esac - case "$1" in - use|u) eval "$(command am reload zsh)"; return ;; - esac - case "$1:$2" in - profile:use|p:use|profile:u|p:u|profile:add|p:add|profile:a|p:a|profile:remove|p:remove|profile:r|p:r) eval "$(command am reload zsh)" ;; - esac - case "$1" in - add|a|remove|r) - case "$*" in - *\ -l\ *|*\ --local\ *|*\ -l|*\ --local) eval "$(command am hook zsh)" ;; - *) eval "$(command am reload zsh)" ;; + add|a|remove|r|use|u|trust|tui|t) + eval "$(command am sync zsh)" ;; + untrust) + eval "$(command am sync --quiet zsh)" ;; + profile|p) + case "$2" in + use|u|add|a|remove|r) eval "$(command am sync zsh)" ;; esac ;; - trust) eval "$(command am hook zsh)" ;; - untrust) eval "$(command am hook --quiet zsh)" ;; esac } # am cd hook -__am_hook() { eval "$(am hook zsh)"; } +__am_hook() { eval "$(am sync zsh)"; } chpwd_functions+=(__am_hook) __am_hook @@ -331,26 +321,6 @@ _arguments "${_arguments_options[@]}" : \ '--version[Print version]' \ && ret=0 ;; -(hook) -_arguments "${_arguments_options[@]}" : \ -'-q[Suppress info and warning messages (still unloads/loads aliases)]' \ -'--quiet[Suppress info and warning messages (still unloads/loads aliases)]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'-V[Print version]' \ -'--version[Print version]' \ -':shell:(bash brush fish powershell zsh)' \ -&& ret=0 -;; -(reload) -_arguments "${_arguments_options[@]}" : \ -'-h[Print help]' \ -'--help[Print help]' \ -'-V[Print version]' \ -'--version[Print version]' \ -':shell:(bash brush fish powershell zsh)' \ -&& ret=0 -;; (sync) _arguments "${_arguments_options[@]}" : \ '-q[Suppress info and warning messages (still unloads/loads aliases)]' \ @@ -458,14 +428,6 @@ _arguments "${_arguments_options[@]}" : \ _arguments "${_arguments_options[@]}" : \ && ret=0 ;; -(hook) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(reload) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; (sync) _arguments "${_arguments_options[@]}" : \ && ret=0 @@ -500,8 +462,6 @@ _am_commands() { 'share:Generate a share command for posting aliases to a pastebin service' \ 'trust:Review and trust the project .aliases file in the current directory' \ 'untrust:Remove trust for the project .aliases file in the current directory' \ -'hook:Internal\: called by the cd hook to load/unload project aliases' \ -'reload:Internal\: called by the am wrapper to reload profile aliases after switching' \ 'sync:Internal\: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' \ 'help:Print this message or the help of the given subcommand(s)' \ ) @@ -534,8 +494,6 @@ _am__subcmd__help_commands() { 'share:Generate a share command for posting aliases to a pastebin service' \ 'trust:Review and trust the project .aliases file in the current directory' \ 'untrust:Remove trust for the project .aliases file in the current directory' \ -'hook:Internal\: called by the cd hook to load/unload project aliases' \ -'reload:Internal\: called by the am wrapper to reload profile aliases after switching' \ 'sync:Internal\: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' \ 'help:Print this message or the help of the given subcommand(s)' \ ) @@ -556,11 +514,6 @@ _am__subcmd__help__subcmd__help_commands() { local commands; commands=() _describe -t commands 'am help help commands' commands "$@" } -(( $+functions[_am__subcmd__help__subcmd__hook_commands] )) || -_am__subcmd__help__subcmd__hook_commands() { - local commands; commands=() - _describe -t commands 'am help hook commands' commands "$@" -} (( $+functions[_am__subcmd__help__subcmd__import_commands] )) || _am__subcmd__help__subcmd__import_commands() { local commands; commands=() @@ -606,11 +559,6 @@ _am__subcmd__help__subcmd__profile__subcmd__use_commands() { local commands; commands=() _describe -t commands 'am help profile use commands' commands "$@" } -(( $+functions[_am__subcmd__help__subcmd__reload_commands] )) || -_am__subcmd__help__subcmd__reload_commands() { - local commands; commands=() - _describe -t commands 'am help reload commands' commands "$@" -} (( $+functions[_am__subcmd__help__subcmd__remove_commands] )) || _am__subcmd__help__subcmd__remove_commands() { local commands; commands=() @@ -656,11 +604,6 @@ _am__subcmd__help__subcmd__use_commands() { local commands; commands=() _describe -t commands 'am help use commands' commands "$@" } -(( $+functions[_am__subcmd__hook_commands] )) || -_am__subcmd__hook_commands() { - local commands; commands=() - _describe -t commands 'am hook commands' commands "$@" -} (( $+functions[_am__subcmd__import_commands] )) || _am__subcmd__import_commands() { local commands; commands=() @@ -743,11 +686,6 @@ _am__subcmd__profile__subcmd__use_commands() { local commands; commands=() _describe -t commands 'am profile use commands' commands "$@" } -(( $+functions[_am__subcmd__reload_commands] )) || -_am__subcmd__reload_commands() { - local commands; commands=() - _describe -t commands 'am reload commands' commands "$@" -} (( $+functions[_am__subcmd__remove_commands] )) || _am__subcmd__remove_commands() { local commands; commands=() diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_bash_fresh_load_project_only.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_bash_fresh_load_project_only.snap new file mode 100644 index 00000000..19ae4226 --- /dev/null +++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_bash_fresh_load_project_only.snap @@ -0,0 +1,6 @@ +--- +source: crates/am/tests/snapshots.rs +expression: output +--- +alias b="cargo build" +export _AM_ALIASES="b|b58de66" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_bash_subcommand_wrapper_fresh_load.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_bash_subcommand_wrapper_fresh_load.snap new file mode 100644 index 00000000..50a78ca5 --- /dev/null +++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_bash_subcommand_wrapper_fresh_load.snap @@ -0,0 +1,12 @@ +--- +source: crates/am/tests/snapshots.rs +expression: output +--- +jj() { + case "$1" in + ab) shift; command jj abandon "$@" ;; + *) command jj "$@" ;; + esac +} +export _AM_ALIASES="jj|33f7a66" +export _AM_SUBCOMMANDS="jj:ab|8296f9c" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_fresh_load_project_only.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_fresh_load_project_only.snap new file mode 100644 index 00000000..78754585 --- /dev/null +++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_fresh_load_project_only.snap @@ -0,0 +1,7 @@ +--- +source: crates/am/tests/snapshots.rs +expression: output +--- +alias b "cargo build" +alias t "cargo test" +set -gx _AM_ALIASES "b|b58de66,t|ab61de4" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_incremental_one_alias_updated.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_incremental_one_alias_updated.snap new file mode 100644 index 00000000..ded8322b --- /dev/null +++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_incremental_one_alias_updated.snap @@ -0,0 +1,7 @@ +--- +source: crates/am/tests/snapshots.rs +expression: output +--- +functions -e b +alias b "cargo build --release" +set -gx _AM_ALIASES "b|0dc3caa,t|ab61de4" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_leaving_project_with_shadow_restoration.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_leaving_project_with_shadow_restoration.snap new file mode 100644 index 00000000..e1c67de4 --- /dev/null +++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_leaving_project_with_shadow_restoration.snap @@ -0,0 +1,8 @@ +--- +source: crates/am/tests/snapshots.rs +expression: output +--- +functions -e b +functions -e t +alias t "cargo test" +set -gx _AM_ALIASES "t|ab61de4,ll|619d266" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_transition_to_new_project.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_transition_to_new_project.snap new file mode 100644 index 00000000..811347e5 --- /dev/null +++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_transition_to_new_project.snap @@ -0,0 +1,7 @@ +--- +source: crates/am/tests/snapshots.rs +expression: output +--- +functions -e old1 +alias new1 "echo new" +set -gx _AM_ALIASES "new1|4d1fcee" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_powershell_fresh_load_project_only.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_powershell_fresh_load_project_only.snap new file mode 100644 index 00000000..14d39ffa --- /dev/null +++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_powershell_fresh_load_project_only.snap @@ -0,0 +1,6 @@ +--- +source: crates/am/tests/snapshots.rs +expression: output +--- +function global:b { cargo build @args } +$env:_AM_ALIASES = "b|b58de66" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_zsh_fresh_load_project_only.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_zsh_fresh_load_project_only.snap new file mode 100644 index 00000000..19ae4226 --- /dev/null +++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_zsh_fresh_load_project_only.snap @@ -0,0 +1,6 @@ +--- +source: crates/am/tests/snapshots.rs +expression: output +--- +alias b="cargo build" +export _AM_ALIASES="b|b58de66" From 577a05c57f9de01fd77c61f641629db10f8942d2 Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 22:06:43 +0200 Subject: [PATCH 19/38] chore: fix fmt and clippy lints - move AliasName import into test module (non-test use was dead) - iterate on .keys() instead of (k, _) pattern in precedence diff loops - apply rustfmt to snapshots.rs --- crates/am/src/bin/am.rs | 5 +- crates/am/src/init.rs | 1 - crates/am/src/precedence.rs | 104 ++++++++++++++++++++++++----------- crates/am/src/update.rs | 24 ++++---- crates/am/tests/snapshots.rs | 7 ++- 5 files changed, 91 insertions(+), 50 deletions(-) diff --git a/crates/am/src/bin/am.rs b/crates/am/src/bin/am.rs index a13da576..d86500d4 100644 --- a/crates/am/src/bin/am.rs +++ b/crates/am/src/bin/am.rs @@ -46,10 +46,7 @@ fn main() -> anyhow::Result<()> { let mut model = AppModel::default(); // Don't log for commands whose stdout is eval'd by the shell - if !matches!( - &cli.command, - Commands::Init { .. } | Commands::Sync { .. } - ) { + if !matches!(&cli.command, Commands::Init { .. } | Commands::Sync { .. }) { setup_logging(); } diff --git a/crates/am/src/init.rs b/crates/am/src/init.rs index 29508d6c..48e54c2a 100644 --- a/crates/am/src/init.rs +++ b/crates/am/src/init.rs @@ -437,5 +437,4 @@ mod tests { "init must use name|hash format in _AM_ALIASES, got: {output}" ); } - } diff --git a/crates/am/src/precedence.rs b/crates/am/src/precedence.rs index ef554963..d06f12fb 100644 --- a/crates/am/src/precedence.rs +++ b/crates/am/src/precedence.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, BTreeSet, HashSet}; -use crate::alias::{AliasName, AliasSet, TomlAlias}; +use crate::alias::{AliasSet, TomlAlias}; use crate::subcommand::{SubcommandEntry, SubcommandSet}; #[derive(Debug, Clone, PartialEq)] @@ -98,7 +98,11 @@ impl Precedence { /// with project > profile > global precedence. fn merged_aliases(&self) -> BTreeMap { let mut out = BTreeMap::new(); - for layer in [&self.global_aliases, &self.profile_aliases, &self.project_aliases] { + for layer in [ + &self.global_aliases, + &self.profile_aliases, + &self.project_aliases, + ] { for (name, alias) in layer.iter() { out.insert(name.as_ref().to_string(), alias.clone()); } @@ -110,7 +114,11 @@ impl Precedence { /// with project > profile > global precedence. fn merged_subcommands(&self) -> SubcommandSet { let mut out = SubcommandSet::new(); - for layer in [&self.global_subcommands, &self.profile_subcommands, &self.project_subcommands] { + for layer in [ + &self.global_subcommands, + &self.profile_subcommands, + &self.project_subcommands, + ] { for (k, v) in layer { out.insert(k.clone(), v.clone()); } @@ -207,9 +215,7 @@ impl Precedence { // Subcommand wrappers (one entry per program). for (program, entries) in &subcmd_groups { - let base_cmd = merged_aliases - .get(program) - .map(|a| a.command().to_string()); + let base_cmd = merged_aliases.get(program).map(|a| a.command().to_string()); let hash = Self::subcmd_program_hash(program, &merged_subcommands); effective.insert( program.clone(), @@ -244,7 +250,7 @@ impl Precedence { let mut diff = PrecedenceDiff::default(); // --- Regular + wrapper diff against shell_alias_state --- - for (name, _) in &self.shell_alias_state { + for name in self.shell_alias_state.keys() { if !effective.contains_key(name) { diff.removed.push(name.clone()); } @@ -264,7 +270,7 @@ impl Precedence { // The program-level wrapper already lives in `effective`/`diff` above. // Here we additionally track individual keys so they appear in // `_AM_SUBCOMMANDS` with fine-grained hashes. - for (name, _) in &self.shell_subcmd_state { + for name in self.shell_subcmd_state.keys() { // A program-level entry (no ':') is tracked in shell_alias_state, not here. if !name.contains(':') { continue; @@ -324,7 +330,11 @@ pub fn render_diff(diff: &PrecedenceDiff, shell: &dyn ShellAdapter) -> String { EntryKind::Alias(alias) => { lines.push(shell.alias(&alias.as_entry(&entry.name))); } - EntryKind::SubcommandWrapper { program, entries, base_cmd } => { + EntryKind::SubcommandWrapper { + program, + entries, + base_cmd, + } => { let cmd = base_cmd .clone() .unwrap_or_else(|| format!("command {program}")); @@ -337,7 +347,12 @@ pub fn render_diff(diff: &PrecedenceDiff, shell: &dyn ShellAdapter) -> String { // 3. Update tracking env vars let mut alias_pairs = Vec::new(); let mut sub_pairs = Vec::new(); - for e in diff.added.iter().chain(diff.changed.iter()).chain(diff.unchanged.iter()) { + for e in diff + .added + .iter() + .chain(diff.changed.iter()) + .chain(diff.unchanged.iter()) + { let pair = format!("{}|{}", e.name, e.hash); match &e.kind { EntryKind::SubcommandKey { .. } => sub_pairs.push(pair), @@ -358,6 +373,7 @@ pub fn render_diff(diff: &PrecedenceDiff, shell: &dyn ShellAdapter) -> String { #[cfg(test)] mod tests { use super::*; + use crate::alias::AliasName; #[test] fn empty_inputs_produce_empty_diff() { @@ -438,8 +454,7 @@ mod tests { #[test] fn parse_shell_state_new_format() { - let p = Precedence::new() - .with_shell_state_from_env(Some("b|abc1234,t|def5678"), None); + let p = Precedence::new().with_shell_state_from_env(Some("b|abc1234,t|def5678"), None); let aliases = p.shell_alias_state_for_test(); assert_eq!(aliases.get("b"), Some(&Some("abc1234".into()))); assert_eq!(aliases.get("t"), Some(&Some("def5678".into()))); @@ -463,8 +478,7 @@ mod tests { #[test] fn parse_shell_state_mixed_format() { - let p = Precedence::new() - .with_shell_state_from_env(Some("b|abc1234,t,gs|fed9876"), None); + let p = Precedence::new().with_shell_state_from_env(Some("b|abc1234,t,gs|fed9876"), None); let aliases = p.shell_alias_state_for_test(); assert_eq!(aliases.get("b"), Some(&Some("abc1234".into()))); assert_eq!(aliases.get("t"), Some(&None)); @@ -503,10 +517,7 @@ mod tests { .with_project(&project, &SubcommandSet::new()) .resolve(); let added_names: BTreeSet<_> = diff.added.iter().map(|e| e.name.as_str()).collect(); - assert_eq!( - added_names, - BTreeSet::from(["ll", "gs", "b"]), - ); + assert_eq!(added_names, BTreeSet::from(["ll", "gs", "b"]),); assert!(diff.changed.is_empty()); assert!(diff.removed.is_empty()); assert!(diff.unchanged.is_empty()); @@ -574,7 +585,11 @@ mod tests { .with_profiles(&profile, &SubcommandSet::new()) .with_shell_state_from_env(Some(&prev), None) .resolve(); - assert_eq!(diff.changed.len(), 1, "shadow restoration must emit a reload"); + assert_eq!( + diff.changed.len(), + 1, + "shadow restoration must emit a reload" + ); assert_eq!(cmd_of(&diff.changed[0]), "profile-t"); assert!(diff.removed.is_empty()); } @@ -595,7 +610,11 @@ mod tests { .resolve(); let wrapper = find(&diff.added, "jj").expect("expected jj wrapper in added"); match &wrapper.kind { - EntryKind::SubcommandWrapper { program, entries, base_cmd } => { + EntryKind::SubcommandWrapper { + program, + entries, + base_cmd, + } => { assert_eq!(program, "jj"); assert_eq!(entries.len(), 1); assert_eq!(entries[0].short_subcommands, vec!["ab"]); @@ -613,9 +632,7 @@ mod tests { fn resolve_subcommand_base_cmd_from_regular_alias_same_name() { let aliases = aset(&[("jj", "just-a-joke")]); let subs = subset(&[("jj:ab", &["abandon"])]); - let diff = Precedence::new() - .with_project(&aliases, &subs) - .resolve(); + let diff = Precedence::new().with_project(&aliases, &subs).resolve(); let wrapper = find(&diff.added, "jj").unwrap(); match &wrapper.kind { EntryKind::SubcommandWrapper { base_cmd, .. } => { @@ -679,7 +696,11 @@ mod tests { assert!(diff.added.is_empty(), "got added: {:?}", diff.added); assert!(diff.changed.is_empty(), "got changed: {:?}", diff.changed); assert!(diff.removed.is_empty(), "got removed: {:?}", diff.removed); - assert_eq!(diff.unchanged.len(), 2, "jj wrapper + jj:ab key both unchanged"); + assert_eq!( + diff.unchanged.len(), + 2, + "jj wrapper + jj:ab key both unchanged" + ); } #[test] @@ -704,7 +725,10 @@ mod tests { let p = Precedence::new() .with_shell_state_from_env(Some("b|abc1234"), None) .with_shell_state_from_introspection(&fns, &HashSet::new()); - assert_eq!(p.shell_alias_state_for_test().get("b"), Some(&Some("abc1234".into()))); + assert_eq!( + p.shell_alias_state_for_test().get("b"), + Some(&Some("abc1234".into())) + ); } #[test] @@ -722,10 +746,19 @@ mod tests { .with_project(&AliasSet::default(), &subs_after) .with_shell_state_from_env(Some(&prev_aliases), Some(&prev_subs)) .resolve(); - assert!(find(&diff.changed, "jj").is_some(), "wrapper must be regenerated"); - assert!(find(&diff.added, "jj:bl").is_some(), "new key must be added"); + assert!( + find(&diff.changed, "jj").is_some(), + "wrapper must be regenerated" + ); + assert!( + find(&diff.added, "jj:bl").is_some(), + "new key must be added" + ); // jj:ab itself unchanged - assert!(find(&diff.unchanged, "jj:ab").is_some(), "jj:ab entry itself is unchanged"); + assert!( + find(&diff.unchanged, "jj:ab").is_some(), + "jj:ab entry itself is unchanged" + ); } use crate::config::ShellsTomlConfig; @@ -744,9 +777,18 @@ mod tests { .resolve(); let out = crate::precedence::render_diff(&diff, shell.as_ref()); - assert!(out.contains("functions -e gone"), "gone must be unloaded: {out}"); - assert!(out.contains("functions -e b"), "changed b must be unloaded: {out}"); - assert!(out.contains("alias b \"make build\""), "b must be reloaded: {out}"); + assert!( + out.contains("functions -e gone"), + "gone must be unloaded: {out}" + ); + assert!( + out.contains("functions -e b"), + "changed b must be unloaded: {out}" + ); + assert!( + out.contains("alias b \"make build\""), + "b must be reloaded: {out}" + ); // env-var update must be the last section let env_pos = out.find("_AM_ALIASES").expect("env update missing"); let alias_pos = out.find("alias b").unwrap(); diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs index 08bf77c2..85b6f020 100644 --- a/crates/am/src/update.rs +++ b/crates/am/src/update.rs @@ -657,9 +657,10 @@ pub fn update(model: &mut AppModel, message: Message) -> Result Result Result Result Date: Wed, 22 Apr 2026 22:13:59 +0200 Subject: [PATCH 20/38] fix: prefix profile activation messages with 'am:' - toggle/use profile messages now match the 'am:' style used by sync - example: 'am: rust deactivated, 5 aliases' --- crates/am/src/update.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs index 85b6f020..fb65df6c 100644 --- a/crates/am/src/update.rs +++ b/crates/am/src/update.rs @@ -814,9 +814,9 @@ pub fn update(model: &mut AppModel, message: Message) -> Result Result Date: Wed, 22 Apr 2026 22:16:25 +0200 Subject: [PATCH 21/38] fix: emit full alias listing on fresh cd into a trusted project Previously 'cd' into a project showed only 'am: aliases changed (N added)'. Now it shows the detailed listing ('am: loaded .aliases' + each name -> command) when entering a directly-owned trusted project for the first time in this shell. - track _AM_PROJECT_PATH for trusted projects too (previously only for excluded) - derive is_fresh_project_load from: include_project && is_direct && !already_seen_path - use render_load_message directly on model.project_alias_set_and_subcommands() instead of re-reading the .aliases file from disk - incremental 'am: aliases changed (...)' still fires on edits and cd within project --- crates/am/src/update.rs | 104 ++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs index fb65df6c..df001595 100644 --- a/crates/am/src/update.rs +++ b/crates/am/src/update.rs @@ -669,40 +669,42 @@ pub fn update(model: &mut AppModel, message: Message) -> Result std::path::Path::new(prev) == cur, + _ => false, + }; + let show_warn = !quiet && is_direct && !already_seen_path; + let mut lines: Vec = Vec::new(); let mut security_changed = false; - let (include_project, project_path) = match model.project_trust() { + let mut include_project = false; + match model.project_trust() { Some(crate::trust::ProjectTrust::Trusted(..)) => { - (true, model.project_path().map(|p| p.to_path_buf())) + include_project = true; } - Some(trust) => { - let path = trust.path().to_path_buf(); - let is_direct = path.parent().is_some_and(|p| p == cwd); - let already_seen = prev_project_path - .as_deref() - .is_some_and(|p| std::path::Path::new(p) == path); - let show_msg = !quiet && is_direct && !already_seen; - match trust { - crate::trust::ProjectTrust::Unknown(_) if show_msg => { - lines.push(shell_impl.echo( - "am: .aliases found but not trusted. Run 'am trust' to review and allow.", - )); - } - crate::trust::ProjectTrust::Tampered(_) => { - security_changed = true; - if show_msg { - lines.push(shell_impl.echo( - "am: .aliases was modified since last trusted. Run 'am trust' to review and allow.", - )); - } - } - _ => {} + Some(crate::trust::ProjectTrust::Unknown(_)) => { + if show_warn { + lines.push(shell_impl.echo( + "am: .aliases found but not trusted. Run 'am trust' to review and allow.", + )); } - (false, Some(path)) } - None => (false, None), - }; + Some(crate::trust::ProjectTrust::Tampered(_)) => { + security_changed = true; + if show_warn { + lines.push(shell_impl.echo( + "am: .aliases was modified since last trusted. Run 'am trust' to review and allow.", + )); + } + } + Some(crate::trust::ProjectTrust::Untrusted(_)) | None => {} + } let (project_aliases, project_subs) = if include_project { model.project_alias_set_and_subcommands() @@ -713,8 +715,10 @@ pub fn update(model: &mut AppModel, message: Message) -> Result Result Result {} + (_, Some(cur)) => { + lines.push(shell_impl.set_env(env_vars::AM_PROJECT_PATH, cur)); + } + (Some(_), None) => { lines.push(shell_impl.unset_env(env_vars::AM_PROJECT_PATH)); } - } else if let Some(p) = project_path.as_deref() { - lines.push(shell_impl.set_env(env_vars::AM_PROJECT_PATH, &p.display().to_string())); - } else if prev_project_path.is_some() { - lines.push(shell_impl.unset_env(env_vars::AM_PROJECT_PATH)); + (None, None) => {} } let joined = lines From 5230686bbd7f6ecee15a7a8a10cc0a3f2478528e Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 22:27:26 +0200 Subject: [PATCH 22/38] fix: unify profile activation output with shadow-by-project breakdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before: am: rust activated — 5 loaded: b, i, l, r, t am: aliases changed (2 added) After: am: profile rust activated — 2 added: b, r | 3 shadowed by .aliases: i, l, t - enrich ToggleProfiles and UseProfilesAt messages to split profile aliases into 'actually added' vs 'shadowed by .aliases' - deactivation mirrors: 'N removed: ... | M kept by .aliases: ...' - shell wrappers call 'am sync --quiet' for 'use'/'profile use' so sync no longer emits its generic summary for these paths (profile handler owns it) - regenerate 10 init snapshots for the new wrapper layout --- crates/am/src/shell_wrappers/wrapper.bash | 7 +- crates/am/src/shell_wrappers/wrapper.fish | 8 +- crates/am/src/shell_wrappers/wrapper.ps1 | 10 +- crates/am/src/shell_wrappers/wrapper.zsh | 7 +- crates/am/src/update.rs | 132 +++++++++++++++--- ...ts__snapshot_init_bash_simple_profile.snap | 7 +- ...ot_init_bash_with_kubectl_subcommands.snap | 7 +- ...pshots__snapshot_init_fish_deep_chain.snap | 8 +- ...t_init_fish_globals_and_multi_profile.snap | 8 +- ...ots__snapshot_init_fish_multi_profile.snap | 8 +- ...ts__snapshot_init_fish_simple_profile.snap | 8 +- ...hots__snapshot_init_fish_with_globals.snap | 8 +- ...hot_init_fish_with_simple_subcommands.snap | 8 +- ...apshot_init_powershell_simple_profile.snap | 10 +- ...ots__snapshot_init_zsh_simple_profile.snap | 7 +- 15 files changed, 178 insertions(+), 65 deletions(-) diff --git a/crates/am/src/shell_wrappers/wrapper.bash b/crates/am/src/shell_wrappers/wrapper.bash index e9ed87e7..6234fc14 100644 --- a/crates/am/src/shell_wrappers/wrapper.bash +++ b/crates/am/src/shell_wrappers/wrapper.bash @@ -3,13 +3,14 @@ am() { local am_status=$? if [[ $am_status -ne 0 ]]; then return $am_status; fi case "$1" in - add|a|remove|r|use|u|trust|tui|t) + add|a|remove|r|trust|tui|t) eval "$(command am sync __SHELL__)" ;; - untrust) + use|u|untrust) eval "$(command am sync --quiet __SHELL__)" ;; profile|p) case "$2" in - use|u|add|a|remove|r) eval "$(command am sync __SHELL__)" ;; + use|u) eval "$(command am sync --quiet __SHELL__)" ;; + add|a|remove|r) eval "$(command am sync __SHELL__)" ;; esac ;; esac } diff --git a/crates/am/src/shell_wrappers/wrapper.fish b/crates/am/src/shell_wrappers/wrapper.fish index 65b3ece6..aa96d0c3 100644 --- a/crates/am/src/shell_wrappers/wrapper.fish +++ b/crates/am/src/shell_wrappers/wrapper.fish @@ -6,13 +6,15 @@ function am --wraps=am return $am_status end switch "$argv[1]" - case add a remove r use u trust tui t + case add a remove r trust tui t command am sync __SHELL__ | source - case untrust + case use u untrust command am sync --quiet __SHELL__ | source case profile p switch "$argv[2]" - case use u add a remove r + case use u + command am sync --quiet __SHELL__ | source + case add a remove r command am sync __SHELL__ | source end end diff --git a/crates/am/src/shell_wrappers/wrapper.ps1 b/crates/am/src/shell_wrappers/wrapper.ps1 index c8071015..effada5f 100644 --- a/crates/am/src/shell_wrappers/wrapper.ps1 +++ b/crates/am/src/shell_wrappers/wrapper.ps1 @@ -16,11 +16,15 @@ function am { if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } } - if ($first -in 'add', 'a', 'remove', 'r', 'use', 'u', 'trust', 'tui', 't') { + if ($first -in 'add', 'a', 'remove', 'r', 'trust', 'tui', 't') { & $runSync - } elseif ($first -eq 'untrust') { + } elseif ($first -in 'use', 'u', 'untrust') { & $runSyncQuiet } elseif ($first -in 'profile', 'p') { - if ($second -in 'use', 'u', 'add', 'a', 'remove', 'r') { & $runSync } + if ($second -in 'use', 'u') { + & $runSyncQuiet + } elseif ($second -in 'add', 'a', 'remove', 'r') { + & $runSync + } } } diff --git a/crates/am/src/shell_wrappers/wrapper.zsh b/crates/am/src/shell_wrappers/wrapper.zsh index e9ed87e7..6234fc14 100644 --- a/crates/am/src/shell_wrappers/wrapper.zsh +++ b/crates/am/src/shell_wrappers/wrapper.zsh @@ -3,13 +3,14 @@ am() { local am_status=$? if [[ $am_status -ne 0 ]]; then return $am_status; fi case "$1" in - add|a|remove|r|use|u|trust|tui|t) + add|a|remove|r|trust|tui|t) eval "$(command am sync __SHELL__)" ;; - untrust) + use|u|untrust) eval "$(command am sync --quiet __SHELL__)" ;; profile|p) case "$2" in - use|u|add|a|remove|r) eval "$(command am sync __SHELL__)" ;; + use|u) eval "$(command am sync --quiet __SHELL__)" ;; + add|a|remove|r) eval "$(command am sync __SHELL__)" ;; esac ;; esac } diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs index df001595..cc63ed3d 100644 --- a/crates/am/src/update.rs +++ b/crates/am/src/update.rs @@ -5,7 +5,6 @@ use crate::effects::Effect; use crate::env_vars; use crate::init::generate_init; use crate::precedence::{self, Precedence}; -use crate::profile::AliasCollection; use crate::project::ProjectAliases; use crate::shell::bash; use crate::shell::zsh; @@ -799,25 +798,28 @@ pub fn update(model: &mut AppModel, message: Message) -> Result = model .profile_config() .get_profile_by_name(&name) - .map(|p| (p.len(), p.short_list())) - .unwrap_or((0, String::new())); + .map(|p| { + p.aliases + .iter() + .map(|(n, _)| n.as_ref().to_string()) + .collect() + }) + .unwrap_or_default(); model.session.toggle_profile(name.clone()); - let action = if was_active { - "deactivated" - } else { - "activated" - }; - let msg = if was_active || list.is_empty() { - format!("am: {name} {action}, {total} aliases") - } else { - format!("am: {name} {action} — {total} loaded: {list}") - }; + let msg = profile_toggle_message( + &name, + !was_active, + None, + &profile_alias_names, + &project_names, + ); effects.push(Effect::Print(msg)); } effects.push(Effect::SaveSession); @@ -830,20 +832,28 @@ pub fn update(model: &mut AppModel, message: Message) -> Result = model .profile_config() .get_profile_by_name(&name) - .map(|p| (p.len(), p.short_list())) - .unwrap_or((0, String::new())); - model.session.use_profile_at(name.clone(), priority + i); + .map(|p| { + p.aliases + .iter() + .map(|(n, _)| n.as_ref().to_string()) + .collect() + }) + .unwrap_or_default(); let pos = priority + i; - let msg = if list.is_empty() { - format!("am: {name} activated at position {pos}, {total} aliases loaded") - } else { - format!("am: {name} activated at position {pos} — {total} loaded: {list}") - }; + model.session.use_profile_at(name.clone(), pos); + let msg = profile_toggle_message( + &name, + true, + Some(pos), + &profile_alias_names, + &project_names, + ); effects.push(Effect::Print(msg)); } effects.push(Effect::SaveSession); @@ -954,6 +964,82 @@ pub fn update(model: &mut AppModel, message: Message) -> Result std::collections::BTreeSet { + model + .project_aliases() + .map(|p| { + p.aliases + .iter() + .map(|(n, _)| n.as_ref().to_string()) + .collect() + }) + .unwrap_or_default() +} + +/// Build the user-facing message for a profile activation/deactivation, +/// highlighting which of the profile's aliases are shadowed by the project's +/// `.aliases`. `activated = false` means the profile is being deactivated. +fn profile_toggle_message( + name: &str, + activated: bool, + position: Option, + profile_aliases: &[String], + project_names: &std::collections::BTreeSet, +) -> String { + if profile_aliases.is_empty() { + let action = if activated { + "activated" + } else { + "deactivated" + }; + return match position { + Some(pos) => format!("am: profile {name} {action} at position {pos}, 0 aliases"), + None => format!("am: profile {name} {action}, 0 aliases"), + }; + } + + let (unshadowed, shadowed): (Vec<&String>, Vec<&String>) = profile_aliases + .iter() + .partition(|n| !project_names.contains(n.as_str())); + + let fmt_list = + |v: &[&String]| -> String { v.iter().map(|s| s.as_str()).collect::>().join(", ") }; + + let head = match (activated, position) { + (true, Some(pos)) => format!("am: profile {name} activated at position {pos}"), + (true, None) => format!("am: profile {name} activated"), + (false, _) => format!("am: profile {name} deactivated"), + }; + + let (primary_verb, secondary_verb) = if activated { + ("added", "shadowed by .aliases") + } else { + ("removed", "kept by .aliases") + }; + + match (unshadowed.is_empty(), shadowed.is_empty()) { + (false, true) => format!( + "{head} — {} {primary_verb}: {}", + unshadowed.len(), + fmt_list(&unshadowed) + ), + (true, false) => format!( + "{head} — all {} {secondary_verb}: {}", + shadowed.len(), + fmt_list(&shadowed) + ), + (false, false) => format!( + "{head} — {} {primary_verb}: {} | {} {secondary_verb}: {}", + unshadowed.len(), + fmt_list(&unshadowed), + shadowed.len(), + fmt_list(&shadowed) + ), + (true, true) => unreachable!("profile_aliases non-empty but both partitions empty"), + } +} + fn resolve_profile<'a>( model: &'a AppModel, target: &AliasTarget, diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_simple_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_simple_profile.snap index 1a2eb344..3857644b 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_simple_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_simple_profile.snap @@ -10,13 +10,14 @@ am() { local am_status=$? if [[ $am_status -ne 0 ]]; then return $am_status; fi case "$1" in - add|a|remove|r|use|u|trust|tui|t) + add|a|remove|r|trust|tui|t) eval "$(command am sync bash)" ;; - untrust) + use|u|untrust) eval "$(command am sync --quiet bash)" ;; profile|p) case "$2" in - use|u|add|a|remove|r) eval "$(command am sync bash)" ;; + use|u) eval "$(command am sync --quiet bash)" ;; + add|a|remove|r) eval "$(command am sync bash)" ;; esac ;; esac } diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_with_kubectl_subcommands.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_with_kubectl_subcommands.snap index 3b551157..0264d221 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_with_kubectl_subcommands.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_with_kubectl_subcommands.snap @@ -39,13 +39,14 @@ am() { local am_status=$? if [[ $am_status -ne 0 ]]; then return $am_status; fi case "$1" in - add|a|remove|r|use|u|trust|tui|t) + add|a|remove|r|trust|tui|t) eval "$(command am sync bash)" ;; - untrust) + use|u|untrust) eval "$(command am sync --quiet bash)" ;; profile|p) case "$2" in - use|u|add|a|remove|r) eval "$(command am sync bash)" ;; + use|u) eval "$(command am sync --quiet bash)" ;; + add|a|remove|r) eval "$(command am sync bash)" ;; esac ;; esac } diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap index fcfee91a..48766dbd 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap @@ -14,13 +14,15 @@ function am --wraps=am return $am_status end switch "$argv[1]" - case add a remove r use u trust tui t + case add a remove r trust tui t command am sync fish | source - case untrust + case use u untrust command am sync --quiet fish | source case profile p switch "$argv[2]" - case use u add a remove r + case use u + command am sync --quiet fish | source + case add a remove r command am sync fish | source end end diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap index 8716c208..d13bc89f 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap @@ -17,13 +17,15 @@ function am --wraps=am return $am_status end switch "$argv[1]" - case add a remove r use u trust tui t + case add a remove r trust tui t command am sync fish | source - case untrust + case use u untrust command am sync --quiet fish | source case profile p switch "$argv[2]" - case use u add a remove r + case use u + command am sync --quiet fish | source + case add a remove r command am sync fish | source end end diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap index 63c351f6..8e89594d 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap @@ -16,13 +16,15 @@ function am --wraps=am return $am_status end switch "$argv[1]" - case add a remove r use u trust tui t + case add a remove r trust tui t command am sync fish | source - case untrust + case use u untrust command am sync --quiet fish | source case profile p switch "$argv[2]" - case use u add a remove r + case use u + command am sync --quiet fish | source + case add a remove r command am sync fish | source end end diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap index b38ff72b..6e4616a1 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap @@ -13,13 +13,15 @@ function am --wraps=am return $am_status end switch "$argv[1]" - case add a remove r use u trust tui t + case add a remove r trust tui t command am sync fish | source - case untrust + case use u untrust command am sync --quiet fish | source case profile p switch "$argv[2]" - case use u add a remove r + case use u + command am sync --quiet fish | source + case add a remove r command am sync fish | source end end diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap index 7bb36783..9a960000 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap @@ -13,13 +13,15 @@ function am --wraps=am return $am_status end switch "$argv[1]" - case add a remove r use u trust tui t + case add a remove r trust tui t command am sync fish | source - case untrust + case use u untrust command am sync --quiet fish | source case profile p switch "$argv[2]" - case use u add a remove r + case use u + command am sync --quiet fish | source + case add a remove r command am sync fish | source end end diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap index 4a4a26cf..cf7f111c 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap @@ -23,13 +23,15 @@ function am --wraps=am return $am_status end switch "$argv[1]" - case add a remove r use u trust tui t + case add a remove r trust tui t command am sync fish | source - case untrust + case use u untrust command am sync --quiet fish | source case profile p switch "$argv[2]" - case use u add a remove r + case use u + command am sync --quiet fish | source + case add a remove r command am sync fish | source end end diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_powershell_simple_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_powershell_simple_profile.snap index 5a555787..cd465ea6 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_powershell_simple_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_powershell_simple_profile.snap @@ -23,12 +23,16 @@ function am { if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } } - if ($first -in 'add', 'a', 'remove', 'r', 'use', 'u', 'trust', 'tui', 't') { + if ($first -in 'add', 'a', 'remove', 'r', 'trust', 'tui', 't') { & $runSync - } elseif ($first -eq 'untrust') { + } elseif ($first -in 'use', 'u', 'untrust') { & $runSyncQuiet } elseif ($first -in 'profile', 'p') { - if ($second -in 'use', 'u', 'add', 'a', 'remove', 'r') { & $runSync } + if ($second -in 'use', 'u') { + & $runSyncQuiet + } elseif ($second -in 'add', 'a', 'remove', 'r') { + & $runSync + } } } diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_zsh_simple_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_zsh_simple_profile.snap index 85d28a31..9abeb63a 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_zsh_simple_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_zsh_simple_profile.snap @@ -10,13 +10,14 @@ am() { local am_status=$? if [[ $am_status -ne 0 ]]; then return $am_status; fi case "$1" in - add|a|remove|r|use|u|trust|tui|t) + add|a|remove|r|trust|tui|t) eval "$(command am sync zsh)" ;; - untrust) + use|u|untrust) eval "$(command am sync --quiet zsh)" ;; profile|p) case "$2" in - use|u|add|a|remove|r) eval "$(command am sync zsh)" ;; + use|u) eval "$(command am sync --quiet zsh)" ;; + add|a|remove|r) eval "$(command am sync zsh)" ;; esac ;; esac } From 63001109cc07dc1e915e4ff8e6534309fdb52086 Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 22:37:31 +0200 Subject: [PATCH 23/38] fix: use 'loaded'/'unloaded' instead of 'added'/'removed' in messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'removed' sounds like deletion from disk — confusing when aliases are only unloaded from the current shell (the profile/.aliases file is untouched). Swap to the same verbs render_load_message / render_unload_message already use. - 'am: profile rust activated — 2 loaded: b, r | 3 shadowed by .aliases: ...' - 'am: profile rust deactivated — 2 unloaded: b, r | 3 kept by .aliases: ...' - 'am: aliases changed (2 loaded, 1 updated, 3 unloaded)' --- crates/am/src/update.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs index cc63ed3d..067979f6 100644 --- a/crates/am/src/update.rs +++ b/crates/am/src/update.rs @@ -740,13 +740,13 @@ pub fn update(model: &mut AppModel, message: Message) -> Result Date: Wed, 22 Apr 2026 23:05:32 +0200 Subject: [PATCH 24/38] fix: include subcommand keys in profile activation message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Given profile git has both 'gm' and subcommand aliases 'git:psh'/'git:st', 'am use git' now reports all three items instead of just the regular alias. - profile_items(&Profile) combines aliases with subcommand keys - project_alias_names(model) now includes project subcommand keys too, so shadowing detection covers 'git:st' in profile vs 'git:st' in .aliases - message: 'am: profile git activated — 3 loaded: gm, git:psh, git:st' --- crates/am/src/update.rs | 67 ++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 38 deletions(-) diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs index 067979f6..d7fffa8c 100644 --- a/crates/am/src/update.rs +++ b/crates/am/src/update.rs @@ -802,24 +802,13 @@ pub fn update(model: &mut AppModel, message: Message) -> Result = model + let items = model .profile_config() .get_profile_by_name(&name) - .map(|p| { - p.aliases - .iter() - .map(|(n, _)| n.as_ref().to_string()) - .collect() - }) + .map(profile_items) .unwrap_or_default(); model.session.toggle_profile(name.clone()); - let msg = profile_toggle_message( - &name, - !was_active, - None, - &profile_alias_names, - &project_names, - ); + let msg = profile_toggle_message(&name, !was_active, None, &items, &project_names); effects.push(Effect::Print(msg)); } effects.push(Effect::SaveSession); @@ -835,25 +824,14 @@ pub fn update(model: &mut AppModel, message: Message) -> Result = model + let items = model .profile_config() .get_profile_by_name(&name) - .map(|p| { - p.aliases - .iter() - .map(|(n, _)| n.as_ref().to_string()) - .collect() - }) + .map(profile_items) .unwrap_or_default(); let pos = priority + i; model.session.use_profile_at(name.clone(), pos); - let msg = profile_toggle_message( - &name, - true, - Some(pos), - &profile_alias_names, - &project_names, - ); + let msg = profile_toggle_message(&name, true, Some(pos), &items, &project_names); effects.push(Effect::Print(msg)); } effects.push(Effect::SaveSession); @@ -964,17 +942,30 @@ pub fn update(model: &mut AppModel, message: Message) -> Result std::collections::BTreeSet { - model - .project_aliases() - .map(|p| { - p.aliases - .iter() - .map(|(n, _)| n.as_ref().to_string()) - .collect() - }) - .unwrap_or_default() + let Some(project) = model.project_aliases() else { + return std::collections::BTreeSet::new(); + }; + let mut set: std::collections::BTreeSet = project + .aliases + .iter() + .map(|(n, _)| n.as_ref().to_string()) + .collect(); + set.extend(project.subcommands.keys().cloned()); + set +} + +/// Profile items (regular alias names + subcommand keys). +fn profile_items(profile: &Profile) -> Vec { + let mut items: Vec = profile + .aliases + .iter() + .map(|(n, _)| n.as_ref().to_string()) + .collect(); + items.extend(profile.subcommands.keys().cloned()); + items } /// Build the user-facing message for a profile activation/deactivation, From f104ef44248d48e9eb57915f69ce7f90db0ea108 Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 22 Apr 2026 23:14:56 +0200 Subject: [PATCH 25/38] docs: update for am sync and new message format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace stale references to 'am hook' and 'am reload' (removed in favor of the unified 'am sync' command) and refresh all marketing / user-facing sample output to match the current engine behavior. - installation subcommand table: 'am hook' -> 'am sync' (EN + DE) - project-aliases 'How It Works': rewritten for the precedence engine; describes layer merging, minimal-diff emission, and automatic shadow restoration (EN + DE) - subcommand-aliases 'How It Works': 'am reload' reference -> 'am sync' triggered by cd or an am mutation (EN + DE) - UseCases.vue / UseCasesDe.vue: profile activation samples now use the 'am: profile X activated — N loaded: ...' format; cd-into-project samples show the full 'am: loaded .aliases' listing that cd actually produces --- website/.vitepress/theme/UseCases.vue | 26 +++++++++++++++++-------- website/.vitepress/theme/UseCasesDe.vue | 26 +++++++++++++++++-------- website/de/guide/installation.md | 2 +- website/de/usage/project-aliases.md | 9 ++++++--- website/de/usage/subcommand-aliases.md | 2 +- website/guide/installation.md | 2 +- website/usage/project-aliases.md | 9 +++++---- website/usage/subcommand-aliases.md | 2 +- 8 files changed, 51 insertions(+), 27 deletions(-) diff --git a/website/.vitepress/theme/UseCases.vue b/website/.vitepress/theme/UseCases.vue index 756c0a6f..f74e679d 100644 --- a/website/.vitepress/theme/UseCases.vue +++ b/website/.vitepress/theme/UseCases.vue @@ -65,12 +65,12 @@
every day after that
am use node
-node activated — 4 loaded: l, b, r, t
+am: profile node activated — 4 loaded: l, b, r, t
 l      # npm run lint
 t      # npm test
 
 am use rust
-rust activated — 4 loaded: l, b, r, t
+am: profile rust activated — 4 loaded: l, b, r, t
 l      # cargo clippy --locked --all-targets -- -D warnings
 t      # cargo test
@@ -126,7 +126,11 @@
every developer, automatically
cd ~/work/myproject
-project activated — 4 loaded: ci, db, deploy, ti
+am: loaded .aliases
+  ci     → just ci-check
+  db     → just db-migrate --env staging
+  deploy → just deploy --target production
+  ti     → cargo test --features integration -- --test-threads 1
 
 am ls
 📁 project (.aliases)
@@ -251,12 +255,18 @@ api-7d4f9b8c6-xk2pm    1/1     Running   0
           
switching clients — nothing to remember
cd ~/clients/client-a
-client-a activated — 4 loaded: deploy, logs, stage, tf:plan
+am: loaded .aliases
+  deploy  → ./scripts/deploy.sh --env staging
+  logs    → ssh app@client-a.internal journalctl -u api -f
+  stage   → open https://staging.client-a.internal
+  tf:plan → terraform plan -var-file=client-a.tfvars
 deploy
 
 cd ~/clients/client-b
-client-a deactivated
-client-b activated — 3 loaded: infra:plan, preview, ship
+am: loaded .aliases
+  infra:plan → terraform -chdir=infra plan
+  preview    → open https://preview.client-b.internal
+  ship       → ./scripts/ship.sh
 preview
 
 # no stale aliases. no cross-contamination.
@@ -317,8 +327,8 @@ client-b activated — 3 loaded: infra:plan, preview, ship
what's actually loaded, right now
am use git work
-git activated — 4 loaded: gl, gm, gp, gs
-work activated — 3 loaded: jira, standup, vpn
+am: profile git activated — 4 loaded: gl, gm, gp, gs
+am: profile work activated — 3 loaded: jira, standup, vpn
 
 am ls
 ├─● git (active)
diff --git a/website/.vitepress/theme/UseCasesDe.vue b/website/.vitepress/theme/UseCasesDe.vue
index 18254f13..b9ae37f6 100644
--- a/website/.vitepress/theme/UseCasesDe.vue
+++ b/website/.vitepress/theme/UseCasesDe.vue
@@ -66,12 +66,12 @@
           
danach jeden Tag
am use node
-node activated — 4 loaded: l, b, r, t
+am: profile node activated — 4 loaded: l, b, r, t
 l      # npm run lint
 t      # npm test
 
 am use rust
-rust activated — 4 loaded: l, b, r, t
+am: profile rust activated — 4 loaded: l, b, r, t
 l      # cargo clippy --locked --all-targets -- -D warnings
 t      # cargo test
@@ -129,7 +129,11 @@
für jeden Entwickler, automatisch
cd ~/work/myproject
-project activated — 4 loaded: ci, db, deploy, ti
+am: loaded .aliases
+  ci     → just ci-check
+  db     → just db-migrate --env staging
+  deploy → just deploy --target production
+  ti     → cargo test --features integration -- --test-threads 1
 
 am ls
 📁 project (.aliases)
@@ -257,12 +261,18 @@ api-7d4f9b8c6-xk2pm    1/1     Running   0
           
Kunden wechseln — nichts zu merken
cd ~/clients/client-a
-client-a activated — 4 loaded: deploy, logs, stage, tf:plan
+am: loaded .aliases
+  deploy  → ./scripts/deploy.sh --env staging
+  logs    → ssh app@client-a.internal journalctl -u api -f
+  stage   → open https://staging.client-a.internal
+  tf:plan → terraform plan -var-file=client-a.tfvars
 deploy
 
 cd ~/clients/client-b
-client-a deactivated
-client-b activated — 3 loaded: infra:plan, preview, ship
+am: loaded .aliases
+  infra:plan → terraform -chdir=infra plan
+  preview    → open https://preview.client-b.internal
+  ship       → ./scripts/ship.sh
 preview
 
 # keine veralteten Aliase. keine Vermischung.
@@ -325,8 +335,8 @@ client-b activated — 3 loaded: infra:plan, preview, ship
was gerade aktiv ist
am use git work
-git activated — 4 loaded: gl, gm, gp, gs
-work activated — 3 loaded: jira, standup, vpn
+am: profile git activated — 4 loaded: gl, gm, gp, gs
+am: profile work activated — 3 loaded: jira, standup, vpn
 
 am ls
 ├─● git (active)
diff --git a/website/de/guide/installation.md b/website/de/guide/installation.md
index 1fd1d96d..c405c7cb 100644
--- a/website/de/guide/installation.md
+++ b/website/de/guide/installation.md
@@ -57,7 +57,7 @@ Der TUI-Companion (`am-tui`) ist ein separates Paket. Optional, aber empfohlen f
 | `am status` | Prüfen, ob die Shell korrekt eingerichtet ist |
 | `am setup` | Geführte Shell-Einrichtung |
 | `am tui` | Interaktives TUI zur Alias-Verwaltung (*separate Installation*) |
-| `am hook` | Wird vom cd-Hook aufgerufen (intern) |
+| `am sync` | Wird vom Shell-Wrapper und cd-Hook zum Aliassynchronisieren aufgerufen (intern) |
 
 ::: tip
 Alle Verben haben Kurzformen: `am a` für add, `am r` für remove, `am p` für profile, `am l` für ls.
diff --git a/website/de/usage/project-aliases.md b/website/de/usage/project-aliases.md
index 3dcc2047..590138be 100644
--- a/website/de/usage/project-aliases.md
+++ b/website/de/usage/project-aliases.md
@@ -117,12 +117,15 @@ Diese Meldungen erscheinen nur beim Betreten oder Verlassen des Verzeichnisses m
 
 ## Wie es funktioniert
 
-Der `am init` Shell-Hook ruft `am hook ` bei jedem Verzeichniswechsel auf. Der Hook:
+Der `am init` Shell-Hook ruft `am sync ` bei jedem Verzeichniswechsel auf. Sync:
 
 1. Sucht vom aktuellen Verzeichnis aufwärts nach einer `.aliases`-Datei (stoppt vor `$HOME`)
 2. Prüft, ob die Datei vertrauenswürdig ist (Pfad + Hash in `security.toml`)
-3. Falls vertrauenswürdig: entlädt vorherige Projekt-Aliase und lädt die neuen
-4. Falls nicht vertrauenswürdig: zeigt eine Warnung oder bleibt still, je nach Vertrauensstatus
+3. Führt alle Ebenen mit Präzedenz zusammen — global < Profil < Projekt — und berechnet den minimalen Satz an Shell-Operationen
+4. Gibt nur die Unloads/Loads aus, die sich tatsächlich auf die Shell auswirken (unveränderte Aliase bleiben bestehen)
+5. Falls die Datei nicht vertrauenswürdig ist, zeigt eine Warnung oder bleibt still, je nach Vertrauensstatus
+
+Dadurch folgen Aliase automatisch dem Kontext — ein Wechsel in ein Rust-Projekt lädt die Rust-Aliase, ein Wechsel in ein Node-Projekt die Node-Aliase — vorausgesetzt, die jeweiligen `.aliases`-Dateien sind vertrauenswürdig. Wenn ein Projekt-Alias denselben Namen wie ein Profil-Alias hat, gewinnt der Projekt-Wert; beim Verlassen des Projekts übernimmt automatisch wieder der Profil-Wert.
 
 ## Workflow
 
diff --git a/website/de/usage/subcommand-aliases.md b/website/de/usage/subcommand-aliases.md
index 50f01397..aff2bde5 100644
--- a/website/de/usage/subcommand-aliases.md
+++ b/website/de/usage/subcommand-aliases.md
@@ -79,7 +79,7 @@ Kurzform: `am r -g jj:ab`
 
 ## Wie es funktioniert
 
-Beim Shell-Init (und bei `am reload`) generiert amoxide eine Wrapper-Funktion für jedes Programm mit Subcommand-Aliasen:
+Beim Shell-Init (und bei jedem `am sync`, ausgelöst durch `cd` oder eine `am`-Änderung) generiert amoxide eine Wrapper-Funktion für jedes Programm mit Subcommand-Aliasen:
 
 ```sh
 # generiert für jj (bash/zsh)
diff --git a/website/guide/installation.md b/website/guide/installation.md
index 39a679ef..9f838e15 100644
--- a/website/guide/installation.md
+++ b/website/guide/installation.md
@@ -57,7 +57,7 @@ The TUI companion (`am-tui`) is a separate install. It's optional but recommende
 | `am status` | Check if the shell is set up correctly |
 | `am setup` | Guided shell setup |
 | `am tui` | Interactive TUI for managing aliases and profiles (*separate install*) |
-| `am hook` | Called by the cd hook (internal) |
+| `am sync` | Called by the shell wrapper and cd hook to sync aliases (internal) |
 
 ::: tip
 All verbs have short forms: `am a` for add, `am r` for remove, `am p` for profile, `am l` for ls.
diff --git a/website/usage/project-aliases.md b/website/usage/project-aliases.md
index 8fee5981..29d9c4cb 100644
--- a/website/usage/project-aliases.md
+++ b/website/usage/project-aliases.md
@@ -117,14 +117,15 @@ These messages only appear when entering or leaving the directory containing the
 
 ## How It Works
 
-The `am init` shell hook calls `am hook ` on every directory change. The hook:
+The `am init` shell hook calls `am sync ` on every directory change. Sync:
 
 1. Walks up from the current directory looking for a `.aliases` file (stopping before `$HOME`)
 2. Checks whether the file is trusted (path + hash match in `security.toml`)
-3. If trusted: unloads any previously active project aliases and loads the new ones
-4. If not trusted: shows a warning or stays silent, depending on the trust state
+3. Merges all layers with precedence — global < profile < project — and computes the minimal set of shell operations needed
+4. Emits only the unloads/loads that actually change the shell (unchanged aliases stay put)
+5. If the file is not trusted, shows a warning or stays silent, depending on the trust state
 
-This means aliases automatically follow your context — switch to a Rust project and get Rust aliases, switch to a Node project and get Node aliases — as long as you've trusted the respective `.aliases` files.
+This means aliases automatically follow your context — switch to a Rust project and get Rust aliases, switch to a Node project and get Node aliases — as long as you've trusted the respective `.aliases` files. When a project alias shares a name with a profile alias, the project value wins; when you leave the project, the profile value takes over automatically.
 
 ## Workflow
 
diff --git a/website/usage/subcommand-aliases.md b/website/usage/subcommand-aliases.md
index 3d5ef58b..3ff15676 100644
--- a/website/usage/subcommand-aliases.md
+++ b/website/usage/subcommand-aliases.md
@@ -86,7 +86,7 @@ Short form: `am r -g jj:ab`
 
 ## How It Works
 
-At shell init (and on `am reload`), amoxide generates a wrapper function for each program that has subcommand aliases:
+At shell init (and on every `am sync` triggered by `cd` or an `am` mutation), amoxide generates a wrapper function for each program that has subcommand aliases:
 
 ```sh
 # generated for jj (bash/zsh)

From 88f6fdcfb19fc9dce9134bcbe1ece7aee77b2028 Mon Sep 17 00:00:00 2001
From: Sven Kanoldt 
Date: Wed, 22 Apr 2026 23:32:52 +0200
Subject: [PATCH 26/38] fix: clear fish completion wraps before redefining
 aliases

Fish's 'alias' builtin stores --wraps as a completion entry via
'complete --wraps'. That entry persists independently of 'functions -e',
so redefining the same alias accumulates --wraps= flags each time:

  function i --wraps='profile_val' --wraps='project_val' --description ...
    project_val $argv
  end

Prepend 'functions -e NAME' + 'complete -e -c NAME' to every fish alias
emission so the completion wraps are cleared before the alias is (re)defined.
Applies to both the alias-form and the template-function form. The abbr form
is unaffected (abbr --add replaces cleanly by name).

- updated 4 fish unit tests
- regenerated 10 fish snapshots (6 init + 4 sync)
---
 crates/am/src/shell/fish.rs                   | 32 ++++++++++++++-----
 ...pshots__snapshot_init_fish_deep_chain.snap |  6 ++++
 ...t_init_fish_globals_and_multi_profile.snap |  8 +++++
 ...ots__snapshot_init_fish_multi_profile.snap |  6 ++++
 ...ts__snapshot_init_fish_simple_profile.snap |  4 +++
 ...hots__snapshot_init_fish_with_globals.snap |  4 +++
 ...hot_init_fish_with_simple_subcommands.snap |  2 ++
 ...hot_sync_fish_fresh_load_project_only.snap |  4 +++
 ...nc_fish_incremental_one_alias_updated.snap |  2 ++
 ...aving_project_with_shadow_restoration.snap |  2 ++
 ...t_sync_fish_transition_to_new_project.snap |  2 ++
 11 files changed, 64 insertions(+), 8 deletions(-)

diff --git a/crates/am/src/shell/fish.rs b/crates/am/src/shell/fish.rs
index cacc3d62..76e8ecaf 100644
--- a/crates/am/src/shell/fish.rs
+++ b/crates/am/src/shell/fish.rs
@@ -127,13 +127,29 @@ impl ShellAdapter for Fish {
     }
 
     fn alias(&self, entry: &AliasEntry) -> String {
+        // Fish's `alias` builtin stores `--wraps` as a completion entry via
+        // `complete --wraps`, which persists independently of `functions -e`.
+        // Redefining an alias without clearing that completion stacks the
+        // `--wraps=` list. Erase both the function and its completion wraps
+        // before redefining to guarantee a clean single `--wraps`.
+        let prelude = format!(
+            "functions -e {name}\ncomplete -e -c {name}",
+            name = entry.name
+        );
         if !entry.raw && has_template_args(entry.command) {
             let body = substitute_fish(entry.command);
-            format!("function {}\n    {}\nend", entry.name, body)
+            format!(
+                "{prelude}\nfunction {name}\n    {body}\nend",
+                name = entry.name
+            )
         } else if self.use_abbr {
             format!("abbr --add {} {}", entry.name, quote_cmd(entry.command))
         } else {
-            format!("alias {} {}", entry.name, quote_cmd(entry.command))
+            format!(
+                "{prelude}\nalias {name} {cmd}",
+                name = entry.name,
+                cmd = quote_cmd(entry.command)
+            )
         }
     }
 
@@ -240,11 +256,11 @@ mod tests {
     fn test_fish_simple_alias() {
         assert_eq!(
             Fish::default().alias(&simple("h", "'echo hello'")),
-            "alias h 'echo hello'"
+            "functions -e h\ncomplete -e -c h\nalias h 'echo hello'"
         );
         assert_eq!(
             Fish::default().alias(&simple("h", "echo hello")),
-            "alias h \"echo hello\""
+            "functions -e h\ncomplete -e -c h\nalias h \"echo hello\""
         );
     }
 
@@ -252,11 +268,11 @@ mod tests {
     fn test_fish_parameterized() {
         assert_eq!(
             Fish::default().alias(&simple("cmf", "cm feat: {{@}}")),
-            "function cmf\n    cm feat: $argv\nend"
+            "functions -e cmf\ncomplete -e -c cmf\nfunction cmf\n    cm feat: $argv\nend"
         );
         assert_eq!(
             Fish::default().alias(&simple("x", "echo {{1}} and {{2}}")),
-            "function x\n    echo $argv[1] and $argv[2]\nend"
+            "functions -e x\ncomplete -e -c x\nfunction x\n    echo $argv[1] and $argv[2]\nend"
         );
     }
 
@@ -264,7 +280,7 @@ mod tests {
     fn test_fish_raw_skips_templates() {
         assert_eq!(
             Fish::default().alias(&raw("my-awk", "awk '{print {{1}}}'")),
-            "alias my-awk \"awk '{print {{1}}}'\""
+            "functions -e my-awk\ncomplete -e -c my-awk\nalias my-awk \"awk '{print {{1}}}'\""
         );
     }
 
@@ -412,7 +428,7 @@ mod tests {
         let fish = Fish { use_abbr: true };
         assert_eq!(
             fish.alias(&simple("cmf", "cm feat: {{@}}")),
-            "function cmf\n    cm feat: $argv\nend"
+            "functions -e cmf\ncomplete -e -c cmf\nfunction cmf\n    cm feat: $argv\nend"
         );
     }
 
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap
index 48766dbd..24468161 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap
@@ -2,8 +2,14 @@
 source: crates/am/tests/snapshots.rs
 expression: output
 ---
+functions -e ct
+complete -e -c ct
 alias ct "cargo test"
+functions -e gs
+complete -e -c gs
 alias gs "git status"
+functions -e ll
+complete -e -c ll
 alias ll "ls -lha"
 set -gx _AM_ALIASES "ct|ab61de4,gs|22db469,ll|619d266"
 # am wrapper: sync after mutations
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap
index d13bc89f..a3d1d6cf 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap
@@ -2,11 +2,19 @@
 source: crates/am/tests/snapshots.rs
 expression: output
 ---
+functions -e cm
+complete -e -c cm
 alias cm "git commit -sm"
+functions -e cmf
+complete -e -c cmf
 function cmf
     cm feat: $argv
 end
+functions -e gs
+complete -e -c gs
 alias gs "git status"
+functions -e ll
+complete -e -c ll
 alias ll "ls -lha"
 set -gx _AM_ALIASES "cm|bed528f,cmf|9c99949,gs|22db469,ll|619d266"
 # am wrapper: sync after mutations
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap
index 8e89594d..2cf38dbf 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap
@@ -2,10 +2,16 @@
 source: crates/am/tests/snapshots.rs
 expression: output
 ---
+functions -e cm
+complete -e -c cm
 alias cm "git commit -sm"
+functions -e cmf
+complete -e -c cmf
 function cmf
     cm feat: $argv
 end
+functions -e gs
+complete -e -c gs
 alias gs "git status"
 set -gx _AM_ALIASES "cm|bed528f,cmf|9c99949,gs|22db469"
 # am wrapper: sync after mutations
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap
index 6e4616a1..5af16f07 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap
@@ -2,7 +2,11 @@
 source: crates/am/tests/snapshots.rs
 expression: output
 ---
+functions -e gs
+complete -e -c gs
 alias gs "git status"
+functions -e ll
+complete -e -c ll
 alias ll "ls -lha"
 set -gx _AM_ALIASES "gs|22db469,ll|619d266"
 # am wrapper: sync after mutations
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap
index 9a960000..1b69a147 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap
@@ -2,7 +2,11 @@
 source: crates/am/tests/snapshots.rs
 expression: output
 ---
+functions -e ct
+complete -e -c ct
 alias ct "cargo test"
+functions -e ll
+complete -e -c ll
 alias ll "ls -lha"
 set -gx _AM_ALIASES "ct|ab61de4,ll|619d266"
 # am wrapper: sync after mutations
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap
index cf7f111c..f0cc9d30 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap
@@ -2,6 +2,8 @@
 source: crates/am/tests/snapshots.rs
 expression: output
 ---
+functions -e gs
+complete -e -c gs
 alias gs "git status"
 function jj --wraps=jj
   switch $argv[1]
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_fresh_load_project_only.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_fresh_load_project_only.snap
index 78754585..cf7b72c7 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_fresh_load_project_only.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_fresh_load_project_only.snap
@@ -2,6 +2,10 @@
 source: crates/am/tests/snapshots.rs
 expression: output
 ---
+functions -e b
+complete -e -c b
 alias b "cargo build"
+functions -e t
+complete -e -c t
 alias t "cargo test"
 set -gx _AM_ALIASES "b|b58de66,t|ab61de4"
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_incremental_one_alias_updated.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_incremental_one_alias_updated.snap
index ded8322b..b2b07e23 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_incremental_one_alias_updated.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_incremental_one_alias_updated.snap
@@ -3,5 +3,7 @@ source: crates/am/tests/snapshots.rs
 expression: output
 ---
 functions -e b
+functions -e b
+complete -e -c b
 alias b "cargo build --release"
 set -gx _AM_ALIASES "b|0dc3caa,t|ab61de4"
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_leaving_project_with_shadow_restoration.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_leaving_project_with_shadow_restoration.snap
index e1c67de4..92a92ef8 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_leaving_project_with_shadow_restoration.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_leaving_project_with_shadow_restoration.snap
@@ -4,5 +4,7 @@ expression: output
 ---
 functions -e b
 functions -e t
+functions -e t
+complete -e -c t
 alias t "cargo test"
 set -gx _AM_ALIASES "t|ab61de4,ll|619d266"
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_transition_to_new_project.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_transition_to_new_project.snap
index 811347e5..52980f27 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_transition_to_new_project.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_transition_to_new_project.snap
@@ -3,5 +3,7 @@ source: crates/am/tests/snapshots.rs
 expression: output
 ---
 functions -e old1
+functions -e new1
+complete -e -c new1
 alias new1 "echo new"
 set -gx _AM_ALIASES "new1|4d1fcee"

From 84bd8a7595457d54bf16ff2ce2e2e6bbb188f3b5 Mon Sep 17 00:00:00 2001
From: Sven Kanoldt 
Date: Wed, 22 Apr 2026 23:38:09 +0200
Subject: [PATCH 27/38] fix: emit fish aliases as plain functions without
 --wraps

The previous attempt (task 88f6fdcf) prepended 'complete -e -c NAME' hoping
to clear the --wraps entry fish's 'alias' builtin stores via the completion
system. On real fish installations that still produced stacked --wraps=
across redefinitions.

Drop fish's 'alias' builtin entirely. Emit 'function NAME\n    CMD \$argv\nend'
directly, with no --wraps, so there is nothing for fish to accumulate.

- 'type i' now shows a single clean function body, no --wraps
- 'functions -e NAME' + 'complete -e -c NAME' still prefixed for each
  emission to scrub any --wraps left over from older amoxide versions
  that used 'alias'
- abbr mode unchanged (abbr --add already replaces cleanly)
- trade-off: loss of --wraps completion inheritance; in practice this was
  unreliable for aliases whose command is a pipe or chain anyway
---
 crates/am/src/init.rs                         |  6 ++---
 crates/am/src/precedence.rs                   |  6 ++---
 crates/am/src/shell/fish.rs                   | 24 +++++++++++--------
 crates/am/tests/snapshots.rs                  |  2 +-
 ...pshots__snapshot_init_fish_deep_chain.snap | 12 +++++++---
 ...t_init_fish_globals_and_multi_profile.snap | 12 +++++++---
 ...ots__snapshot_init_fish_multi_profile.snap |  8 +++++--
 ...ts__snapshot_init_fish_simple_profile.snap |  8 +++++--
 ...hots__snapshot_init_fish_with_globals.snap |  8 +++++--
 ...hot_init_fish_with_simple_subcommands.snap |  4 +++-
 ...hot_sync_fish_fresh_load_project_only.snap |  8 +++++--
 ...nc_fish_incremental_one_alias_updated.snap |  4 +++-
 ...aving_project_with_shadow_restoration.snap |  4 +++-
 ...t_sync_fish_transition_to_new_project.snap |  4 +++-
 14 files changed, 75 insertions(+), 35 deletions(-)

diff --git a/crates/am/src/init.rs b/crates/am/src/init.rs
index 48e54c2a..493d1d0d 100644
--- a/crates/am/src/init.rs
+++ b/crates/am/src/init.rs
@@ -169,8 +169,8 @@ mod tests {
             &aliases,
             &SubcommandSet::new(),
         );
-        assert!(output.contains("alias gs \"git status\""));
-        assert!(output.contains("alias ll \"ls -lha\""));
+        assert!(output.contains("function gs\n    git status $argv\nend"));
+        assert!(output.contains("function ll\n    ls -lha $argv\nend"));
     }
 
     #[test]
@@ -275,7 +275,7 @@ mod tests {
             &AliasSet::default(),
             &SubcommandSet::new(),
         );
-        assert!(output.contains("alias ll \"ls -lha\""));
+        assert!(output.contains("function ll\n    ls -lha $argv\nend"));
     }
 
     #[test]
diff --git a/crates/am/src/precedence.rs b/crates/am/src/precedence.rs
index d06f12fb..2121db8a 100644
--- a/crates/am/src/precedence.rs
+++ b/crates/am/src/precedence.rs
@@ -786,13 +786,13 @@ mod tests {
             "changed b must be unloaded: {out}"
         );
         assert!(
-            out.contains("alias b \"make build\""),
+            out.contains("function b\n    make build $argv\nend"),
             "b must be reloaded: {out}"
         );
         // env-var update must be the last section
         let env_pos = out.find("_AM_ALIASES").expect("env update missing");
-        let alias_pos = out.find("alias b").unwrap();
-        assert!(env_pos > alias_pos, "env update must come after loads");
+        let fn_pos = out.find("function b").unwrap();
+        assert!(env_pos > fn_pos, "env update must come after loads");
     }
 
     #[test]
diff --git a/crates/am/src/shell/fish.rs b/crates/am/src/shell/fish.rs
index 76e8ecaf..f5a55124 100644
--- a/crates/am/src/shell/fish.rs
+++ b/crates/am/src/shell/fish.rs
@@ -127,11 +127,15 @@ impl ShellAdapter for Fish {
     }
 
     fn alias(&self, entry: &AliasEntry) -> String {
-        // Fish's `alias` builtin stores `--wraps` as a completion entry via
-        // `complete --wraps`, which persists independently of `functions -e`.
-        // Redefining an alias without clearing that completion stacks the
-        // `--wraps=` list. Erase both the function and its completion wraps
-        // before redefining to guarantee a clean single `--wraps`.
+        // Emit a plain `function` instead of going through fish's `alias`
+        // builtin. Fish's `alias` records `--wraps` via the completion
+        // system, and that entry survives `functions -e`; redefining the
+        // same alias stacks `--wraps=` flags. Using `function` directly
+        // with no `--wraps` avoids the issue entirely (at the cost of
+        // completion inheritance, which was unreliable anyway for aliases
+        // whose command is a pipe/chain). `functions -e` still prefixes to
+        // keep the redefinition clean, and `complete -e -c NAME` clears any
+        // `--wraps` left over from prior amoxide versions that used `alias`.
         let prelude = format!(
             "functions -e {name}\ncomplete -e -c {name}",
             name = entry.name
@@ -146,9 +150,9 @@ impl ShellAdapter for Fish {
             format!("abbr --add {} {}", entry.name, quote_cmd(entry.command))
         } else {
             format!(
-                "{prelude}\nalias {name} {cmd}",
+                "{prelude}\nfunction {name}\n    {cmd} $argv\nend",
                 name = entry.name,
-                cmd = quote_cmd(entry.command)
+                cmd = entry.command,
             )
         }
     }
@@ -256,11 +260,11 @@ mod tests {
     fn test_fish_simple_alias() {
         assert_eq!(
             Fish::default().alias(&simple("h", "'echo hello'")),
-            "functions -e h\ncomplete -e -c h\nalias h 'echo hello'"
+            "functions -e h\ncomplete -e -c h\nfunction h\n    'echo hello' $argv\nend"
         );
         assert_eq!(
             Fish::default().alias(&simple("h", "echo hello")),
-            "functions -e h\ncomplete -e -c h\nalias h \"echo hello\""
+            "functions -e h\ncomplete -e -c h\nfunction h\n    echo hello $argv\nend"
         );
     }
 
@@ -280,7 +284,7 @@ mod tests {
     fn test_fish_raw_skips_templates() {
         assert_eq!(
             Fish::default().alias(&raw("my-awk", "awk '{print {{1}}}'")),
-            "functions -e my-awk\ncomplete -e -c my-awk\nalias my-awk \"awk '{print {{1}}}'\""
+            "functions -e my-awk\ncomplete -e -c my-awk\nfunction my-awk\n    awk '{print {{1}}}' $argv\nend"
         );
     }
 
diff --git a/crates/am/tests/snapshots.rs b/crates/am/tests/snapshots.rs
index f3c1fa1d..d8bf5451 100644
--- a/crates/am/tests/snapshots.rs
+++ b/crates/am/tests/snapshots.rs
@@ -680,7 +680,7 @@ fn sync_fresh_load_emits_aliases_and_env_var() {
         .resolve();
     let shell = Shell::Fish.as_shell(&Default::default(), Default::default(), Default::default());
     let out = render_diff(&diff, shell.as_ref());
-    assert!(out.contains("alias gs \"git status\""));
+    assert!(out.contains("function gs\n    git status $argv\nend"));
     assert!(out.contains("_AM_ALIASES"));
     assert!(out.contains("gs|"));
 }
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap
index 24468161..f1d9994e 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap
@@ -4,13 +4,19 @@ expression: output
 ---
 functions -e ct
 complete -e -c ct
-alias ct "cargo test"
+function ct
+    cargo test $argv
+end
 functions -e gs
 complete -e -c gs
-alias gs "git status"
+function gs
+    git status $argv
+end
 functions -e ll
 complete -e -c ll
-alias ll "ls -lha"
+function ll
+    ls -lha $argv
+end
 set -gx _AM_ALIASES "ct|ab61de4,gs|22db469,ll|619d266"
 # am wrapper: sync after mutations
 function am --wraps=am
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap
index a3d1d6cf..791f0508 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap
@@ -4,7 +4,9 @@ expression: output
 ---
 functions -e cm
 complete -e -c cm
-alias cm "git commit -sm"
+function cm
+    git commit -sm $argv
+end
 functions -e cmf
 complete -e -c cmf
 function cmf
@@ -12,10 +14,14 @@ function cmf
 end
 functions -e gs
 complete -e -c gs
-alias gs "git status"
+function gs
+    git status $argv
+end
 functions -e ll
 complete -e -c ll
-alias ll "ls -lha"
+function ll
+    ls -lha $argv
+end
 set -gx _AM_ALIASES "cm|bed528f,cmf|9c99949,gs|22db469,ll|619d266"
 # am wrapper: sync after mutations
 function am --wraps=am
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap
index 2cf38dbf..ad3b016a 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap
@@ -4,7 +4,9 @@ expression: output
 ---
 functions -e cm
 complete -e -c cm
-alias cm "git commit -sm"
+function cm
+    git commit -sm $argv
+end
 functions -e cmf
 complete -e -c cmf
 function cmf
@@ -12,7 +14,9 @@ function cmf
 end
 functions -e gs
 complete -e -c gs
-alias gs "git status"
+function gs
+    git status $argv
+end
 set -gx _AM_ALIASES "cm|bed528f,cmf|9c99949,gs|22db469"
 # am wrapper: sync after mutations
 function am --wraps=am
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap
index 5af16f07..a6bf63d8 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap
@@ -4,10 +4,14 @@ expression: output
 ---
 functions -e gs
 complete -e -c gs
-alias gs "git status"
+function gs
+    git status $argv
+end
 functions -e ll
 complete -e -c ll
-alias ll "ls -lha"
+function ll
+    ls -lha $argv
+end
 set -gx _AM_ALIASES "gs|22db469,ll|619d266"
 # am wrapper: sync after mutations
 function am --wraps=am
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap
index 1b69a147..2b805b8d 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap
@@ -4,10 +4,14 @@ expression: output
 ---
 functions -e ct
 complete -e -c ct
-alias ct "cargo test"
+function ct
+    cargo test $argv
+end
 functions -e ll
 complete -e -c ll
-alias ll "ls -lha"
+function ll
+    ls -lha $argv
+end
 set -gx _AM_ALIASES "ct|ab61de4,ll|619d266"
 # am wrapper: sync after mutations
 function am --wraps=am
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap
index f0cc9d30..6c22f27d 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap
@@ -4,7 +4,9 @@ expression: output
 ---
 functions -e gs
 complete -e -c gs
-alias gs "git status"
+function gs
+    git status $argv
+end
 function jj --wraps=jj
   switch $argv[1]
     case 'ab'
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_fresh_load_project_only.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_fresh_load_project_only.snap
index cf7b72c7..1ca9b073 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_fresh_load_project_only.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_fresh_load_project_only.snap
@@ -4,8 +4,12 @@ expression: output
 ---
 functions -e b
 complete -e -c b
-alias b "cargo build"
+function b
+    cargo build $argv
+end
 functions -e t
 complete -e -c t
-alias t "cargo test"
+function t
+    cargo test $argv
+end
 set -gx _AM_ALIASES "b|b58de66,t|ab61de4"
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_incremental_one_alias_updated.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_incremental_one_alias_updated.snap
index b2b07e23..f6f1fb35 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_incremental_one_alias_updated.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_incremental_one_alias_updated.snap
@@ -5,5 +5,7 @@ expression: output
 functions -e b
 functions -e b
 complete -e -c b
-alias b "cargo build --release"
+function b
+    cargo build --release $argv
+end
 set -gx _AM_ALIASES "b|0dc3caa,t|ab61de4"
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_leaving_project_with_shadow_restoration.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_leaving_project_with_shadow_restoration.snap
index 92a92ef8..0e5bc851 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_leaving_project_with_shadow_restoration.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_leaving_project_with_shadow_restoration.snap
@@ -6,5 +6,7 @@ functions -e b
 functions -e t
 functions -e t
 complete -e -c t
-alias t "cargo test"
+function t
+    cargo test $argv
+end
 set -gx _AM_ALIASES "t|ab61de4,ll|619d266"
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_transition_to_new_project.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_transition_to_new_project.snap
index 52980f27..c57007e5 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_transition_to_new_project.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_transition_to_new_project.snap
@@ -5,5 +5,7 @@ expression: output
 functions -e old1
 functions -e new1
 complete -e -c new1
-alias new1 "echo new"
+function new1
+    echo new $argv
+end
 set -gx _AM_ALIASES "new1|4d1fcee"

From eb1561aa492d94ec0a89250f0642bcbbe23da0d7 Mon Sep 17 00:00:00 2001
From: Sven Kanoldt 
Date: Wed, 22 Apr 2026 23:44:48 +0200
Subject: [PATCH 28/38] fix: restore fish completion inheritance without
 --wraps stacking

Follow-up to 84bd8a75 which dropped --wraps entirely. Bring back completion
inheritance by emitting a separate 'complete -c NAME --wraps CMD' line after
the function body, instead of on the function signature.

Why this works where the prior approach failed:
- fish's 'alias' builtin put '--wraps=' directly in the
  function signature. On redefinition, fish accumulates --wraps= flags
  there (and 'type' displays them all).
- A plain 'function NAME' has no --wraps in its signature, so 'type' stays
  clean (just the body).
- 'complete -c NAME --wraps CMD' registers inheritance in the completion
  system; our prelude 'complete -e -c NAME' wipes the slate before each
  emission so no stacking.
- The wrap target is the first whitespace-separated token of the command
  (the actual binary), which is what the completion system expects -
  rather than the full command with args/pipes/chains.

- 'type i' now shows: function i\n    cmd $argv\nend (clean, no --wraps)
- tab completion for 'i ' works as if tab-completing for 'cargo'
- regenerated 10 fish snapshots + 4 unit tests
---
 crates/am/src/shell/fish.rs                   | 54 ++++++++++++-------
 ...pshots__snapshot_init_fish_deep_chain.snap |  3 ++
 ...t_init_fish_globals_and_multi_profile.snap |  4 ++
 ...ots__snapshot_init_fish_multi_profile.snap |  3 ++
 ...ts__snapshot_init_fish_simple_profile.snap |  2 +
 ...hots__snapshot_init_fish_with_globals.snap |  2 +
 ...hot_init_fish_with_simple_subcommands.snap |  1 +
 ...hot_sync_fish_fresh_load_project_only.snap |  2 +
 ...nc_fish_incremental_one_alias_updated.snap |  1 +
 ...aving_project_with_shadow_restoration.snap |  1 +
 ...t_sync_fish_transition_to_new_project.snap |  1 +
 11 files changed, 56 insertions(+), 18 deletions(-)

diff --git a/crates/am/src/shell/fish.rs b/crates/am/src/shell/fish.rs
index f5a55124..b9df718e 100644
--- a/crates/am/src/shell/fish.rs
+++ b/crates/am/src/shell/fish.rs
@@ -127,32 +127,50 @@ impl ShellAdapter for Fish {
     }
 
     fn alias(&self, entry: &AliasEntry) -> String {
-        // Emit a plain `function` instead of going through fish's `alias`
-        // builtin. Fish's `alias` records `--wraps` via the completion
-        // system, and that entry survives `functions -e`; redefining the
-        // same alias stacks `--wraps=` flags. Using `function` directly
-        // with no `--wraps` avoids the issue entirely (at the cost of
-        // completion inheritance, which was unreliable anyway for aliases
-        // whose command is a pipe/chain). `functions -e` still prefixes to
-        // keep the redefinition clean, and `complete -e -c NAME` clears any
-        // `--wraps` left over from prior amoxide versions that used `alias`.
+        // Emit a plain `function` and register completion inheritance via a
+        // separate `complete -c NAME --wraps CMD` call, rather than going
+        // through fish's `alias` builtin. Reasons:
+        //   - fish's `alias` puts `--wraps=` directly in the
+        //     function signature, which shows up in `type NAME` output and
+        //     stacks on redefinition (the `--wraps=` entries accumulate).
+        //   - `function NAME` alone has a clean signature.
+        //   - `complete -c NAME --wraps` registers inheritance in the
+        //     completion system, which the prelude's `complete -e -c NAME`
+        //     wipes before each emission, so no stacking.
+        //   - We wrap the first whitespace-separated token of the command —
+        //     the actual wrapped binary — rather than the full argv, which
+        //     is what fish's completion system expects.
         let prelude = format!(
             "functions -e {name}\ncomplete -e -c {name}",
             name = entry.name
         );
+        let wrap_target = entry
+            .command
+            .split_whitespace()
+            .next()
+            .filter(|s| !s.is_empty());
+        let wraps_line = wrap_target.map(|w| {
+            format!(
+                "\ncomplete -c {name} --wraps {cmd}",
+                name = entry.name,
+                cmd = quote_cmd(w),
+            )
+        });
         if !entry.raw && has_template_args(entry.command) {
             let body = substitute_fish(entry.command);
             format!(
-                "{prelude}\nfunction {name}\n    {body}\nend",
-                name = entry.name
+                "{prelude}\nfunction {name}\n    {body}\nend{wraps}",
+                name = entry.name,
+                wraps = wraps_line.as_deref().unwrap_or(""),
             )
         } else if self.use_abbr {
             format!("abbr --add {} {}", entry.name, quote_cmd(entry.command))
         } else {
             format!(
-                "{prelude}\nfunction {name}\n    {cmd} $argv\nend",
+                "{prelude}\nfunction {name}\n    {cmd} $argv\nend{wraps}",
                 name = entry.name,
                 cmd = entry.command,
+                wraps = wraps_line.as_deref().unwrap_or(""),
             )
         }
     }
@@ -260,11 +278,11 @@ mod tests {
     fn test_fish_simple_alias() {
         assert_eq!(
             Fish::default().alias(&simple("h", "'echo hello'")),
-            "functions -e h\ncomplete -e -c h\nfunction h\n    'echo hello' $argv\nend"
+            "functions -e h\ncomplete -e -c h\nfunction h\n    'echo hello' $argv\nend\ncomplete -c h --wraps \"'echo\""
         );
         assert_eq!(
             Fish::default().alias(&simple("h", "echo hello")),
-            "functions -e h\ncomplete -e -c h\nfunction h\n    echo hello $argv\nend"
+            "functions -e h\ncomplete -e -c h\nfunction h\n    echo hello $argv\nend\ncomplete -c h --wraps \"echo\""
         );
     }
 
@@ -272,11 +290,11 @@ mod tests {
     fn test_fish_parameterized() {
         assert_eq!(
             Fish::default().alias(&simple("cmf", "cm feat: {{@}}")),
-            "functions -e cmf\ncomplete -e -c cmf\nfunction cmf\n    cm feat: $argv\nend"
+            "functions -e cmf\ncomplete -e -c cmf\nfunction cmf\n    cm feat: $argv\nend\ncomplete -c cmf --wraps \"cm\""
         );
         assert_eq!(
             Fish::default().alias(&simple("x", "echo {{1}} and {{2}}")),
-            "functions -e x\ncomplete -e -c x\nfunction x\n    echo $argv[1] and $argv[2]\nend"
+            "functions -e x\ncomplete -e -c x\nfunction x\n    echo $argv[1] and $argv[2]\nend\ncomplete -c x --wraps \"echo\""
         );
     }
 
@@ -284,7 +302,7 @@ mod tests {
     fn test_fish_raw_skips_templates() {
         assert_eq!(
             Fish::default().alias(&raw("my-awk", "awk '{print {{1}}}'")),
-            "functions -e my-awk\ncomplete -e -c my-awk\nfunction my-awk\n    awk '{print {{1}}}' $argv\nend"
+            "functions -e my-awk\ncomplete -e -c my-awk\nfunction my-awk\n    awk '{print {{1}}}' $argv\nend\ncomplete -c my-awk --wraps \"awk\""
         );
     }
 
@@ -432,7 +450,7 @@ mod tests {
         let fish = Fish { use_abbr: true };
         assert_eq!(
             fish.alias(&simple("cmf", "cm feat: {{@}}")),
-            "functions -e cmf\ncomplete -e -c cmf\nfunction cmf\n    cm feat: $argv\nend"
+            "functions -e cmf\ncomplete -e -c cmf\nfunction cmf\n    cm feat: $argv\nend\ncomplete -c cmf --wraps \"cm\""
         );
     }
 
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap
index f1d9994e..f0822361 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap
@@ -7,16 +7,19 @@ complete -e -c ct
 function ct
     cargo test $argv
 end
+complete -c ct --wraps "cargo"
 functions -e gs
 complete -e -c gs
 function gs
     git status $argv
 end
+complete -c gs --wraps "git"
 functions -e ll
 complete -e -c ll
 function ll
     ls -lha $argv
 end
+complete -c ll --wraps "ls"
 set -gx _AM_ALIASES "ct|ab61de4,gs|22db469,ll|619d266"
 # am wrapper: sync after mutations
 function am --wraps=am
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap
index 791f0508..6393b261 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap
@@ -7,21 +7,25 @@ complete -e -c cm
 function cm
     git commit -sm $argv
 end
+complete -c cm --wraps "git"
 functions -e cmf
 complete -e -c cmf
 function cmf
     cm feat: $argv
 end
+complete -c cmf --wraps "cm"
 functions -e gs
 complete -e -c gs
 function gs
     git status $argv
 end
+complete -c gs --wraps "git"
 functions -e ll
 complete -e -c ll
 function ll
     ls -lha $argv
 end
+complete -c ll --wraps "ls"
 set -gx _AM_ALIASES "cm|bed528f,cmf|9c99949,gs|22db469,ll|619d266"
 # am wrapper: sync after mutations
 function am --wraps=am
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap
index ad3b016a..0a54fbe8 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap
@@ -7,16 +7,19 @@ complete -e -c cm
 function cm
     git commit -sm $argv
 end
+complete -c cm --wraps "git"
 functions -e cmf
 complete -e -c cmf
 function cmf
     cm feat: $argv
 end
+complete -c cmf --wraps "cm"
 functions -e gs
 complete -e -c gs
 function gs
     git status $argv
 end
+complete -c gs --wraps "git"
 set -gx _AM_ALIASES "cm|bed528f,cmf|9c99949,gs|22db469"
 # am wrapper: sync after mutations
 function am --wraps=am
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap
index a6bf63d8..294d5f0a 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap
@@ -7,11 +7,13 @@ complete -e -c gs
 function gs
     git status $argv
 end
+complete -c gs --wraps "git"
 functions -e ll
 complete -e -c ll
 function ll
     ls -lha $argv
 end
+complete -c ll --wraps "ls"
 set -gx _AM_ALIASES "gs|22db469,ll|619d266"
 # am wrapper: sync after mutations
 function am --wraps=am
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap
index 2b805b8d..1e3176ea 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap
@@ -7,11 +7,13 @@ complete -e -c ct
 function ct
     cargo test $argv
 end
+complete -c ct --wraps "cargo"
 functions -e ll
 complete -e -c ll
 function ll
     ls -lha $argv
 end
+complete -c ll --wraps "ls"
 set -gx _AM_ALIASES "ct|ab61de4,ll|619d266"
 # am wrapper: sync after mutations
 function am --wraps=am
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap
index 6c22f27d..bde09557 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap
@@ -7,6 +7,7 @@ complete -e -c gs
 function gs
     git status $argv
 end
+complete -c gs --wraps "git"
 function jj --wraps=jj
   switch $argv[1]
     case 'ab'
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_fresh_load_project_only.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_fresh_load_project_only.snap
index 1ca9b073..9a11d91f 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_fresh_load_project_only.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_fresh_load_project_only.snap
@@ -7,9 +7,11 @@ complete -e -c b
 function b
     cargo build $argv
 end
+complete -c b --wraps "cargo"
 functions -e t
 complete -e -c t
 function t
     cargo test $argv
 end
+complete -c t --wraps "cargo"
 set -gx _AM_ALIASES "b|b58de66,t|ab61de4"
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_incremental_one_alias_updated.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_incremental_one_alias_updated.snap
index f6f1fb35..578ffeb1 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_incremental_one_alias_updated.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_incremental_one_alias_updated.snap
@@ -8,4 +8,5 @@ complete -e -c b
 function b
     cargo build --release $argv
 end
+complete -c b --wraps "cargo"
 set -gx _AM_ALIASES "b|0dc3caa,t|ab61de4"
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_leaving_project_with_shadow_restoration.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_leaving_project_with_shadow_restoration.snap
index 0e5bc851..613e869e 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_leaving_project_with_shadow_restoration.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_leaving_project_with_shadow_restoration.snap
@@ -9,4 +9,5 @@ complete -e -c t
 function t
     cargo test $argv
 end
+complete -c t --wraps "cargo"
 set -gx _AM_ALIASES "t|ab61de4,ll|619d266"
diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_transition_to_new_project.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_transition_to_new_project.snap
index c57007e5..0aadfe84 100644
--- a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_transition_to_new_project.snap
+++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_transition_to_new_project.snap
@@ -8,4 +8,5 @@ complete -e -c new1
 function new1
     echo new $argv
 end
+complete -c new1 --wraps "echo"
 set -gx _AM_ALIASES "new1|4d1fcee"

From d4ddb972d4b983243b41102f88fe39b11f856af1 Mon Sep 17 00:00:00 2001
From: Sven Kanoldt 
Date: Thu, 23 Apr 2026 09:11:55 +0200
Subject: [PATCH 29/38] refactor: wrap SubcommandSet in a newtype with minimal
 API surface
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Mirror AliasSet's shape but keep the surface smaller: SubcommandSet now
exposes only `new`, `is_empty` (needed by serde skip_serializing_if), and
IntoIterator. Map-ish operations (`insert`, `remove`, `get`, `contains_key`,
`len`, `iter`, `keys`, `values`, indexing) go through `.as_ref()` or
`.as_mut()` — the escape hatch is explicit rather than delegated.

- newtype with serde(transparent) so TOML layout is unchanged
- AsRef> and AsMut>
- IntoIterator for &SubcommandSet and SubcommandSet
- FromIterator for tuple-iterator collection
- 3 new unit tests covering the newtype's API
- all call sites across both crates (lib + tests) route through
  as_ref/as_mut where needed
---
 crates/am-tui/src/tree.rs                   |   3 +-
 crates/am-tui/src/update/mod.rs             |  17 ++-
 crates/am-tui/src/update/profile_actions.rs |   2 +
 crates/am-tui/src/update/transfer.rs        |  11 +-
 crates/am-tui/src/view.rs                   |   1 +
 crates/am/src/app_model.rs                  |   2 +-
 crates/am/src/bin/am.rs                     |   2 +-
 crates/am/src/config.rs                     |  10 +-
 crates/am/src/display.rs                    |   5 +-
 crates/am/src/exchange.rs                   |  62 ++++++---
 crates/am/src/import_export.rs              |   8 +-
 crates/am/src/init.rs                       |   2 +-
 crates/am/src/precedence.rs                 |  13 +-
 crates/am/src/profile.rs                    |   7 +-
 crates/am/src/project.rs                    |   9 +-
 crates/am/src/subcommand.rs                 | 136 ++++++++++++++++++--
 crates/am/src/update.rs                     |  45 ++++---
 crates/am/tests/snapshots.rs                |  26 ++--
 18 files changed, 272 insertions(+), 89 deletions(-)

diff --git a/crates/am-tui/src/tree.rs b/crates/am-tui/src/tree.rs
index fbe6f864..c3342d6c 100644
--- a/crates/am-tui/src/tree.rs
+++ b/crates/am-tui/src/tree.rs
@@ -130,7 +130,7 @@ fn emit_trie_children(
 
 fn collect_prog_names(subcommands: &amoxide::SubcommandSet) -> Vec {
     let mut names = std::collections::BTreeSet::new();
-    for key in subcommands.keys() {
+    for key in subcommands.as_ref().keys() {
         if let Some(prog) = key.split(':').next() {
             names.insert(prog.to_string());
         }
@@ -597,6 +597,7 @@ mod tests {
         fn global_subcommand(mut self, key: &str, longs: &[&str]) -> Self {
             self.config
                 .subcommands
+                .as_mut()
                 .insert(key.into(), longs.iter().map(|s| s.to_string()).collect());
             self
         }
diff --git a/crates/am-tui/src/update/mod.rs b/crates/am-tui/src/update/mod.rs
index 2ed73842..d2b3edf2 100644
--- a/crates/am-tui/src/update/mod.rs
+++ b/crates/am-tui/src/update/mod.rs
@@ -826,6 +826,7 @@ mod tests {
             .app_model
             .config
             .subcommands
+            .as_mut()
             .insert("jj:ab".into(), vec!["abandon".into()]);
         model.rebuild_tree();
         let idx = model
@@ -1007,13 +1008,18 @@ mod tests {
         });
         update(&mut model, TuiMessage::TextInputConfirm);
         assert_eq!(model.mode, Mode::Normal);
-        assert!(model.app_model.config.subcommands.contains_key("jj:ab"));
+        assert!(model
+            .app_model
+            .config
+            .subcommands
+            .as_ref()
+            .contains_key("jj:ab"));
     }
 
     fn make_subcmd_model(keys: &[(&str, &[&str])]) -> TuiModel {
         let mut config = amoxide::Config::default();
         for (key, longs) in keys {
-            config.subcommands.insert(
+            config.subcommands.as_mut().insert(
                 key.to_string(),
                 longs.iter().map(|s| s.to_string()).collect(),
             );
@@ -1057,7 +1063,12 @@ mod tests {
             .unwrap();
         model.cursor = idx;
         update(&mut model, TuiMessage::DeleteItem);
-        assert!(!model.app_model.config.subcommands.contains_key("jj:ab"));
+        assert!(!model
+            .app_model
+            .config
+            .subcommands
+            .as_ref()
+            .contains_key("jj:ab"));
     }
 
     #[test]
diff --git a/crates/am-tui/src/update/profile_actions.rs b/crates/am-tui/src/update/profile_actions.rs
index bb04cfc9..a3059bc2 100644
--- a/crates/am-tui/src/update/profile_actions.rs
+++ b/crates/am-tui/src/update/profile_actions.rs
@@ -49,6 +49,7 @@ pub fn handle(model: &mut TuiModel, msg: TuiMessage) {
                     let keys_to_remove: Vec = {
                         let lib_target = derive_target_from_cursor(model);
                         get_subcommand_set(model, &lib_target)
+                            .as_ref()
                             .keys()
                             .filter(|k| k.starts_with(&prefix))
                             .cloned()
@@ -75,6 +76,7 @@ pub fn handle(model: &mut TuiModel, msg: TuiMessage) {
                     let keys_to_remove: Vec = {
                         let lib_target = derive_target_from_cursor(model);
                         get_subcommand_set(model, &lib_target)
+                            .as_ref()
                             .keys()
                             .filter(|k| k.starts_with(&prog_prefix))
                             .cloned()
diff --git a/crates/am-tui/src/update/transfer.rs b/crates/am-tui/src/update/transfer.rs
index 351dc31d..f46b6751 100644
--- a/crates/am-tui/src/update/transfer.rs
+++ b/crates/am-tui/src/update/transfer.rs
@@ -216,16 +216,21 @@ pub(super) fn is_same_source(id: &AliasId, dest: &MoveDestination) -> bool {
 pub(super) fn alias_exists_at_dest(model: &TuiModel, id: &AliasId, dest: &MoveDestination) -> bool {
     match id {
         AliasId::Subcommand { key, .. } => match dest {
-            MoveDestination::Global => model.app_model.config.subcommands.contains_key(key),
+            MoveDestination::Global => model
+                .app_model
+                .config
+                .subcommands
+                .as_ref()
+                .contains_key(key),
             MoveDestination::Project => model
                 .app_model
                 .project_aliases()
-                .is_some_and(|p| p.subcommands.contains_key(key)),
+                .is_some_and(|p| p.subcommands.as_ref().contains_key(key)),
             MoveDestination::Profile(name) => model
                 .app_model
                 .profile_config()
                 .get_profile_by_name(name)
-                .is_some_and(|p| p.subcommands.contains_key(key)),
+                .is_some_and(|p| p.subcommands.as_ref().contains_key(key)),
         },
         _ => {
             let alias_name_str = match id {
diff --git a/crates/am-tui/src/view.rs b/crates/am-tui/src/view.rs
index 768c2fb5..c0a3e677 100644
--- a/crates/am-tui/src/view.rs
+++ b/crates/am-tui/src/view.rs
@@ -866,6 +866,7 @@ mod subcommand_render {
         let mut config = Config::default();
         config
             .subcommands
+            .as_mut()
             .insert("jj:ab".into(), vec!["abandon".into()]);
         let app = amoxide::update::AppModel::new(config, ProfileConfig::default());
         let mut model = TuiModel::new().unwrap();
diff --git a/crates/am/src/app_model.rs b/crates/am/src/app_model.rs
index fa78fdae..d1d621b2 100644
--- a/crates/am/src/app_model.rs
+++ b/crates/am/src/app_model.rs
@@ -262,7 +262,7 @@ impl AppModel {
         let path = self.project_path_or_create();
         let mut project = self.project_aliases().cloned().unwrap_or_default();
         for (key, longs) in subcommands {
-            project.subcommands.insert(key, longs);
+            project.subcommands.as_mut().insert(key, longs);
         }
         project.save(&path)?;
         let hash = compute_file_hash(&path)?;
diff --git a/crates/am/src/bin/am.rs b/crates/am/src/bin/am.rs
index d86500d4..fbf987d3 100644
--- a/crates/am/src/bin/am.rs
+++ b/crates/am/src/bin/am.rs
@@ -216,7 +216,7 @@ fn main() -> anyhow::Result<()> {
                         .ok_or_else(|| anyhow::anyhow!("Profile '{name}' not found"))?;
                     if !profile.is_empty() {
                         let alias_count = profile.aliases.iter().count();
-                        let subcmd_count = profile.subcommands.len();
+                        let subcmd_count = profile.subcommands.as_ref().len();
                         let question = match (alias_count, subcmd_count) {
                             (a, 0) => format!(
                                 "Profile '{name}' has {a} alias{}. Remove?",
diff --git a/crates/am/src/config.rs b/crates/am/src/config.rs
index c3b1b2dd..a50b9681 100644
--- a/crates/am/src/config.rs
+++ b/crates/am/src/config.rs
@@ -86,11 +86,12 @@ impl Config {
     }
 
     pub fn add_subcommand(&mut self, key: String, long_subcommands: Vec) {
-        self.subcommands.insert(key, long_subcommands);
+        self.subcommands.as_mut().insert(key, long_subcommands);
     }
 
     pub fn remove_subcommand(&mut self, key: &str) -> crate::Result<()> {
         self.subcommands
+            .as_mut()
             .remove(key)
             .ok_or_else(|| anyhow::anyhow!("Subcommand alias '{key}' not found"))?;
         Ok(())
@@ -164,19 +165,20 @@ mod tests {
         let mut config = Config::default();
         config
             .subcommands
+            .as_mut()
             .insert("jj:ab".into(), vec!["abandon".into()]);
         config.save_to(dir.path()).unwrap();
 
         let loaded = Config::load_from(dir.path()).unwrap();
-        assert_eq!(loaded.subcommands.len(), 1);
-        assert_eq!(loaded.subcommands["jj:ab"], vec!["abandon"]);
+        assert_eq!(loaded.subcommands.as_ref().len(), 1);
+        assert_eq!(loaded.subcommands.as_ref()["jj:ab"], vec!["abandon"]);
     }
 
     #[test]
     fn test_add_and_remove_subcommand() {
         let mut config = Config::default();
         config.add_subcommand("jj:ab".into(), vec!["abandon".into()]);
-        assert_eq!(config.subcommands.len(), 1);
+        assert_eq!(config.subcommands.as_ref().len(), 1);
 
         config.remove_subcommand("jj:ab").unwrap();
         assert!(config.subcommands.is_empty());
diff --git a/crates/am/src/display.rs b/crates/am/src/display.rs
index cdec6d24..e08fc51e 100644
--- a/crates/am/src/display.rs
+++ b/crates/am/src/display.rs
@@ -594,8 +594,9 @@ mod tests {
 
         let config: ProfileConfig = ProfileConfig::default();
         let mut subs = SubcommandSet::new();
-        subs.insert("jj:ab".into(), vec!["abandon".into()]);
-        subs.insert("jj:b:l".into(), vec!["branch".into(), "list".into()]);
+        subs.as_mut().insert("jj:ab".into(), vec!["abandon".into()]);
+        subs.as_mut()
+            .insert("jj:b:l".into(), vec!["branch".into(), "list".into()]);
 
         let output = render_listing(&AliasSet::default(), &subs, &config, &[], None, None);
         assert!(output.contains("jj (subcommands)"));
diff --git a/crates/am/src/exchange.rs b/crates/am/src/exchange.rs
index e5ca6bf3..4c989a1b 100644
--- a/crates/am/src/exchange.rs
+++ b/crates/am/src/exchange.rs
@@ -54,15 +54,15 @@ impl ExportAll {
     pub fn flatten_subcommands(&self) -> SubcommandSet {
         let mut result = SubcommandSet::new();
         for (k, v) in &self.global_subcommands {
-            result.insert(k.clone(), v.clone());
+            result.as_mut().insert(k.clone(), v.clone());
         }
         for profile in &self.profiles {
             for (k, v) in &profile.subcommands {
-                result.insert(k.clone(), v.clone());
+                result.as_mut().insert(k.clone(), v.clone());
             }
         }
         for (k, v) in &self.local_subcommands {
-            result.insert(k.clone(), v.clone());
+            result.as_mut().insert(k.clone(), v.clone());
         }
         result
     }
@@ -103,9 +103,11 @@ pub fn subcommand_merge_check(
     let mut new_subcommands = SubcommandSet::new();
     let mut conflicts = Vec::new();
     for (key, incoming_longs) in incoming {
-        match current.get(key) {
+        match current.as_ref().get(key) {
             None => {
-                new_subcommands.insert(key.clone(), incoming_longs.clone());
+                new_subcommands
+                    .as_mut()
+                    .insert(key.clone(), incoming_longs.clone());
             }
             Some(existing_longs) => {
                 if existing_longs != incoming_longs {
@@ -183,7 +185,7 @@ pub fn render_import_summary_subcommands(
     scope_name: &str,
     result: &SubcommandMergeResult,
 ) -> String {
-    let total = result.new_subcommands.len() + result.conflicts.len();
+    let total = result.new_subcommands.as_ref().len() + result.conflicts.len();
     let mut output = format!("Importing subcommands into \"{scope_name}\" ({total} entries)\n");
 
     if !result.new_subcommands.is_empty() {
@@ -872,25 +874,27 @@ mod tests {
         let mut export = ExportAll::default();
         export
             .global_subcommands
+            .as_mut()
             .insert("jj:ab".into(), vec!["abandon".into()]);
         export.profiles.push(Profile {
             name: "vcs".into(),
             aliases: AliasSet::default(),
             subcommands: {
                 let mut s = SubcommandSet::new();
-                s.insert("jj:d".into(), vec!["diff".into()]);
+                s.as_mut().insert("jj:d".into(), vec!["diff".into()]);
                 s
             },
         });
         export
             .local_subcommands
+            .as_mut()
             .insert("git:psh".into(), vec!["push".into()]);
 
         let flat = export.flatten_subcommands();
-        assert_eq!(flat.len(), 3);
-        assert!(flat.contains_key("jj:ab"));
-        assert!(flat.contains_key("jj:d"));
-        assert!(flat.contains_key("git:psh"));
+        assert_eq!(flat.as_ref().len(), 3);
+        assert!(flat.as_ref().contains_key("jj:ab"));
+        assert!(flat.as_ref().contains_key("jj:d"));
+        assert!(flat.as_ref().contains_key("git:psh"));
     }
 
     #[test]
@@ -898,14 +902,15 @@ mod tests {
         let mut export = ExportAll::default();
         export
             .global_subcommands
+            .as_mut()
             .insert("jj:ab".into(), vec!["abandon".into()]);
-        export.local_subcommands.insert(
+        export.local_subcommands.as_mut().insert(
             "jj:ab".into(),
             vec!["abandon", "!"].into_iter().map(String::from).collect(),
         );
 
         let flat = export.flatten_subcommands();
-        assert_eq!(flat["jj:ab"], vec!["abandon", "!"]);
+        assert_eq!(flat.as_ref()["jj:ab"], vec!["abandon", "!"]);
     }
 
     // ─── subcommand_merge_check ──────────────────────────────────────────
@@ -914,19 +919,23 @@ mod tests {
     fn test_merge_check_new_entries() {
         let current = SubcommandSet::new();
         let mut incoming = SubcommandSet::new();
-        incoming.insert("jj:ab".into(), vec!["abandon".into()]);
+        incoming
+            .as_mut()
+            .insert("jj:ab".into(), vec!["abandon".into()]);
 
         let result = subcommand_merge_check(¤t, &incoming);
-        assert_eq!(result.new_subcommands.len(), 1);
+        assert_eq!(result.new_subcommands.as_ref().len(), 1);
         assert!(result.conflicts.is_empty());
     }
 
     #[test]
     fn test_merge_check_conflict() {
         let mut current = SubcommandSet::new();
-        current.insert("jj:ab".into(), vec!["abandon".into()]);
+        current
+            .as_mut()
+            .insert("jj:ab".into(), vec!["abandon".into()]);
         let mut incoming = SubcommandSet::new();
-        incoming.insert(
+        incoming.as_mut().insert(
             "jj:ab".into(),
             vec!["abandon", "--detach"]
                 .into_iter()
@@ -943,9 +952,13 @@ mod tests {
     #[test]
     fn test_merge_check_identical_entry_skipped() {
         let mut current = SubcommandSet::new();
-        current.insert("jj:ab".into(), vec!["abandon".into()]);
+        current
+            .as_mut()
+            .insert("jj:ab".into(), vec!["abandon".into()]);
         let mut incoming = SubcommandSet::new();
-        incoming.insert("jj:ab".into(), vec!["abandon".into()]);
+        incoming
+            .as_mut()
+            .insert("jj:ab".into(), vec!["abandon".into()]);
 
         let result = subcommand_merge_check(¤t, &incoming);
         assert!(result.new_subcommands.is_empty());
@@ -993,7 +1006,9 @@ mod tests {
     #[test]
     fn test_render_import_summary_subcommands_new_only() {
         let mut new_subcommands = SubcommandSet::new();
-        new_subcommands.insert("jj:ab".into(), vec!["abandon".into()]);
+        new_subcommands
+            .as_mut()
+            .insert("jj:ab".into(), vec!["abandon".into()]);
         let result = SubcommandMergeResult {
             new_subcommands,
             conflicts: vec![],
@@ -1028,6 +1043,7 @@ mod tests {
         let mut export = ExportAll::default();
         export
             .global_subcommands
+            .as_mut()
             .insert("jj:\x1Bab".into(), vec!["abandon".into()]);
         let findings = scan_suspicious(&export);
         assert_eq!(findings.len(), 1);
@@ -1040,6 +1056,7 @@ mod tests {
         let mut export = ExportAll::default();
         export
             .local_subcommands
+            .as_mut()
             .insert("jj:ab".into(), vec!["aban\x07don".into()]);
         let findings = scan_suspicious(&export);
         assert_eq!(findings.len(), 1);
@@ -1055,7 +1072,8 @@ mod tests {
                 aliases: AliasSet::default(),
                 subcommands: {
                     let mut s = SubcommandSet::new();
-                    s.insert("jj:ab".into(), vec!["aban\x1Bdon".into()]);
+                    s.as_mut()
+                        .insert("jj:ab".into(), vec!["aban\x1Bdon".into()]);
                     s
                 },
             }],
@@ -1074,6 +1092,7 @@ mod tests {
         let mut export = ExportAll::default();
         export
             .global_subcommands
+            .as_mut()
             .insert("jj:ab".into(), vec!["abandon".into()]);
         assert!(!export.is_empty());
     }
@@ -1083,6 +1102,7 @@ mod tests {
         let mut export = ExportAll::default();
         export
             .local_subcommands
+            .as_mut()
             .insert("git:psh".into(), vec!["push".into()]);
         assert!(!export.is_empty());
     }
diff --git a/crates/am/src/import_export.rs b/crates/am/src/import_export.rs
index ce4ba170..212590f0 100644
--- a/crates/am/src/import_export.rs
+++ b/crates/am/src/import_export.rs
@@ -481,11 +481,13 @@ pub fn prompt_merge_subcommands(
 
         if apply_overwrites {
             for conflict in &merge.conflicts {
-                accepted.insert(conflict.key.clone(), conflict.incoming.clone());
+                accepted
+                    .as_mut()
+                    .insert(conflict.key.clone(), conflict.incoming.clone());
             }
         }
 
-        let imported = accepted.len();
+        let imported = accepted.as_ref().len();
         let skipped = if apply_overwrites { 0 } else { n };
         eprintln!(
             "\u{2713} Imported {imported} subcommand aliases into \"{scope}\" ({skipped} skipped)"
@@ -493,7 +495,7 @@ pub fn prompt_merge_subcommands(
     } else {
         eprintln!(
             "\u{2713} Imported {} subcommand aliases into \"{scope}\"",
-            accepted.len()
+            accepted.as_ref().len()
         );
     }
 
diff --git a/crates/am/src/init.rs b/crates/am/src/init.rs
index 493d1d0d..9fa9c42f 100644
--- a/crates/am/src/init.rs
+++ b/crates/am/src/init.rs
@@ -143,7 +143,7 @@ mod tests {
 
     fn test_subcommands() -> SubcommandSet {
         let mut subs = SubcommandSet::new();
-        subs.insert("jj:ab".into(), vec!["abandon".into()]);
+        subs.as_mut().insert("jj:ab".into(), vec!["abandon".into()]);
         subs
     }
 
diff --git a/crates/am/src/precedence.rs b/crates/am/src/precedence.rs
index 2121db8a..f952e041 100644
--- a/crates/am/src/precedence.rs
+++ b/crates/am/src/precedence.rs
@@ -120,7 +120,7 @@ impl Precedence {
             &self.project_subcommands,
         ] {
             for (k, v) in layer {
-                out.insert(k.clone(), v.clone());
+                out.as_mut().insert(k.clone(), v.clone());
             }
         }
         out
@@ -132,6 +132,7 @@ impl Precedence {
 
     fn subcmd_program_hash(program: &str, subs: &SubcommandSet) -> String {
         let entries_str: String = subs
+            .as_ref()
             .iter()
             .filter(|(k, _)| k.starts_with(&format!("{program}:")))
             .map(|(k, v)| format!("{k}={}", v.join(",")))
@@ -233,7 +234,7 @@ impl Precedence {
 
         // Per-key subcommand tracking for `_AM_SUBCOMMANDS`.
         let mut effective_subkeys: BTreeMap = BTreeMap::new();
-        for (key, longs) in merged_subcommands.iter() {
+        for (key, longs) in &merged_subcommands {
             let hash = Self::subcmd_key_hash(longs);
             effective_subkeys.insert(
                 key.clone(),
@@ -432,9 +433,10 @@ mod tests {
     #[test]
     fn hash_subcmd_program_includes_all_entries_under_it() {
         let mut a = SubcommandSet::new();
-        a.insert("jj:ab".into(), vec!["abandon".into()]);
+        a.as_mut().insert("jj:ab".into(), vec!["abandon".into()]);
         let mut b = a.clone();
-        b.insert("jj:bl".into(), vec!["branch".into(), "list".into()]);
+        b.as_mut()
+            .insert("jj:bl".into(), vec!["branch".into(), "list".into()]);
 
         let h_a = Precedence::subcmd_program_hash_for_test("jj", &a);
         let h_b = Precedence::subcmd_program_hash_for_test("jj", &b);
@@ -597,7 +599,8 @@ mod tests {
     fn subset(pairs: &[(&str, &[&str])]) -> SubcommandSet {
         let mut s = SubcommandSet::new();
         for (k, longs) in pairs {
-            s.insert((*k).into(), longs.iter().map(|x| (*x).into()).collect());
+            s.as_mut()
+                .insert((*k).into(), longs.iter().map(|x| (*x).into()).collect());
         }
         s
     }
diff --git a/crates/am/src/profile.rs b/crates/am/src/profile.rs
index b8fe8345..08bf5b01 100644
--- a/crates/am/src/profile.rs
+++ b/crates/am/src/profile.rs
@@ -79,7 +79,7 @@ impl ProfileConfig {
         for name in profile_names {
             if let Some(profile) = self.get_profile_by_name(name.as_ref()) {
                 for (key, values) in &profile.subcommands {
-                    resolved.insert(key.clone(), values.clone());
+                    resolved.as_mut().insert(key.clone(), values.clone());
                 }
             }
         }
@@ -256,11 +256,12 @@ impl Profile {
     }
 
     pub fn add_subcommand(&mut self, key: String, long_subcommands: Vec) {
-        self.subcommands.insert(key, long_subcommands);
+        self.subcommands.as_mut().insert(key, long_subcommands);
     }
 
     pub fn remove_subcommand(&mut self, key: &str) -> Result<()> {
         self.subcommands
+            .as_mut()
             .remove(key)
             .ok_or_else(|| anyhow::anyhow!("Subcommand alias '{key}' not found"))?;
         Ok(())
@@ -273,7 +274,7 @@ impl AliasCollection for Profile {
     }
 
     fn len(&self) -> usize {
-        self.aliases.len() + self.subcommands.len()
+        self.aliases.len() + self.subcommands.as_ref().len()
     }
 
     fn short_list(&self) -> String {
diff --git a/crates/am/src/project.rs b/crates/am/src/project.rs
index c5f14fc8..e7fffd37 100644
--- a/crates/am/src/project.rs
+++ b/crates/am/src/project.rs
@@ -125,11 +125,11 @@ impl ProjectAliases {
     }
 
     pub fn add_subcommand(&mut self, key: String, long_subcommands: Vec) {
-        self.subcommands.insert(key, long_subcommands);
+        self.subcommands.as_mut().insert(key, long_subcommands);
     }
 
     pub fn remove_subcommand(&mut self, key: &str) -> crate::Result<()> {
-        self.subcommands.remove(key).ok_or_else(|| {
+        self.subcommands.as_mut().remove(key).ok_or_else(|| {
             anyhow::anyhow!("Subcommand alias '{key}' not found in {ALIASES_FILE}")
         })?;
         Ok(())
@@ -238,11 +238,12 @@ mod tests {
         let mut project = ProjectAliases::default();
         project
             .subcommands
+            .as_mut()
             .insert("jj:ab".into(), vec!["abandon".into()]);
         project.save(&path).unwrap();
 
         let loaded = ProjectAliases::load(&path).unwrap();
-        assert_eq!(loaded.subcommands.len(), 1);
-        assert_eq!(loaded.subcommands["jj:ab"], vec!["abandon"]);
+        assert_eq!(loaded.subcommands.as_ref().len(), 1);
+        assert_eq!(loaded.subcommands.as_ref()["jj:ab"], vec!["abandon"]);
     }
 }
diff --git a/crates/am/src/subcommand.rs b/crates/am/src/subcommand.rs
index 7dec96c5..b913989a 100644
--- a/crates/am/src/subcommand.rs
+++ b/crates/am/src/subcommand.rs
@@ -2,6 +2,7 @@ use std::collections::BTreeMap;
 
 use anyhow::anyhow;
 use log::warn;
+use serde::{Deserialize, Serialize};
 
 /// Validates that a name is a safe shell identifier.
 ///
@@ -86,8 +87,61 @@ impl SubcommandEntry {
 }
 
 /// Storage type for subcommand aliases. Key is the full colon-joined string
-/// (e.g., "jj:b:l"), value is the Vec of long subcommands (e.g., ["branch", "list"]).
-pub type SubcommandSet = BTreeMap>;
+/// (e.g., `"jj:b:l"`), value is the Vec of long subcommands (e.g.,
+/// `["branch", "list"]`).
+///
+/// Wraps `BTreeMap>` as a newtype so the API is explicit
+/// and the serde boundary is transparent (preserves `[subcommands]` TOML
+/// layout).
+#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)]
+#[serde(transparent)]
+pub struct SubcommandSet(BTreeMap>);
+
+impl AsRef>> for SubcommandSet {
+    fn as_ref(&self) -> &BTreeMap> {
+        &self.0
+    }
+}
+
+impl AsMut>> for SubcommandSet {
+    fn as_mut(&mut self) -> &mut BTreeMap> {
+        &mut self.0
+    }
+}
+
+impl SubcommandSet {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    /// Kept as a method so `#[serde(skip_serializing_if = "SubcommandSet::is_empty")]`
+    /// can reference it directly. All other access should go through `AsRef`/`AsMut`.
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+}
+
+impl<'a> IntoIterator for &'a SubcommandSet {
+    type Item = (&'a String, &'a Vec);
+    type IntoIter = std::collections::btree_map::Iter<'a, String, Vec>;
+    fn into_iter(self) -> Self::IntoIter {
+        self.0.iter()
+    }
+}
+
+impl IntoIterator for SubcommandSet {
+    type Item = (String, Vec);
+    type IntoIter = std::collections::btree_map::IntoIter>;
+    fn into_iter(self) -> Self::IntoIter {
+        self.0.into_iter()
+    }
+}
+
+impl FromIterator<(String, Vec)> for SubcommandSet {
+    fn from_iter)>>(iter: I) -> Self {
+        Self(BTreeMap::from_iter(iter))
+    }
+}
 
 /// Group subcommand entries by program name.
 pub fn group_by_program(set: &SubcommandSet) -> BTreeMap> {
@@ -220,9 +274,11 @@ mod tests {
     #[test]
     fn group_by_program_groups_correctly() {
         let mut set = SubcommandSet::new();
-        set.insert("jj:ab".into(), vec!["abandon".into()]);
-        set.insert("jj:b:l".into(), vec!["branch".into(), "list".into()]);
-        set.insert("git:co".into(), vec!["checkout".into()]);
+        set.as_mut().insert("jj:ab".into(), vec!["abandon".into()]);
+        set.as_mut()
+            .insert("jj:b:l".into(), vec!["branch".into(), "list".into()]);
+        set.as_mut()
+            .insert("git:co".into(), vec!["checkout".into()]);
 
         let groups = group_by_program(&set);
         assert_eq!(groups.len(), 2);
@@ -240,15 +296,79 @@ mod tests {
     #[test]
     fn group_by_program_skips_invalid_entries() {
         let mut set = SubcommandSet::new();
-        set.insert("jj:ab".into(), vec!["abandon".into()]);
+        set.as_mut().insert("jj:ab".into(), vec!["abandon".into()]);
         // mismatched counts — invalid
-        set.insert("jj:b:l".into(), vec!["branch".into()]);
+        set.as_mut().insert("jj:b:l".into(), vec!["branch".into()]);
         // no colon — invalid
-        set.insert("bad".into(), vec!["whatever".into()]);
+        set.as_mut().insert("bad".into(), vec!["whatever".into()]);
 
         let groups = group_by_program(&set);
         assert_eq!(groups.len(), 1);
         assert_eq!(groups["jj"].len(), 1);
         assert_eq!(groups["jj"][0].short_subcommands, vec!["ab"]);
     }
+
+    // --- SubcommandSet newtype API ---
+
+    #[test]
+    fn subcommandset_basic_ops_via_as_ref_as_mut() {
+        let mut set = SubcommandSet::new();
+        assert!(set.is_empty());
+
+        set.as_mut().insert("jj:ab".into(), vec!["abandon".into()]);
+        assert_eq!(set.as_ref().len(), 1);
+        assert!(set.as_ref().contains_key("jj:ab"));
+        assert_eq!(
+            set.as_ref().get("jj:ab"),
+            Some(&vec!["abandon".to_string()])
+        );
+
+        let removed = set.as_mut().remove("jj:ab");
+        assert_eq!(removed, Some(vec!["abandon".to_string()]));
+        assert!(set.is_empty());
+    }
+
+    #[test]
+    fn subcommandset_iteration_via_into_iterator() {
+        let set: SubcommandSet = [
+            ("a:x".to_string(), vec!["one".to_string()]),
+            ("b:y".to_string(), vec!["two".to_string()]),
+        ]
+        .into_iter()
+        .collect();
+
+        // IntoIterator for &SubcommandSet lets for-loops work directly.
+        let keys: Vec<&str> = (&set).into_iter().map(|(k, _)| k.as_str()).collect();
+        assert_eq!(keys, vec!["a:x", "b:y"]);
+
+        // Owning IntoIterator yields (String, Vec).
+        let owned: Vec<(String, Vec)> = set.into_iter().collect();
+        assert_eq!(owned.len(), 2);
+    }
+
+    #[test]
+    fn subcommandset_serde_transparent() {
+        let set: SubcommandSet = [
+            ("jj:ab".to_string(), vec!["abandon".to_string()]),
+            (
+                "jj:b:l".to_string(),
+                vec!["branch".to_string(), "list".to_string()],
+            ),
+        ]
+        .into_iter()
+        .collect();
+
+        // Serializes as a plain map (not as a tuple-struct wrapper).
+        #[derive(serde::Serialize, serde::Deserialize)]
+        struct Wrapper {
+            subcommands: SubcommandSet,
+        }
+        let toml_str = toml::to_string(&Wrapper { subcommands: set }).unwrap();
+        assert!(toml_str.contains("[subcommands]"));
+        assert!(toml_str.contains("\"jj:ab\" = [\"abandon\"]"));
+
+        let parsed: Wrapper = toml::from_str(&toml_str).unwrap();
+        assert_eq!(parsed.subcommands.as_ref().len(), 2);
+        assert_eq!(parsed.subcommands.as_ref()["jj:ab"], vec!["abandon"]);
+    }
 }
diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs
index d7fffa8c..0ed2a8a1 100644
--- a/crates/am/src/update.rs
+++ b/crates/am/src/update.rs
@@ -306,7 +306,7 @@ pub fn update(model: &mut AppModel, message: Message) -> Result match target {
             AliasTarget::Global => {
-                model.config.subcommands.remove(&original_key);
+                model.config.subcommands.as_mut().remove(&original_key);
                 model.config.add_subcommand(new_key, long_subcommands);
                 Ok(UpdateResult::effect(Effect::SaveConfig))
             }
@@ -324,7 +324,7 @@ pub fn update(model: &mut AppModel, message: Message) -> Result Result {
                 let profile = resolve_profile_mut(model, &target)?;
-                profile.subcommands.remove(&original_key);
+                profile.subcommands.as_mut().remove(&original_key);
                 profile.add_subcommand(new_key, long_subcommands);
                 Ok(UpdateResult::effect(Effect::SaveProfiles))
             }
@@ -361,7 +361,7 @@ pub fn update(model: &mut AppModel, message: Message) -> Result keys
                         .iter()
-                        .filter_map(|k| Some((k.clone(), subs.get(k)?.clone())))
+                        .filter_map(|k| Some((k.clone(), subs.as_ref().get(k)?.clone())))
                         .collect(),
                     None => vec![],
                 }
@@ -415,7 +415,7 @@ pub fn update(model: &mut AppModel, message: Message) -> Result keys
                         .iter()
-                        .filter_map(|k| Some((k.clone(), subs.get(k)?.clone())))
+                        .filter_map(|k| Some((k.clone(), subs.as_ref().get(k)?.clone())))
                         .collect(),
                     None => vec![],
                 }
@@ -447,7 +447,7 @@ pub fn update(model: &mut AppModel, message: Message) -> Result {
                     for (key, _) in &pairs {
-                        model.config.subcommands.remove(key);
+                        model.config.subcommands.as_mut().remove(key);
                     }
                 }
                 AliasTarget::Local => {} // handled via effects below
@@ -455,7 +455,7 @@ pub fn update(model: &mut AppModel, message: Message) -> Result Result Result (zsh::scan_external_functions(), zsh::scan_external_aliases()),
@@ -856,7 +856,7 @@ pub fn update(model: &mut AppModel, message: Message) -> Result std::collections::BTreeSet {
         .iter()
         .map(|(n, _)| n.as_ref().to_string())
         .collect();
-    set.extend(project.subcommands.keys().cloned());
+    set.extend(project.subcommands.as_ref().keys().cloned());
     set
 }
 
@@ -964,7 +964,7 @@ fn profile_items(profile: &Profile) -> Vec {
         .iter()
         .map(|(n, _)| n.as_ref().to_string())
         .collect();
-    items.extend(profile.subcommands.keys().cloned());
+    items.extend(profile.subcommands.as_ref().keys().cloned());
     items
 }
 
@@ -1231,6 +1231,7 @@ mod tests {
         model
             .config
             .subcommands
+            .as_mut()
             .insert("jj:ab".into(), vec!["abandon".into()]);
         let result = update(
             &mut model,
@@ -1242,9 +1243,9 @@ mod tests {
             },
         )
         .unwrap();
-        assert!(!model.config.subcommands.contains_key("jj:ab"));
+        assert!(!model.config.subcommands.as_ref().contains_key("jj:ab"));
         assert_eq!(
-            model.config.subcommands.get("jj:a"),
+            model.config.subcommands.as_ref().get("jj:a"),
             Some(&vec!["abandon".to_string()])
         );
         assert!(result
@@ -1259,6 +1260,7 @@ mod tests {
         model
             .config
             .subcommands
+            .as_mut()
             .insert("jj:ab".into(), vec!["abandon".into()]);
         model.profile_config_mut().add_profile("rust").unwrap();
         let _ = update(
@@ -1272,11 +1274,11 @@ mod tests {
         .unwrap();
         let profile = model.profile_config().get_profile_by_name("rust").unwrap();
         assert_eq!(
-            profile.subcommands.get("jj:ab"),
+            profile.subcommands.as_ref().get("jj:ab"),
             Some(&vec!["abandon".to_string()])
         );
         // Source preserved
-        assert!(model.config.subcommands.contains_key("jj:ab"));
+        assert!(model.config.subcommands.as_ref().contains_key("jj:ab"));
     }
 
     #[test]
@@ -1285,6 +1287,7 @@ mod tests {
         model
             .config
             .subcommands
+            .as_mut()
             .insert("jj:ab".into(), vec!["abandon".into()]);
         model.profile_config_mut().add_profile("rust").unwrap();
         let _ = update(
@@ -1296,9 +1299,9 @@ mod tests {
             },
         )
         .unwrap();
-        assert!(!model.config.subcommands.contains_key("jj:ab"));
+        assert!(!model.config.subcommands.as_ref().contains_key("jj:ab"));
         let profile = model.profile_config().get_profile_by_name("rust").unwrap();
-        assert!(profile.subcommands.contains_key("jj:ab"));
+        assert!(profile.subcommands.as_ref().contains_key("jj:ab"));
     }
 
     #[test]
@@ -1733,8 +1736,8 @@ mod tests {
         )
         .unwrap();
 
-        assert_eq!(model.config.subcommands.len(), 1);
-        assert_eq!(model.config.subcommands["jj:ab"], vec!["abandon"]);
+        assert_eq!(model.config.subcommands.as_ref().len(), 1);
+        assert_eq!(model.config.subcommands.as_ref()["jj:ab"], vec!["abandon"]);
         assert_eq!(result.effects, vec![Effect::SaveConfig]);
     }
 
@@ -1795,7 +1798,7 @@ mod tests {
 
         assert_eq!(result.effects, vec![Effect::SaveProfiles]);
         let profile = model.profile_config().get_profile_by_name("rust").unwrap();
-        assert_eq!(profile.subcommands.len(), 1);
+        assert_eq!(profile.subcommands.as_ref().len(), 1);
     }
 
     #[test]
diff --git a/crates/am/tests/snapshots.rs b/crates/am/tests/snapshots.rs
index d8bf5451..6569bd32 100644
--- a/crates/am/tests/snapshots.rs
+++ b/crates/am/tests/snapshots.rs
@@ -209,8 +209,12 @@ fn snapshot_init_fish_deep_chain() {
 fn snapshot_init_fish_with_simple_subcommands() {
     let globals = aliases(&[("gs", "git status")]);
     let mut subcommands = SubcommandSet::new();
-    subcommands.insert("jj:ab".into(), vec!["abandon".into()]);
-    subcommands.insert("jj:new".into(), vec!["new --no-edit".into()]);
+    subcommands
+        .as_mut()
+        .insert("jj:ab".into(), vec!["abandon".into()]);
+    subcommands
+        .as_mut()
+        .insert("jj:new".into(), vec!["new --no-edit".into()]);
     let output = generate_init(
         &default_ctx(&Shell::Fish),
         &globals,
@@ -223,17 +227,23 @@ fn snapshot_init_fish_with_simple_subcommands() {
 #[test]
 fn snapshot_init_bash_with_kubectl_subcommands() {
     let mut subcommands = SubcommandSet::new();
-    subcommands.insert("kubectl:get:po".into(), vec!["get".into(), "pods".into()]);
-    subcommands.insert(
+    subcommands
+        .as_mut()
+        .insert("kubectl:get:po".into(), vec!["get".into(), "pods".into()]);
+    subcommands.as_mut().insert(
         "kubectl:get:svc".into(),
         vec!["get".into(), "services".into()],
     );
-    subcommands.insert("kubectl:apply:f".into(), vec!["apply".into(), "-f".into()]);
-    subcommands.insert(
+    subcommands
+        .as_mut()
+        .insert("kubectl:apply:f".into(), vec!["apply".into(), "-f".into()]);
+    subcommands.as_mut().insert(
         "kubectl:rollout:status".into(),
         vec!["rollout".into(), "status".into()],
     );
-    subcommands.insert("kubectl:logs:f".into(), vec!["logs".into(), "-f".into()]);
+    subcommands
+        .as_mut()
+        .insert("kubectl:logs:f".into(), vec!["logs".into(), "-f".into()]);
     let output = generate_init(
         &default_ctx(&Shell::Bash),
         &AliasSet::default(),
@@ -848,7 +858,7 @@ fn snapshot_sync_fish_incremental_one_alias_updated() {
 fn snapshot_sync_bash_subcommand_wrapper_fresh_load() {
     use amoxide::precedence::{render_diff, Precedence};
     let mut subs = SubcommandSet::new();
-    subs.insert("jj:ab".into(), vec!["abandon".into()]);
+    subs.as_mut().insert("jj:ab".into(), vec!["abandon".into()]);
     let shell = Shell::Bash.as_shell(&Default::default(), Default::default(), Default::default());
     let diff = Precedence::new()
         .with_project(&AliasSet::default(), &subs)

From 4030e071daa2251ffdd96eeed1dde98067618766 Mon Sep 17 00:00:00 2001
From: Sven Kanoldt 
Date: Thu, 23 Apr 2026 09:17:24 +0200
Subject: [PATCH 30/38] refactor: make group_by_program a method on
 SubcommandSet

The free `group_by_program(&SubcommandSet)` function only operated on
`self`, so it belongs on the type. Callers switch from
`group_by_program(&subs)` to `subs.group_by_program()`.

- moved into `impl SubcommandSet` as `group_by_program(&self)`
- deleted the free function
- updated call sites in display, profile, precedence, trust, bin/am, and tests
---
 crates/am/src/bin/am.rs     |  2 +-
 crates/am/src/display.rs    |  2 +-
 crates/am/src/precedence.rs |  2 +-
 crates/am/src/profile.rs    |  4 ++--
 crates/am/src/subcommand.rs | 41 ++++++++++++++++++++-----------------
 crates/am/src/trust.rs      |  2 +-
 6 files changed, 28 insertions(+), 25 deletions(-)

diff --git a/crates/am/src/bin/am.rs b/crates/am/src/bin/am.rs
index fbf987d3..7cd01920 100644
--- a/crates/am/src/bin/am.rs
+++ b/crates/am/src/bin/am.rs
@@ -332,7 +332,7 @@ fn main() -> anyhow::Result<()> {
                 let cmd = alias_value.command();
                 println!("  {:width$} \u{2192} {cmd}", name, width = max_name_len);
             }
-            let subcmd_groups = amoxide::subcommand::group_by_program(&project.subcommands);
+            let subcmd_groups = project.subcommands.group_by_program();
             if !subcmd_groups.is_empty() {
                 println!();
                 for (program, entries) in &subcmd_groups {
diff --git a/crates/am/src/display.rs b/crates/am/src/display.rs
index e08fc51e..fa629d74 100644
--- a/crates/am/src/display.rs
+++ b/crates/am/src/display.rs
@@ -23,7 +23,7 @@ fn render_items(
     aliases: &AliasSet,
     subcommands: &crate::subcommand::SubcommandSet,
 ) {
-    let subcmd_groups = crate::subcommand::group_by_program(subcommands);
+    let subcmd_groups = subcommands.group_by_program();
 
     // Chain aliases then subcommand groups into one peekable stream.
     // Each "item" is rendered with ├─ unless peek() returns None (last item).
diff --git a/crates/am/src/precedence.rs b/crates/am/src/precedence.rs
index f952e041..2fd7e6ca 100644
--- a/crates/am/src/precedence.rs
+++ b/crates/am/src/precedence.rs
@@ -193,7 +193,7 @@ impl Precedence {
     pub fn resolve(self) -> PrecedenceDiff {
         let merged_aliases = self.merged_aliases();
         let merged_subcommands = self.merged_subcommands();
-        let subcmd_groups = crate::subcommand::group_by_program(&merged_subcommands);
+        let subcmd_groups = merged_subcommands.group_by_program();
         let program_names: BTreeSet = subcmd_groups.keys().cloned().collect();
 
         let mut effective: BTreeMap = BTreeMap::new();
diff --git a/crates/am/src/profile.rs b/crates/am/src/profile.rs
index 08bf5b01..3c49b715 100644
--- a/crates/am/src/profile.rs
+++ b/crates/am/src/profile.rs
@@ -4,7 +4,7 @@ use log::info;
 use serde::{Deserialize, Serialize};
 
 use crate::dirs::config_dir;
-use crate::subcommand::{group_by_program, SubcommandSet};
+use crate::subcommand::SubcommandSet;
 use crate::{AliasDetail, AliasName, AliasSet, Result, TomlAlias};
 
 /// A collection of aliases (regular and/or subcommand) that can report its
@@ -284,7 +284,7 @@ impl AliasCollection for Profile {
             .map(|(k, _)| k.as_ref().to_string())
             .collect();
 
-        let groups = group_by_program(&self.subcommands);
+        let groups = self.subcommands.group_by_program();
         for (program, entries) in &groups {
             // Each entry's short_subcommands are space-joined; entries within
             // the same program are comma-separated.
diff --git a/crates/am/src/subcommand.rs b/crates/am/src/subcommand.rs
index b913989a..8c61c115 100644
--- a/crates/am/src/subcommand.rs
+++ b/crates/am/src/subcommand.rs
@@ -119,6 +119,25 @@ impl SubcommandSet {
     pub fn is_empty(&self) -> bool {
         self.0.is_empty()
     }
+
+    /// Group the entries by program name. Each entry whose key fails to parse
+    /// (mismatched short/long count, empty segment, etc.) is skipped with a
+    /// warning. Returns a `BTreeMap>` so callers
+    /// can iterate per-program.
+    pub fn group_by_program(&self) -> BTreeMap> {
+        let mut groups: BTreeMap> = BTreeMap::new();
+        for (key, values) in self {
+            match SubcommandEntry::parse_key(key, values.clone()) {
+                Ok(entry) => {
+                    groups.entry(entry.program.clone()).or_default().push(entry);
+                }
+                Err(e) => {
+                    warn!("Skipping invalid subcommand alias '{key}': {e}");
+                }
+            }
+        }
+        groups
+    }
 }
 
 impl<'a> IntoIterator for &'a SubcommandSet {
@@ -143,22 +162,6 @@ impl FromIterator<(String, Vec)> for SubcommandSet {
     }
 }
 
-/// Group subcommand entries by program name.
-pub fn group_by_program(set: &SubcommandSet) -> BTreeMap> {
-    let mut groups: BTreeMap> = BTreeMap::new();
-    for (key, values) in set {
-        match SubcommandEntry::parse_key(key, values.clone()) {
-            Ok(entry) => {
-                groups.entry(entry.program.clone()).or_default().push(entry);
-            }
-            Err(e) => {
-                warn!("Skipping invalid subcommand alias '{key}': {e}");
-            }
-        }
-    }
-    groups
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -280,7 +283,7 @@ mod tests {
         set.as_mut()
             .insert("git:co".into(), vec!["checkout".into()]);
 
-        let groups = group_by_program(&set);
+        let groups = set.group_by_program();
         assert_eq!(groups.len(), 2);
         assert_eq!(groups["jj"].len(), 2);
         assert_eq!(groups["git"].len(), 1);
@@ -289,7 +292,7 @@ mod tests {
     #[test]
     fn group_by_program_empty() {
         let set = SubcommandSet::new();
-        let groups = group_by_program(&set);
+        let groups = set.group_by_program();
         assert!(groups.is_empty());
     }
 
@@ -302,7 +305,7 @@ mod tests {
         // no colon — invalid
         set.as_mut().insert("bad".into(), vec!["whatever".into()]);
 
-        let groups = group_by_program(&set);
+        let groups = set.group_by_program();
         assert_eq!(groups.len(), 1);
         assert_eq!(groups["jj"].len(), 1);
         assert_eq!(groups["jj"][0].short_subcommands, vec!["ab"]);
diff --git a/crates/am/src/trust.rs b/crates/am/src/trust.rs
index b6f12901..bc0655ce 100644
--- a/crates/am/src/trust.rs
+++ b/crates/am/src/trust.rs
@@ -79,7 +79,7 @@ pub fn render_load_message(
         lines.push(format!("  {padded} \u{2192} {cmd}"));
     }
 
-    let subcmd_groups = crate::subcommand::group_by_program(subcommands);
+    let subcmd_groups = subcommands.group_by_program();
     for (program, entries) in &subcmd_groups {
         lines.push(format!("  {program} (subcommands):"));
         for entry in entries {

From 994ff004c5e06e50a8b6702217307aec767751e4 Mon Sep 17 00:00:00 2001
From: Sven Kanoldt 
Date: Thu, 23 Apr 2026 09:28:06 +0200
Subject: [PATCH 31/38] refactor: make render a method on PrecedenceDiff

Free `render_diff(&PrecedenceDiff, &dyn ShellAdapter)` had no reason to
live outside the type. Moved it to `impl PrecedenceDiff` as
`render(&self, &dyn ShellAdapter) -> String`.

- callers switch from `render_diff(&diff, shell)` to `diff.render(shell)`
- updated init.rs, update.rs, precedence tests, snapshot tests (8 sites)
- dropped the now-unused `precedence::{self}` import alias in update.rs
---
 crates/am/src/init.rs        |   6 +-
 crates/am/src/precedence.rs  | 135 ++++++++++++++++++-----------------
 crates/am/src/update.rs      |   6 +-
 crates/am/tests/snapshots.rs |  36 +++++-----
 4 files changed, 92 insertions(+), 91 deletions(-)

diff --git a/crates/am/src/init.rs b/crates/am/src/init.rs
index 9fa9c42f..80b1214b 100644
--- a/crates/am/src/init.rs
+++ b/crates/am/src/init.rs
@@ -26,7 +26,7 @@ pub fn generate_init(
     profile_aliases: &AliasSet,
     subcommands: &SubcommandSet,
 ) -> String {
-    use crate::precedence::{self, Precedence};
+    use crate::precedence::Precedence;
 
     let shell_impl = ctx.shell.clone().as_shell(
         ctx.cfg,
@@ -45,7 +45,7 @@ pub fn generate_init(
         .with_profiles(profile_aliases, subcommands)
         .resolve();
 
-    let mut output = precedence::render_diff(&diff, shell_impl.as_ref());
+    let mut output = diff.render(shell_impl.as_ref());
 
     // Wrapper function + cd hook + completions.
     if !output.is_empty() {
@@ -426,7 +426,7 @@ mod tests {
 
     #[test]
     fn init_delegates_alias_emission_to_precedence() {
-        // init output must match render_diff output for the same inputs.
+        // init output must match PrecedenceDiff::render output for the same inputs.
         let aliases = test_aliases();
         let ctx = default_ctx(&Shell::Fish);
         let output = generate_init(&ctx, &AliasSet::default(), &aliases, &SubcommandSet::new());
diff --git a/crates/am/src/precedence.rs b/crates/am/src/precedence.rs
index 2fd7e6ca..39d0c40a 100644
--- a/crates/am/src/precedence.rs
+++ b/crates/am/src/precedence.rs
@@ -1,6 +1,8 @@
 use std::collections::{BTreeMap, BTreeSet, HashSet};
 
 use crate::alias::{AliasSet, TomlAlias};
+use crate::env_vars;
+use crate::shell::ShellAdapter;
 use crate::subcommand::{SubcommandEntry, SubcommandSet};
 
 #[derive(Debug, Clone, PartialEq)]
@@ -294,81 +296,80 @@ impl Precedence {
     }
 }
 
-use crate::env_vars;
-use crate::shell::ShellAdapter;
-
-/// Render a [`PrecedenceDiff`] into shell code using the given adapter.
-///
-/// Emission order:
-///   1. unload (removed + changed) — skipping subcommand-key names (they're
-///      tracking-only, not shell functions)
-///   2. load (added + changed)
-///   3. set `_AM_ALIASES` / `_AM_SUBCOMMANDS` to the union of added + changed
-///      + unchanged
-pub fn render_diff(diff: &PrecedenceDiff, shell: &dyn ShellAdapter) -> String {
-    let mut lines: Vec = Vec::new();
-
-    // 1. Unload
-    for name in &diff.removed {
-        if name.contains(':') {
-            continue;
-        }
-        lines.push(shell.unalias(name));
-    }
-    for entry in &diff.changed {
-        if matches!(entry.kind, EntryKind::SubcommandKey { .. }) {
-            continue;
+impl PrecedenceDiff {
+    /// Render this diff into shell code using the given adapter.
+    ///
+    /// Emission order:
+    ///   1. unload (removed + changed) — skipping subcommand-key names
+    ///      (they're tracking-only, not shell functions)
+    ///   2. load (added + changed)
+    ///   3. set `_AM_ALIASES` / `_AM_SUBCOMMANDS` to the union of added +
+    ///      changed + unchanged
+    pub fn render(&self, shell: &dyn ShellAdapter) -> String {
+        let mut lines: Vec = Vec::new();
+
+        // 1. Unload
+        for name in &self.removed {
+            if name.contains(':') {
+                continue;
+            }
+            lines.push(shell.unalias(name));
         }
-        if entry.name.contains(':') {
-            continue;
+        for entry in &self.changed {
+            if matches!(entry.kind, EntryKind::SubcommandKey { .. }) {
+                continue;
+            }
+            if entry.name.contains(':') {
+                continue;
+            }
+            lines.push(shell.unalias(&entry.name));
         }
-        lines.push(shell.unalias(&entry.name));
-    }
 
-    // 2. Load (added + changed)
-    for entry in diff.added.iter().chain(diff.changed.iter()) {
-        match &entry.kind {
-            EntryKind::Alias(alias) => {
-                lines.push(shell.alias(&alias.as_entry(&entry.name)));
+        // 2. Load (added + changed)
+        for entry in self.added.iter().chain(self.changed.iter()) {
+            match &entry.kind {
+                EntryKind::Alias(alias) => {
+                    lines.push(shell.alias(&alias.as_entry(&entry.name)));
+                }
+                EntryKind::SubcommandWrapper {
+                    program,
+                    entries,
+                    base_cmd,
+                } => {
+                    let cmd = base_cmd
+                        .clone()
+                        .unwrap_or_else(|| format!("command {program}"));
+                    lines.push(shell.subcommand_wrapper(program, &cmd, entries));
+                }
+                EntryKind::SubcommandKey { .. } => {}
             }
-            EntryKind::SubcommandWrapper {
-                program,
-                entries,
-                base_cmd,
-            } => {
-                let cmd = base_cmd
-                    .clone()
-                    .unwrap_or_else(|| format!("command {program}"));
-                lines.push(shell.subcommand_wrapper(program, &cmd, entries));
+        }
+
+        // 3. Update tracking env vars
+        let mut alias_pairs = Vec::new();
+        let mut sub_pairs = Vec::new();
+        for e in self
+            .added
+            .iter()
+            .chain(self.changed.iter())
+            .chain(self.unchanged.iter())
+        {
+            let pair = format!("{}|{}", e.name, e.hash);
+            match &e.kind {
+                EntryKind::SubcommandKey { .. } => sub_pairs.push(pair),
+                _ => alias_pairs.push(pair),
             }
-            EntryKind::SubcommandKey { .. } => {}
         }
-    }
 
-    // 3. Update tracking env vars
-    let mut alias_pairs = Vec::new();
-    let mut sub_pairs = Vec::new();
-    for e in diff
-        .added
-        .iter()
-        .chain(diff.changed.iter())
-        .chain(diff.unchanged.iter())
-    {
-        let pair = format!("{}|{}", e.name, e.hash);
-        match &e.kind {
-            EntryKind::SubcommandKey { .. } => sub_pairs.push(pair),
-            _ => alias_pairs.push(pair),
+        if !alias_pairs.is_empty() {
+            lines.push(shell.set_env(env_vars::AM_ALIASES, &alias_pairs.join(",")));
+        }
+        if !sub_pairs.is_empty() {
+            lines.push(shell.set_env(env_vars::AM_SUBCOMMANDS, &sub_pairs.join(",")));
         }
-    }
 
-    if !alias_pairs.is_empty() {
-        lines.push(shell.set_env(env_vars::AM_ALIASES, &alias_pairs.join(",")));
-    }
-    if !sub_pairs.is_empty() {
-        lines.push(shell.set_env(env_vars::AM_SUBCOMMANDS, &sub_pairs.join(",")));
+        lines.join("\n")
     }
-
-    lines.join("\n")
 }
 
 #[cfg(test)]
@@ -779,7 +780,7 @@ mod tests {
             .with_shell_state_from_env(Some("b|0000000,gone|aaa"), None)
             .resolve();
 
-        let out = crate::precedence::render_diff(&diff, shell.as_ref());
+        let out = diff.render(shell.as_ref());
         assert!(
             out.contains("functions -e gone"),
             "gone must be unloaded: {out}"
@@ -802,7 +803,7 @@ mod tests {
     fn render_empty_diff_produces_empty_string() {
         let cfg = ShellsTomlConfig::default();
         let shell = Shell::Fish.as_shell(&cfg, Default::default(), Default::default());
-        let out = crate::precedence::render_diff(&PrecedenceDiff::default(), shell.as_ref());
+        let out = PrecedenceDiff::default().render(shell.as_ref());
         assert!(out.is_empty());
     }
 }
diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs
index 0ed2a8a1..03e68865 100644
--- a/crates/am/src/update.rs
+++ b/crates/am/src/update.rs
@@ -4,7 +4,7 @@ use crate::display::render_listing;
 use crate::effects::Effect;
 use crate::env_vars;
 use crate::init::generate_init;
-use crate::precedence::{self, Precedence};
+use crate::precedence::Precedence;
 use crate::project::ProjectAliases;
 use crate::shell::bash;
 use crate::shell::zsh;
@@ -604,7 +604,7 @@ pub fn update(model: &mut AppModel, message: Message) -> Result Result
Date: Thu, 23 Apr 2026 09:36:04 +0200
Subject: [PATCH 32/38] fix: include alias names in sync incremental message
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Before:
  am: aliases changed (1 unloaded)

After:
  am: aliases changed — 1 unloaded: foo

Matches the style of the profile activation message and makes 'am tui'
edits (or any other mutation that doesn't already print a rich message)
self-explanatory — no more guessing which alias was affected.

- extract shared format_section helper used by both profile_toggle_message
  and the sync handler's incremental branch
- switch delimiter from '()' to ' — ' for consistency
- sections now read 'N verb: name1, name2, ...' and are '|'-separated when
  more than one category is non-empty
---
 crates/am/src/update.rs | 49 ++++++++++++++++++++++-------------------
 1 file changed, 26 insertions(+), 23 deletions(-)

diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs
index 03e68865..39681a78 100644
--- a/crates/am/src/update.rs
+++ b/crates/am/src/update.rs
@@ -739,18 +739,22 @@ pub fn update(model: &mut AppModel, message: Message) -> Result = diff.added.iter().map(|e| e.name.as_str()).collect();
+                    if !added.is_empty() {
+                        parts.push(format_section("loaded", &added));
                     }
-                    if !diff.changed.is_empty() {
-                        parts.push(format!("{} updated", diff.changed.len()));
+                    let changed: Vec<&str> = diff.changed.iter().map(|e| e.name.as_str()).collect();
+                    if !changed.is_empty() {
+                        parts.push(format_section("updated", &changed));
                     }
-                    if !diff.removed.is_empty() {
-                        parts.push(format!("{} unloaded", diff.removed.len()));
+                    let removed: Vec<&str> = diff.removed.iter().map(|s| s.as_str()).collect();
+                    if !removed.is_empty() {
+                        parts.push(format_section("unloaded", &removed));
                     }
                     if !parts.is_empty() {
                         lines.push(
-                            shell_impl.echo(&format!("am: aliases changed ({})", parts.join(", "))),
+                            shell_impl
+                                .echo(&format!("am: aliases changed — {}", parts.join(" | "))),
                         );
                     }
                 }
@@ -990,12 +994,10 @@ fn profile_toggle_message(
         };
     }
 
-    let (unshadowed, shadowed): (Vec<&String>, Vec<&String>) = profile_aliases
+    let (unshadowed, shadowed): (Vec<&str>, Vec<&str>) = profile_aliases
         .iter()
-        .partition(|n| !project_names.contains(n.as_str()));
-
-    let fmt_list =
-        |v: &[&String]| -> String { v.iter().map(|s| s.as_str()).collect::>().join(", ") };
+        .map(|s| s.as_str())
+        .partition(|n| !project_names.contains(*n));
 
     let head = match (activated, position) {
         (true, Some(pos)) => format!("am: profile {name} activated at position {pos}"),
@@ -1010,27 +1012,28 @@ fn profile_toggle_message(
     };
 
     match (unshadowed.is_empty(), shadowed.is_empty()) {
-        (false, true) => format!(
-            "{head} — {} {primary_verb}: {}",
-            unshadowed.len(),
-            fmt_list(&unshadowed)
-        ),
+        (false, true) => format!("{head} — {}", format_section(primary_verb, &unshadowed)),
         (true, false) => format!(
             "{head} — all {} {secondary_verb}: {}",
             shadowed.len(),
-            fmt_list(&shadowed)
+            shadowed.join(", ")
         ),
         (false, false) => format!(
-            "{head} — {} {primary_verb}: {} | {} {secondary_verb}: {}",
-            unshadowed.len(),
-            fmt_list(&unshadowed),
-            shadowed.len(),
-            fmt_list(&shadowed)
+            "{head} — {} | {}",
+            format_section(primary_verb, &unshadowed),
+            format_section(secondary_verb, &shadowed)
         ),
         (true, true) => unreachable!("profile_aliases non-empty but both partitions empty"),
     }
 }
 
+/// Format a list of names as `"N verb: a, b, c"`. Shared between the profile
+/// activation/deactivation message and the sync handler's incremental summary
+/// so they stay consistent.
+fn format_section(verb: &str, names: &[&str]) -> String {
+    format!("{} {verb}: {}", names.len(), names.join(", "))
+}
+
 fn resolve_profile<'a>(
     model: &'a AppModel,
     target: &AliasTarget,

From 59cb1d0aa08c62a830fe15ce8151a10eb420fd33 Mon Sep 17 00:00:00 2001
From: Sven Kanoldt 
Date: Thu, 23 Apr 2026 09:42:32 +0200
Subject: [PATCH 33/38] refactor: central format_change_summary helper + drop
 dead render_unload_message
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Two small follow-ups on the message-formatting work:

- Extract format_change_summary(head, sections) that assembles the full
  ' — N verb1: a, b | M verb2: c' line. Both the sync handler's
  incremental branch and the profile toggle message now call it so the
  head separator and section joiner live in exactly one place. The only
  bit still inline is the profile-specific 'all N shadowed by .aliases'
  phrasing, which has no equivalent in sync.
- Delete render_unload_message in trust.rs: dead since the sync handler
  rewrite (no production callers, only its own tests).
---
 crates/am/src/trust.rs  | 17 -----------
 crates/am/src/update.rs | 66 +++++++++++++++++++++++------------------
 2 files changed, 37 insertions(+), 46 deletions(-)

diff --git a/crates/am/src/trust.rs b/crates/am/src/trust.rs
index bc0655ce..8311abcb 100644
--- a/crates/am/src/trust.rs
+++ b/crates/am/src/trust.rs
@@ -92,11 +92,6 @@ pub fn render_load_message(
     lines.join("\n")
 }
 
-/// Render the "unloaded" info message shown when leaving a trusted directory.
-pub fn render_unload_message(alias_names: &[&str]) -> String {
-    format!("am: unloaded .aliases: {}", alias_names.join(", "))
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -192,18 +187,6 @@ mod tests {
         assert!(msg.contains("cargo build"));
     }
 
-    #[test]
-    fn render_unload_message_comma_separated() {
-        let msg = render_unload_message(&["b", "t", "cb"]);
-        assert_eq!(msg, "am: unloaded .aliases: b, t, cb");
-    }
-
-    #[test]
-    fn render_unload_message_single() {
-        let msg = render_unload_message(&["b"]);
-        assert_eq!(msg, "am: unloaded .aliases: b");
-    }
-
     #[test]
     fn project_trust_path_returns_path_for_all_variants() {
         let path = PathBuf::from("/project/.aliases");
diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs
index 39681a78..278fe3c5 100644
--- a/crates/am/src/update.rs
+++ b/crates/am/src/update.rs
@@ -734,28 +734,19 @@ pub fn update(model: &mut AppModel, message: Message) -> Result = diff.added.iter().map(|e| e.name.as_str()).collect();
-                    if !added.is_empty() {
-                        parts.push(format_section("loaded", &added));
-                    }
                     let changed: Vec<&str> = diff.changed.iter().map(|e| e.name.as_str()).collect();
-                    if !changed.is_empty() {
-                        parts.push(format_section("updated", &changed));
-                    }
                     let removed: Vec<&str> = diff.removed.iter().map(|s| s.as_str()).collect();
-                    if !removed.is_empty() {
-                        parts.push(format_section("unloaded", &removed));
-                    }
-                    if !parts.is_empty() {
-                        lines.push(
-                            shell_impl
-                                .echo(&format!("am: aliases changed — {}", parts.join(" | "))),
-                        );
+                    if let Some(msg) = format_change_summary(
+                        "am: aliases changed",
+                        &[
+                            ("loaded", &added),
+                            ("updated", &changed),
+                            ("unloaded", &removed),
+                        ],
+                    ) {
+                        lines.push(shell_impl.echo(&msg));
                     }
                 }
             }
@@ -1011,20 +1002,21 @@ fn profile_toggle_message(
         ("unloaded", "kept by .aliases")
     };
 
-    match (unshadowed.is_empty(), shadowed.is_empty()) {
-        (false, true) => format!("{head} — {}", format_section(primary_verb, &unshadowed)),
-        (true, false) => format!(
+    // "All shadowed" / "all kept" is a special-case phrasing only the profile
+    // path uses — sync never has this shape. Keep it inline.
+    if unshadowed.is_empty() && !shadowed.is_empty() {
+        return format!(
             "{head} — all {} {secondary_verb}: {}",
             shadowed.len(),
             shadowed.join(", ")
-        ),
-        (false, false) => format!(
-            "{head} — {} | {}",
-            format_section(primary_verb, &unshadowed),
-            format_section(secondary_verb, &shadowed)
-        ),
-        (true, true) => unreachable!("profile_aliases non-empty but both partitions empty"),
+        );
     }
+
+    format_change_summary(
+        &head,
+        &[(primary_verb, &unshadowed), (secondary_verb, &shadowed)],
+    )
+    .expect("profile_aliases non-empty but produced no sections")
 }
 
 /// Format a list of names as `"N verb: a, b, c"`. Shared between the profile
@@ -1034,6 +1026,22 @@ fn format_section(verb: &str, names: &[&str]) -> String {
     format!("{} {verb}: {}", names.len(), names.join(", "))
 }
 
+/// Build a full change-summary line like
+/// `" — N verb1: a, b | M verb2: c"`. Empty sections are skipped.
+/// Returns `None` when every section is empty (caller decides to stay silent).
+fn format_change_summary(head: &str, sections: &[(&str, &[&str])]) -> Option {
+    let parts: Vec = sections
+        .iter()
+        .filter(|(_, names)| !names.is_empty())
+        .map(|(verb, names)| format_section(verb, names))
+        .collect();
+    if parts.is_empty() {
+        None
+    } else {
+        Some(format!("{head} — {}", parts.join(" | ")))
+    }
+}
+
 fn resolve_profile<'a>(
     model: &'a AppModel,
     target: &AliasTarget,

From e4d8d6275bc36e0a125111e2e00682f7df10cd5e Mon Sep 17 00:00:00 2001
From: Sven Kanoldt 
Date: Thu, 23 Apr 2026 09:59:32 +0200
Subject: [PATCH 34/38] refactor: move sync change summary onto PrecedenceDiff
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The sync handler's "am: aliases changed — ..." message is a pure function
of the diff, so it belongs on the type. Moved to
`PrecedenceDiff::change_summary(&self) -> Option`.

- sync handler reduces to: `if let Some(msg) = diff.change_summary() { ... }`
- profile_toggle_message keeps its own `format_change_summary` helper — it
  needs a dynamic head and the special "all N shadowed" phrasing that
  sync doesn't have
- inline the now single-caller `format_section` into `format_change_summary`
---
 crates/am/src/precedence.rs | 24 ++++++++++++++++++++++++
 crates/am/src/update.rs     | 28 ++++++----------------------
 2 files changed, 30 insertions(+), 22 deletions(-)

diff --git a/crates/am/src/precedence.rs b/crates/am/src/precedence.rs
index 39d0c40a..1dfa127e 100644
--- a/crates/am/src/precedence.rs
+++ b/crates/am/src/precedence.rs
@@ -297,6 +297,30 @@ impl Precedence {
 }
 
 impl PrecedenceDiff {
+    /// Human-readable summary of what changed, suitable for echoing to the
+    /// user (e.g. `"am: aliases changed — 1 loaded: b | 1 unloaded: t"`).
+    ///
+    /// Returns `None` when nothing changed so callers can stay silent.
+    pub fn change_summary(&self) -> Option {
+        let added: Vec<&str> = self.added.iter().map(|e| e.name.as_str()).collect();
+        let changed: Vec<&str> = self.changed.iter().map(|e| e.name.as_str()).collect();
+        let removed: Vec<&str> = self.removed.iter().map(|s| s.as_str()).collect();
+        let parts: Vec = [
+            ("loaded", &added[..]),
+            ("updated", &changed[..]),
+            ("unloaded", &removed[..]),
+        ]
+        .iter()
+        .filter(|(_, names)| !names.is_empty())
+        .map(|(verb, names)| format!("{} {verb}: {}", names.len(), names.join(", ")))
+        .collect();
+        if parts.is_empty() {
+            None
+        } else {
+            Some(format!("am: aliases changed — {}", parts.join(" | ")))
+        }
+    }
+
     /// Render this diff into shell code using the given adapter.
     ///
     /// Emission order:
diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs
index 278fe3c5..9feff619 100644
--- a/crates/am/src/update.rs
+++ b/crates/am/src/update.rs
@@ -734,20 +734,8 @@ pub fn update(model: &mut AppModel, message: Message) -> Result = diff.added.iter().map(|e| e.name.as_str()).collect();
-                    let changed: Vec<&str> = diff.changed.iter().map(|e| e.name.as_str()).collect();
-                    let removed: Vec<&str> = diff.removed.iter().map(|s| s.as_str()).collect();
-                    if let Some(msg) = format_change_summary(
-                        "am: aliases changed",
-                        &[
-                            ("loaded", &added),
-                            ("updated", &changed),
-                            ("unloaded", &removed),
-                        ],
-                    ) {
-                        lines.push(shell_impl.echo(&msg));
-                    }
+                } else if let Some(msg) = diff.change_summary() {
+                    lines.push(shell_impl.echo(&msg));
                 }
             }
 
@@ -1019,21 +1007,17 @@ fn profile_toggle_message(
     .expect("profile_aliases non-empty but produced no sections")
 }
 
-/// Format a list of names as `"N verb: a, b, c"`. Shared between the profile
-/// activation/deactivation message and the sync handler's incremental summary
-/// so they stay consistent.
-fn format_section(verb: &str, names: &[&str]) -> String {
-    format!("{} {verb}: {}", names.len(), names.join(", "))
-}
-
 /// Build a full change-summary line like
 /// `" — N verb1: a, b | M verb2: c"`. Empty sections are skipped.
 /// Returns `None` when every section is empty (caller decides to stay silent).
+///
+/// Kept as a private helper for `profile_toggle_message`. The sync handler's
+/// equivalent lives on the diff itself as [`PrecedenceDiff::change_summary`].
 fn format_change_summary(head: &str, sections: &[(&str, &[&str])]) -> Option {
     let parts: Vec = sections
         .iter()
         .filter(|(_, names)| !names.is_empty())
-        .map(|(verb, names)| format_section(verb, names))
+        .map(|(verb, names)| format!("{} {verb}: {}", names.len(), names.join(", ")))
         .collect();
     if parts.is_empty() {
         None

From 2ba737483f10fc75c8986a180b9192c7dddb35a3 Mon Sep 17 00:00:00 2001
From: Sven Kanoldt 
Date: Thu, 23 Apr 2026 11:36:42 +0200
Subject: [PATCH 35/38] refactor: wrap env-var entry format in AliasWithHash
 newtype

Three places were splitting on '|' and ',' to parse the
_AM_ALIASES / _AM_SUBCOMMANDS env-var format: parse_state,
PrecedenceDiff::render, and the init --force nuke loop. Centralise the
invariant ("name|hash", or bare "name" for legacy compat) behind two
newtypes so the format lives in exactly one place.

- AliasWithHash: one entry with name() / hash() accessors, parse() that
  rejects empty-name tokens, Display that round-trips to the wire form.
- AliasWithHashList: comma-separated list wrapper. parse() drops
  malformed tokens silently; Display joins with ',' in order.
- parse_state, PrecedenceDiff::render, and update.rs's force-init nuke
  loop all delegate to these types. No more ad-hoc split_once('|') /
  split(',') in callers.
- 9 unit tests covering parse/display round-trip, bare-name backward
  compat, empty/None inputs, malformed-token skipping.
---
 crates/am/src/precedence.rs | 224 ++++++++++++++++++++++++++++++++----
 crates/am/src/update.rs     |  22 ++--
 2 files changed, 213 insertions(+), 33 deletions(-)

diff --git a/crates/am/src/precedence.rs b/crates/am/src/precedence.rs
index 1dfa127e..78770986 100644
--- a/crates/am/src/precedence.rs
+++ b/crates/am/src/precedence.rs
@@ -1,10 +1,135 @@
 use std::collections::{BTreeMap, BTreeSet, HashSet};
+use std::fmt;
 
 use crate::alias::{AliasSet, TomlAlias};
 use crate::env_vars;
 use crate::shell::ShellAdapter;
 use crate::subcommand::{SubcommandEntry, SubcommandSet};
 
+/// One entry in the `_AM_ALIASES` / `_AM_SUBCOMMANDS` env var, in the
+/// `"name|hash"` format (or a legacy bare `"name"` with no hash).
+///
+/// `hash = None` means the shell reloaded from an older amoxide that only
+/// tracked names; the diff treats such entries as "always differs" so they
+/// get reloaded on the next sync.
+#[derive(Debug, Clone, PartialEq)]
+pub struct AliasWithHash {
+    name: String,
+    hash: Option,
+}
+
+impl AliasWithHash {
+    pub fn new(name: impl Into, hash: Option) -> Self {
+        Self {
+            name: name.into(),
+            hash,
+        }
+    }
+
+    pub fn name(&self) -> &str {
+        &self.name
+    }
+
+    pub fn hash(&self) -> Option<&str> {
+        self.hash.as_deref()
+    }
+
+    /// Parse one `"name|hash"` (or bare `"name"`) token. Returns `None` when
+    /// the name segment is empty — callers skip such entries silently.
+    pub fn parse(token: &str) -> Option {
+        match token.split_once('|') {
+            Some((name, hash)) if !name.is_empty() => Some(Self {
+                name: name.to_string(),
+                hash: Some(hash.to_string()),
+            }),
+            Some(_) => None, // empty name before '|'
+            None if token.is_empty() => None,
+            None => Some(Self {
+                name: token.to_string(),
+                hash: None,
+            }),
+        }
+    }
+}
+
+impl fmt::Display for AliasWithHash {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match &self.hash {
+            Some(h) => write!(f, "{}|{}", self.name, h),
+            None => write!(f, "{}", self.name),
+        }
+    }
+}
+
+/// A comma-separated list of [`AliasWithHash`] entries — the on-the-wire
+/// format of `_AM_ALIASES` and `_AM_SUBCOMMANDS`.
+///
+/// Owns round-trip parsing and rendering so no other module has to know
+/// about the `"name|hash,name|hash,..."` layout.
+#[derive(Debug, Default, Clone, PartialEq)]
+pub struct AliasWithHashList(Vec);
+
+impl AliasWithHashList {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    pub fn push(&mut self, entry: AliasWithHash) {
+        self.0.push(entry);
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+
+    pub fn iter(&self) -> std::slice::Iter<'_, AliasWithHash> {
+        self.0.iter()
+    }
+
+    /// Parse an `_AM_ALIASES` / `_AM_SUBCOMMANDS` value. `None` or empty
+    /// string yields an empty list; malformed tokens are skipped.
+    pub fn parse(raw: Option<&str>) -> Self {
+        let Some(s) = raw.filter(|s| !s.is_empty()) else {
+            return Self::new();
+        };
+        Self(s.split(',').filter_map(AliasWithHash::parse).collect())
+    }
+}
+
+impl fmt::Display for AliasWithHashList {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        for (i, entry) in self.0.iter().enumerate() {
+            if i > 0 {
+                f.write_str(",")?;
+            }
+            write!(f, "{entry}")?;
+        }
+        Ok(())
+    }
+}
+
+impl FromIterator for AliasWithHashList {
+    fn from_iter>(iter: I) -> Self {
+        Self(iter.into_iter().collect())
+    }
+}
+
+impl<'a> IntoIterator for &'a AliasWithHashList {
+    type Item = &'a AliasWithHash;
+    type IntoIter = std::slice::Iter<'a, AliasWithHash>;
+    fn into_iter(self) -> Self::IntoIter {
+        self.0.iter()
+    }
+}
+
+impl IntoIterator for AliasWithHashList {
+    type Item = AliasWithHash;
+    type IntoIter = std::vec::IntoIter;
+    fn into_iter(self) -> Self::IntoIter {
+        self.0.into_iter()
+    }
+}
+
 #[derive(Debug, Clone, PartialEq)]
 pub enum EntryKind {
     Alias(TomlAlias),
@@ -148,18 +273,10 @@ impl Precedence {
     }
 
     fn parse_state(raw: Option<&str>) -> BTreeMap> {
-        let mut map = BTreeMap::new();
-        let Some(s) = raw.filter(|s| !s.is_empty()) else {
-            return map;
-        };
-        for entry in s.split(',') {
-            if let Some((name, hash)) = entry.split_once('|') {
-                map.insert(name.to_string(), Some(hash.to_string()));
-            } else {
-                map.insert(entry.to_string(), None);
-            }
-        }
-        map
+        AliasWithHashList::parse(raw)
+            .into_iter()
+            .map(|e| (e.name().to_string(), e.hash().map(String::from)))
+            .collect()
     }
 
     #[cfg(test)]
@@ -370,26 +487,26 @@ impl PrecedenceDiff {
         }
 
         // 3. Update tracking env vars
-        let mut alias_pairs = Vec::new();
-        let mut sub_pairs = Vec::new();
+        let mut alias_list = AliasWithHashList::new();
+        let mut sub_list = AliasWithHashList::new();
         for e in self
             .added
             .iter()
             .chain(self.changed.iter())
             .chain(self.unchanged.iter())
         {
-            let pair = format!("{}|{}", e.name, e.hash);
+            let entry = AliasWithHash::new(&e.name, Some(e.hash.clone()));
             match &e.kind {
-                EntryKind::SubcommandKey { .. } => sub_pairs.push(pair),
-                _ => alias_pairs.push(pair),
+                EntryKind::SubcommandKey { .. } => sub_list.push(entry),
+                _ => alias_list.push(entry),
             }
         }
 
-        if !alias_pairs.is_empty() {
-            lines.push(shell.set_env(env_vars::AM_ALIASES, &alias_pairs.join(",")));
+        if !alias_list.is_empty() {
+            lines.push(shell.set_env(env_vars::AM_ALIASES, &alias_list.to_string()));
         }
-        if !sub_pairs.is_empty() {
-            lines.push(shell.set_env(env_vars::AM_SUBCOMMANDS, &sub_pairs.join(",")));
+        if !sub_list.is_empty() {
+            lines.push(shell.set_env(env_vars::AM_SUBCOMMANDS, &sub_list.to_string()));
         }
 
         lines.join("\n")
@@ -401,6 +518,71 @@ mod tests {
     use super::*;
     use crate::alias::AliasName;
 
+    // ─── AliasWithHash / AliasWithHashList ──────────────────────────────
+
+    #[test]
+    fn alias_with_hash_parse_new_format() {
+        let e = AliasWithHash::parse("b|abc1234").unwrap();
+        assert_eq!(e.name(), "b");
+        assert_eq!(e.hash(), Some("abc1234"));
+    }
+
+    #[test]
+    fn alias_with_hash_parse_bare_name() {
+        let e = AliasWithHash::parse("t").unwrap();
+        assert_eq!(e.name(), "t");
+        assert_eq!(e.hash(), None);
+    }
+
+    #[test]
+    fn alias_with_hash_parse_empty_returns_none() {
+        assert!(AliasWithHash::parse("").is_none());
+        assert!(AliasWithHash::parse("|abc").is_none());
+    }
+
+    #[test]
+    fn alias_with_hash_display_roundtrip() {
+        assert_eq!(
+            AliasWithHash::new("b", Some("abc1234".into())).to_string(),
+            "b|abc1234"
+        );
+        assert_eq!(AliasWithHash::new("t", None).to_string(), "t");
+    }
+
+    #[test]
+    fn alias_with_hash_list_parse_and_render() {
+        let list = AliasWithHashList::parse(Some("b|abc1234,t|def5678"));
+        assert_eq!(list.iter().count(), 2);
+        assert_eq!(list.to_string(), "b|abc1234,t|def5678");
+    }
+
+    #[test]
+    fn alias_with_hash_list_parse_mixed_format() {
+        let list = AliasWithHashList::parse(Some("b|abc1234,t,gs|fed9876"));
+        let names: Vec<&str> = list.iter().map(|e| e.name()).collect();
+        assert_eq!(names, vec!["b", "t", "gs"]);
+        assert_eq!(list.iter().nth(1).unwrap().hash(), None);
+    }
+
+    #[test]
+    fn alias_with_hash_list_parse_empty_and_none() {
+        assert!(AliasWithHashList::parse(None).is_empty());
+        assert!(AliasWithHashList::parse(Some("")).is_empty());
+    }
+
+    #[test]
+    fn alias_with_hash_list_parse_skips_malformed_tokens() {
+        // Leading empty token and "|xxx" token get dropped silently.
+        let list = AliasWithHashList::parse(Some(",|xxx,b|abc1234"));
+        let names: Vec<&str> = list.iter().map(|e| e.name()).collect();
+        assert_eq!(names, vec!["b"]);
+    }
+
+    #[test]
+    fn alias_with_hash_list_display_empty() {
+        assert_eq!(AliasWithHashList::new().to_string(), "");
+    }
+
     #[test]
     fn empty_inputs_produce_empty_diff() {
         let diff = Precedence::new().resolve();
diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs
index 9feff619..a25d5049 100644
--- a/crates/am/src/update.rs
+++ b/crates/am/src/update.rs
@@ -591,21 +591,19 @@ pub fn update(model: &mut AppModel, message: Message) -> Result =
-                    std::collections::BTreeSet::new();
-                for raw in prev_global.split(',') {
-                    let name = raw.split_once('|').map_or(raw, |(n, _)| n);
-                    if !name.is_empty() && !name.contains(':') {
-                        names.insert(name.to_string());
-                    }
-                }
                 // Per-key subcommand entries (containing ':') are tracking-only,
                 // not shell functions. Program-level wrapper names (no ':') are
-                // picked up from prev_global because PrecedenceDiff::render writes them there.
-                let _ = prev_subs;
+                // picked up from prev_global because PrecedenceDiff::render writes
+                // them there alongside regular aliases.
+                let mut names: std::collections::BTreeSet =
+                    crate::precedence::AliasWithHashList::parse(prev_global.as_deref())
+                        .iter()
+                        .map(|e| e.name())
+                        .filter(|n| !n.contains(':'))
+                        .map(String::from)
+                        .collect();
 
                 // Union with shell introspection for bash/zsh.
                 match shell {

From 147ffd06c5ce30be60daa0b2ada92955c74125db Mon Sep 17 00:00:00 2001
From: Sven Kanoldt 
Date: Thu, 23 Apr 2026 12:37:33 +0200
Subject: [PATCH 36/38] refactor: split precedence.rs into a folder module
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

1015-line precedence.rs had three cohesive groups: env-var wire format,
diff output types with rendering, and the Precedence builder engine.
Split into separate files under crates/am/src/precedence/ so each file
holds one responsibility and stays a comfortable read.

- precedence/mod.rs       — module declarations + re-exports only
- precedence/env_state.rs — AliasWithHash + AliasWithHashList (~193 lines)
- precedence/diff.rs      — EntryKind, EffectiveEntry, PrecedenceDiff
                            with change_summary + render (~194 lines)
- precedence/engine.rs    — Precedence builder + resolve + hashing helpers
                            (~653 lines)

Public API is unchanged. All external uses of
amoxide::precedence::{Precedence, PrecedenceDiff, EntryKind,
EffectiveEntry, AliasWithHash, AliasWithHashList} still resolve. No
signatures, behavior, or test count changed.
---
 crates/am/src/precedence/diff.rs              | 194 +++++++++
 .../{precedence.rs => precedence/engine.rs}   | 368 +-----------------
 crates/am/src/precedence/env_state.rs         | 193 +++++++++
 crates/am/src/precedence/mod.rs               |  16 +
 4 files changed, 406 insertions(+), 365 deletions(-)
 create mode 100644 crates/am/src/precedence/diff.rs
 rename crates/am/src/{precedence.rs => precedence/engine.rs} (67%)
 create mode 100644 crates/am/src/precedence/env_state.rs
 create mode 100644 crates/am/src/precedence/mod.rs

diff --git a/crates/am/src/precedence/diff.rs b/crates/am/src/precedence/diff.rs
new file mode 100644
index 00000000..ba73be68
--- /dev/null
+++ b/crates/am/src/precedence/diff.rs
@@ -0,0 +1,194 @@
+use crate::alias::TomlAlias;
+use crate::env_vars;
+use crate::shell::ShellAdapter;
+use crate::subcommand::SubcommandEntry;
+
+use super::env_state::{AliasWithHash, AliasWithHashList};
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum EntryKind {
+    Alias(TomlAlias),
+    SubcommandWrapper {
+        program: String,
+        entries: Vec,
+        base_cmd: Option,
+    },
+    /// Per-key subcommand entry tracked in `_AM_SUBCOMMANDS` for fine-grained
+    /// change detection. Never emitted as shell code — the program-level
+    /// `SubcommandWrapper` is the shell-visible unit.
+    SubcommandKey {
+        longs: Vec,
+    },
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct EffectiveEntry {
+    pub name: String,
+    pub kind: EntryKind,
+    pub hash: String,
+}
+
+#[derive(Debug, Default, Clone, PartialEq)]
+pub struct PrecedenceDiff {
+    pub added: Vec,
+    pub changed: Vec,
+    pub removed: Vec,
+    pub unchanged: Vec,
+}
+
+impl PrecedenceDiff {
+    /// Human-readable summary of what changed, suitable for echoing to the
+    /// user (e.g. `"am: aliases changed — 1 loaded: b | 1 unloaded: t"`).
+    ///
+    /// Returns `None` when nothing changed so callers can stay silent.
+    pub fn change_summary(&self) -> Option {
+        let added: Vec<&str> = self.added.iter().map(|e| e.name.as_str()).collect();
+        let changed: Vec<&str> = self.changed.iter().map(|e| e.name.as_str()).collect();
+        let removed: Vec<&str> = self.removed.iter().map(|s| s.as_str()).collect();
+        let parts: Vec = [
+            ("loaded", &added[..]),
+            ("updated", &changed[..]),
+            ("unloaded", &removed[..]),
+        ]
+        .iter()
+        .filter(|(_, names)| !names.is_empty())
+        .map(|(verb, names)| format!("{} {verb}: {}", names.len(), names.join(", ")))
+        .collect();
+        if parts.is_empty() {
+            None
+        } else {
+            Some(format!("am: aliases changed — {}", parts.join(" | ")))
+        }
+    }
+
+    /// Render this diff into shell code using the given adapter.
+    ///
+    /// Emission order:
+    ///   1. unload (removed + changed) — skipping subcommand-key names
+    ///      (they're tracking-only, not shell functions)
+    ///   2. load (added + changed)
+    ///   3. set `_AM_ALIASES` / `_AM_SUBCOMMANDS` to the union of added +
+    ///      changed + unchanged
+    pub fn render(&self, shell: &dyn ShellAdapter) -> String {
+        let mut lines: Vec = Vec::new();
+
+        // 1. Unload
+        for name in &self.removed {
+            if name.contains(':') {
+                continue;
+            }
+            lines.push(shell.unalias(name));
+        }
+        for entry in &self.changed {
+            if matches!(entry.kind, EntryKind::SubcommandKey { .. }) {
+                continue;
+            }
+            if entry.name.contains(':') {
+                continue;
+            }
+            lines.push(shell.unalias(&entry.name));
+        }
+
+        // 2. Load (added + changed)
+        for entry in self.added.iter().chain(self.changed.iter()) {
+            match &entry.kind {
+                EntryKind::Alias(alias) => {
+                    lines.push(shell.alias(&alias.as_entry(&entry.name)));
+                }
+                EntryKind::SubcommandWrapper {
+                    program,
+                    entries,
+                    base_cmd,
+                } => {
+                    let cmd = base_cmd
+                        .clone()
+                        .unwrap_or_else(|| format!("command {program}"));
+                    lines.push(shell.subcommand_wrapper(program, &cmd, entries));
+                }
+                EntryKind::SubcommandKey { .. } => {}
+            }
+        }
+
+        // 3. Update tracking env vars
+        let mut alias_list = AliasWithHashList::new();
+        let mut sub_list = AliasWithHashList::new();
+        for e in self
+            .added
+            .iter()
+            .chain(self.changed.iter())
+            .chain(self.unchanged.iter())
+        {
+            let entry = AliasWithHash::new(&e.name, Some(e.hash.clone()));
+            match &e.kind {
+                EntryKind::SubcommandKey { .. } => sub_list.push(entry),
+                _ => alias_list.push(entry),
+            }
+        }
+
+        if !alias_list.is_empty() {
+            lines.push(shell.set_env(env_vars::AM_ALIASES, &alias_list.to_string()));
+        }
+        if !sub_list.is_empty() {
+            lines.push(shell.set_env(env_vars::AM_SUBCOMMANDS, &sub_list.to_string()));
+        }
+
+        lines.join("\n")
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::super::engine::Precedence;
+    use super::*;
+    use crate::alias::{AliasName, AliasSet};
+    use crate::config::ShellsTomlConfig;
+    use crate::shell::Shell;
+    use crate::subcommand::SubcommandSet;
+
+    fn aset(pairs: &[(&str, &str)]) -> AliasSet {
+        let mut s = AliasSet::default();
+        for (n, c) in pairs {
+            s.insert(AliasName::from(*n), TomlAlias::Command((*c).into()));
+        }
+        s
+    }
+
+    #[test]
+    fn render_emits_unloads_then_loads_then_env() {
+        let cfg = ShellsTomlConfig::default();
+        let shell = Shell::Fish.as_shell(&cfg, Default::default(), Default::default());
+
+        // Previous shell state: `b|0000000,gone|aaa` ; new effective: `b|make build`.
+        let project = aset(&[("b", "make build")]);
+        let diff = Precedence::new()
+            .with_project(&project, &SubcommandSet::new())
+            .with_shell_state_from_env(Some("b|0000000,gone|aaa"), None)
+            .resolve();
+
+        let out = diff.render(shell.as_ref());
+        assert!(
+            out.contains("functions -e gone"),
+            "gone must be unloaded: {out}"
+        );
+        assert!(
+            out.contains("functions -e b"),
+            "changed b must be unloaded: {out}"
+        );
+        assert!(
+            out.contains("function b\n    make build $argv\nend"),
+            "b must be reloaded: {out}"
+        );
+        // env-var update must be the last section
+        let env_pos = out.find("_AM_ALIASES").expect("env update missing");
+        let fn_pos = out.find("function b").unwrap();
+        assert!(env_pos > fn_pos, "env update must come after loads");
+    }
+
+    #[test]
+    fn render_empty_diff_produces_empty_string() {
+        let cfg = ShellsTomlConfig::default();
+        let shell = Shell::Fish.as_shell(&cfg, Default::default(), Default::default());
+        let out = PrecedenceDiff::default().render(shell.as_ref());
+        assert!(out.is_empty());
+    }
+}
diff --git a/crates/am/src/precedence.rs b/crates/am/src/precedence/engine.rs
similarity index 67%
rename from crates/am/src/precedence.rs
rename to crates/am/src/precedence/engine.rs
index 78770986..8445b839 100644
--- a/crates/am/src/precedence.rs
+++ b/crates/am/src/precedence/engine.rs
@@ -1,165 +1,10 @@
 use std::collections::{BTreeMap, BTreeSet, HashSet};
-use std::fmt;
 
 use crate::alias::{AliasSet, TomlAlias};
-use crate::env_vars;
-use crate::shell::ShellAdapter;
-use crate::subcommand::{SubcommandEntry, SubcommandSet};
-
-/// One entry in the `_AM_ALIASES` / `_AM_SUBCOMMANDS` env var, in the
-/// `"name|hash"` format (or a legacy bare `"name"` with no hash).
-///
-/// `hash = None` means the shell reloaded from an older amoxide that only
-/// tracked names; the diff treats such entries as "always differs" so they
-/// get reloaded on the next sync.
-#[derive(Debug, Clone, PartialEq)]
-pub struct AliasWithHash {
-    name: String,
-    hash: Option,
-}
-
-impl AliasWithHash {
-    pub fn new(name: impl Into, hash: Option) -> Self {
-        Self {
-            name: name.into(),
-            hash,
-        }
-    }
-
-    pub fn name(&self) -> &str {
-        &self.name
-    }
-
-    pub fn hash(&self) -> Option<&str> {
-        self.hash.as_deref()
-    }
-
-    /// Parse one `"name|hash"` (or bare `"name"`) token. Returns `None` when
-    /// the name segment is empty — callers skip such entries silently.
-    pub fn parse(token: &str) -> Option {
-        match token.split_once('|') {
-            Some((name, hash)) if !name.is_empty() => Some(Self {
-                name: name.to_string(),
-                hash: Some(hash.to_string()),
-            }),
-            Some(_) => None, // empty name before '|'
-            None if token.is_empty() => None,
-            None => Some(Self {
-                name: token.to_string(),
-                hash: None,
-            }),
-        }
-    }
-}
-
-impl fmt::Display for AliasWithHash {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        match &self.hash {
-            Some(h) => write!(f, "{}|{}", self.name, h),
-            None => write!(f, "{}", self.name),
-        }
-    }
-}
+use crate::subcommand::SubcommandSet;
 
-/// A comma-separated list of [`AliasWithHash`] entries — the on-the-wire
-/// format of `_AM_ALIASES` and `_AM_SUBCOMMANDS`.
-///
-/// Owns round-trip parsing and rendering so no other module has to know
-/// about the `"name|hash,name|hash,..."` layout.
-#[derive(Debug, Default, Clone, PartialEq)]
-pub struct AliasWithHashList(Vec);
-
-impl AliasWithHashList {
-    pub fn new() -> Self {
-        Self::default()
-    }
-
-    pub fn push(&mut self, entry: AliasWithHash) {
-        self.0.push(entry);
-    }
-
-    pub fn is_empty(&self) -> bool {
-        self.0.is_empty()
-    }
-
-    pub fn iter(&self) -> std::slice::Iter<'_, AliasWithHash> {
-        self.0.iter()
-    }
-
-    /// Parse an `_AM_ALIASES` / `_AM_SUBCOMMANDS` value. `None` or empty
-    /// string yields an empty list; malformed tokens are skipped.
-    pub fn parse(raw: Option<&str>) -> Self {
-        let Some(s) = raw.filter(|s| !s.is_empty()) else {
-            return Self::new();
-        };
-        Self(s.split(',').filter_map(AliasWithHash::parse).collect())
-    }
-}
-
-impl fmt::Display for AliasWithHashList {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        for (i, entry) in self.0.iter().enumerate() {
-            if i > 0 {
-                f.write_str(",")?;
-            }
-            write!(f, "{entry}")?;
-        }
-        Ok(())
-    }
-}
-
-impl FromIterator for AliasWithHashList {
-    fn from_iter>(iter: I) -> Self {
-        Self(iter.into_iter().collect())
-    }
-}
-
-impl<'a> IntoIterator for &'a AliasWithHashList {
-    type Item = &'a AliasWithHash;
-    type IntoIter = std::slice::Iter<'a, AliasWithHash>;
-    fn into_iter(self) -> Self::IntoIter {
-        self.0.iter()
-    }
-}
-
-impl IntoIterator for AliasWithHashList {
-    type Item = AliasWithHash;
-    type IntoIter = std::vec::IntoIter;
-    fn into_iter(self) -> Self::IntoIter {
-        self.0.into_iter()
-    }
-}
-
-#[derive(Debug, Clone, PartialEq)]
-pub enum EntryKind {
-    Alias(TomlAlias),
-    SubcommandWrapper {
-        program: String,
-        entries: Vec,
-        base_cmd: Option,
-    },
-    /// Per-key subcommand entry tracked in `_AM_SUBCOMMANDS` for fine-grained
-    /// change detection. Never emitted as shell code — the program-level
-    /// `SubcommandWrapper` is the shell-visible unit.
-    SubcommandKey {
-        longs: Vec,
-    },
-}
-
-#[derive(Debug, Clone, PartialEq)]
-pub struct EffectiveEntry {
-    pub name: String,
-    pub kind: EntryKind,
-    pub hash: String,
-}
-
-#[derive(Debug, Default, Clone, PartialEq)]
-pub struct PrecedenceDiff {
-    pub added: Vec,
-    pub changed: Vec,
-    pub removed: Vec,
-    pub unchanged: Vec,
-}
+use super::diff::{EffectiveEntry, EntryKind, PrecedenceDiff};
+use super::env_state::AliasWithHashList;
 
 #[derive(Debug, Default)]
 pub struct Precedence {
@@ -413,176 +258,11 @@ impl Precedence {
     }
 }
 
-impl PrecedenceDiff {
-    /// Human-readable summary of what changed, suitable for echoing to the
-    /// user (e.g. `"am: aliases changed — 1 loaded: b | 1 unloaded: t"`).
-    ///
-    /// Returns `None` when nothing changed so callers can stay silent.
-    pub fn change_summary(&self) -> Option {
-        let added: Vec<&str> = self.added.iter().map(|e| e.name.as_str()).collect();
-        let changed: Vec<&str> = self.changed.iter().map(|e| e.name.as_str()).collect();
-        let removed: Vec<&str> = self.removed.iter().map(|s| s.as_str()).collect();
-        let parts: Vec = [
-            ("loaded", &added[..]),
-            ("updated", &changed[..]),
-            ("unloaded", &removed[..]),
-        ]
-        .iter()
-        .filter(|(_, names)| !names.is_empty())
-        .map(|(verb, names)| format!("{} {verb}: {}", names.len(), names.join(", ")))
-        .collect();
-        if parts.is_empty() {
-            None
-        } else {
-            Some(format!("am: aliases changed — {}", parts.join(" | ")))
-        }
-    }
-
-    /// Render this diff into shell code using the given adapter.
-    ///
-    /// Emission order:
-    ///   1. unload (removed + changed) — skipping subcommand-key names
-    ///      (they're tracking-only, not shell functions)
-    ///   2. load (added + changed)
-    ///   3. set `_AM_ALIASES` / `_AM_SUBCOMMANDS` to the union of added +
-    ///      changed + unchanged
-    pub fn render(&self, shell: &dyn ShellAdapter) -> String {
-        let mut lines: Vec = Vec::new();
-
-        // 1. Unload
-        for name in &self.removed {
-            if name.contains(':') {
-                continue;
-            }
-            lines.push(shell.unalias(name));
-        }
-        for entry in &self.changed {
-            if matches!(entry.kind, EntryKind::SubcommandKey { .. }) {
-                continue;
-            }
-            if entry.name.contains(':') {
-                continue;
-            }
-            lines.push(shell.unalias(&entry.name));
-        }
-
-        // 2. Load (added + changed)
-        for entry in self.added.iter().chain(self.changed.iter()) {
-            match &entry.kind {
-                EntryKind::Alias(alias) => {
-                    lines.push(shell.alias(&alias.as_entry(&entry.name)));
-                }
-                EntryKind::SubcommandWrapper {
-                    program,
-                    entries,
-                    base_cmd,
-                } => {
-                    let cmd = base_cmd
-                        .clone()
-                        .unwrap_or_else(|| format!("command {program}"));
-                    lines.push(shell.subcommand_wrapper(program, &cmd, entries));
-                }
-                EntryKind::SubcommandKey { .. } => {}
-            }
-        }
-
-        // 3. Update tracking env vars
-        let mut alias_list = AliasWithHashList::new();
-        let mut sub_list = AliasWithHashList::new();
-        for e in self
-            .added
-            .iter()
-            .chain(self.changed.iter())
-            .chain(self.unchanged.iter())
-        {
-            let entry = AliasWithHash::new(&e.name, Some(e.hash.clone()));
-            match &e.kind {
-                EntryKind::SubcommandKey { .. } => sub_list.push(entry),
-                _ => alias_list.push(entry),
-            }
-        }
-
-        if !alias_list.is_empty() {
-            lines.push(shell.set_env(env_vars::AM_ALIASES, &alias_list.to_string()));
-        }
-        if !sub_list.is_empty() {
-            lines.push(shell.set_env(env_vars::AM_SUBCOMMANDS, &sub_list.to_string()));
-        }
-
-        lines.join("\n")
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
     use crate::alias::AliasName;
 
-    // ─── AliasWithHash / AliasWithHashList ──────────────────────────────
-
-    #[test]
-    fn alias_with_hash_parse_new_format() {
-        let e = AliasWithHash::parse("b|abc1234").unwrap();
-        assert_eq!(e.name(), "b");
-        assert_eq!(e.hash(), Some("abc1234"));
-    }
-
-    #[test]
-    fn alias_with_hash_parse_bare_name() {
-        let e = AliasWithHash::parse("t").unwrap();
-        assert_eq!(e.name(), "t");
-        assert_eq!(e.hash(), None);
-    }
-
-    #[test]
-    fn alias_with_hash_parse_empty_returns_none() {
-        assert!(AliasWithHash::parse("").is_none());
-        assert!(AliasWithHash::parse("|abc").is_none());
-    }
-
-    #[test]
-    fn alias_with_hash_display_roundtrip() {
-        assert_eq!(
-            AliasWithHash::new("b", Some("abc1234".into())).to_string(),
-            "b|abc1234"
-        );
-        assert_eq!(AliasWithHash::new("t", None).to_string(), "t");
-    }
-
-    #[test]
-    fn alias_with_hash_list_parse_and_render() {
-        let list = AliasWithHashList::parse(Some("b|abc1234,t|def5678"));
-        assert_eq!(list.iter().count(), 2);
-        assert_eq!(list.to_string(), "b|abc1234,t|def5678");
-    }
-
-    #[test]
-    fn alias_with_hash_list_parse_mixed_format() {
-        let list = AliasWithHashList::parse(Some("b|abc1234,t,gs|fed9876"));
-        let names: Vec<&str> = list.iter().map(|e| e.name()).collect();
-        assert_eq!(names, vec!["b", "t", "gs"]);
-        assert_eq!(list.iter().nth(1).unwrap().hash(), None);
-    }
-
-    #[test]
-    fn alias_with_hash_list_parse_empty_and_none() {
-        assert!(AliasWithHashList::parse(None).is_empty());
-        assert!(AliasWithHashList::parse(Some("")).is_empty());
-    }
-
-    #[test]
-    fn alias_with_hash_list_parse_skips_malformed_tokens() {
-        // Leading empty token and "|xxx" token get dropped silently.
-        let list = AliasWithHashList::parse(Some(",|xxx,b|abc1234"));
-        let names: Vec<&str> = list.iter().map(|e| e.name()).collect();
-        assert_eq!(names, vec!["b"]);
-    }
-
-    #[test]
-    fn alias_with_hash_list_display_empty() {
-        assert_eq!(AliasWithHashList::new().to_string(), "");
-    }
-
     #[test]
     fn empty_inputs_produce_empty_diff() {
         let diff = Precedence::new().resolve();
@@ -970,46 +650,4 @@ mod tests {
             "jj:ab entry itself is unchanged"
         );
     }
-
-    use crate::config::ShellsTomlConfig;
-    use crate::shell::Shell;
-
-    #[test]
-    fn render_emits_unloads_then_loads_then_env() {
-        let cfg = ShellsTomlConfig::default();
-        let shell = Shell::Fish.as_shell(&cfg, Default::default(), Default::default());
-
-        // Previous shell state: `b|0000000,gone|aaa` ; new effective: `b|make build`.
-        let project = aset(&[("b", "make build")]);
-        let diff = Precedence::new()
-            .with_project(&project, &SubcommandSet::new())
-            .with_shell_state_from_env(Some("b|0000000,gone|aaa"), None)
-            .resolve();
-
-        let out = diff.render(shell.as_ref());
-        assert!(
-            out.contains("functions -e gone"),
-            "gone must be unloaded: {out}"
-        );
-        assert!(
-            out.contains("functions -e b"),
-            "changed b must be unloaded: {out}"
-        );
-        assert!(
-            out.contains("function b\n    make build $argv\nend"),
-            "b must be reloaded: {out}"
-        );
-        // env-var update must be the last section
-        let env_pos = out.find("_AM_ALIASES").expect("env update missing");
-        let fn_pos = out.find("function b").unwrap();
-        assert!(env_pos > fn_pos, "env update must come after loads");
-    }
-
-    #[test]
-    fn render_empty_diff_produces_empty_string() {
-        let cfg = ShellsTomlConfig::default();
-        let shell = Shell::Fish.as_shell(&cfg, Default::default(), Default::default());
-        let out = PrecedenceDiff::default().render(shell.as_ref());
-        assert!(out.is_empty());
-    }
 }
diff --git a/crates/am/src/precedence/env_state.rs b/crates/am/src/precedence/env_state.rs
new file mode 100644
index 00000000..d1a262ef
--- /dev/null
+++ b/crates/am/src/precedence/env_state.rs
@@ -0,0 +1,193 @@
+use std::fmt;
+
+/// One entry in the `_AM_ALIASES` / `_AM_SUBCOMMANDS` env var, in the
+/// `"name|hash"` format (or a legacy bare `"name"` with no hash).
+///
+/// `hash = None` means the shell reloaded from an older amoxide that only
+/// tracked names; the diff treats such entries as "always differs" so they
+/// get reloaded on the next sync.
+#[derive(Debug, Clone, PartialEq)]
+pub struct AliasWithHash {
+    name: String,
+    hash: Option,
+}
+
+impl AliasWithHash {
+    pub fn new(name: impl Into, hash: Option) -> Self {
+        Self {
+            name: name.into(),
+            hash,
+        }
+    }
+
+    pub fn name(&self) -> &str {
+        &self.name
+    }
+
+    pub fn hash(&self) -> Option<&str> {
+        self.hash.as_deref()
+    }
+
+    /// Parse one `"name|hash"` (or bare `"name"`) token. Returns `None` when
+    /// the name segment is empty — callers skip such entries silently.
+    pub fn parse(token: &str) -> Option {
+        match token.split_once('|') {
+            Some((name, hash)) if !name.is_empty() => Some(Self {
+                name: name.to_string(),
+                hash: Some(hash.to_string()),
+            }),
+            Some(_) => None, // empty name before '|'
+            None if token.is_empty() => None,
+            None => Some(Self {
+                name: token.to_string(),
+                hash: None,
+            }),
+        }
+    }
+}
+
+impl fmt::Display for AliasWithHash {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match &self.hash {
+            Some(h) => write!(f, "{}|{}", self.name, h),
+            None => write!(f, "{}", self.name),
+        }
+    }
+}
+
+/// A comma-separated list of [`AliasWithHash`] entries — the on-the-wire
+/// format of `_AM_ALIASES` and `_AM_SUBCOMMANDS`.
+///
+/// Owns round-trip parsing and rendering so no other module has to know
+/// about the `"name|hash,name|hash,..."` layout.
+#[derive(Debug, Default, Clone, PartialEq)]
+pub struct AliasWithHashList(Vec);
+
+impl AliasWithHashList {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    pub fn push(&mut self, entry: AliasWithHash) {
+        self.0.push(entry);
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+
+    pub fn iter(&self) -> std::slice::Iter<'_, AliasWithHash> {
+        self.0.iter()
+    }
+
+    /// Parse an `_AM_ALIASES` / `_AM_SUBCOMMANDS` value. `None` or empty
+    /// string yields an empty list; malformed tokens are skipped.
+    pub fn parse(raw: Option<&str>) -> Self {
+        let Some(s) = raw.filter(|s| !s.is_empty()) else {
+            return Self::new();
+        };
+        Self(s.split(',').filter_map(AliasWithHash::parse).collect())
+    }
+}
+
+impl fmt::Display for AliasWithHashList {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        for (i, entry) in self.0.iter().enumerate() {
+            if i > 0 {
+                f.write_str(",")?;
+            }
+            write!(f, "{entry}")?;
+        }
+        Ok(())
+    }
+}
+
+impl FromIterator for AliasWithHashList {
+    fn from_iter>(iter: I) -> Self {
+        Self(iter.into_iter().collect())
+    }
+}
+
+impl<'a> IntoIterator for &'a AliasWithHashList {
+    type Item = &'a AliasWithHash;
+    type IntoIter = std::slice::Iter<'a, AliasWithHash>;
+    fn into_iter(self) -> Self::IntoIter {
+        self.0.iter()
+    }
+}
+
+impl IntoIterator for AliasWithHashList {
+    type Item = AliasWithHash;
+    type IntoIter = std::vec::IntoIter;
+    fn into_iter(self) -> Self::IntoIter {
+        self.0.into_iter()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn alias_with_hash_parse_new_format() {
+        let e = AliasWithHash::parse("b|abc1234").unwrap();
+        assert_eq!(e.name(), "b");
+        assert_eq!(e.hash(), Some("abc1234"));
+    }
+
+    #[test]
+    fn alias_with_hash_parse_bare_name() {
+        let e = AliasWithHash::parse("t").unwrap();
+        assert_eq!(e.name(), "t");
+        assert_eq!(e.hash(), None);
+    }
+
+    #[test]
+    fn alias_with_hash_parse_empty_returns_none() {
+        assert!(AliasWithHash::parse("").is_none());
+        assert!(AliasWithHash::parse("|abc").is_none());
+    }
+
+    #[test]
+    fn alias_with_hash_display_roundtrip() {
+        assert_eq!(
+            AliasWithHash::new("b", Some("abc1234".into())).to_string(),
+            "b|abc1234"
+        );
+        assert_eq!(AliasWithHash::new("t", None).to_string(), "t");
+    }
+
+    #[test]
+    fn alias_with_hash_list_parse_and_render() {
+        let list = AliasWithHashList::parse(Some("b|abc1234,t|def5678"));
+        assert_eq!(list.iter().count(), 2);
+        assert_eq!(list.to_string(), "b|abc1234,t|def5678");
+    }
+
+    #[test]
+    fn alias_with_hash_list_parse_mixed_format() {
+        let list = AliasWithHashList::parse(Some("b|abc1234,t,gs|fed9876"));
+        let names: Vec<&str> = list.iter().map(|e| e.name()).collect();
+        assert_eq!(names, vec!["b", "t", "gs"]);
+        assert_eq!(list.iter().nth(1).unwrap().hash(), None);
+    }
+
+    #[test]
+    fn alias_with_hash_list_parse_empty_and_none() {
+        assert!(AliasWithHashList::parse(None).is_empty());
+        assert!(AliasWithHashList::parse(Some("")).is_empty());
+    }
+
+    #[test]
+    fn alias_with_hash_list_parse_skips_malformed_tokens() {
+        // Leading empty token and "|xxx" token get dropped silently.
+        let list = AliasWithHashList::parse(Some(",|xxx,b|abc1234"));
+        let names: Vec<&str> = list.iter().map(|e| e.name()).collect();
+        assert_eq!(names, vec!["b"]);
+    }
+
+    #[test]
+    fn alias_with_hash_list_display_empty() {
+        assert_eq!(AliasWithHashList::new().to_string(), "");
+    }
+}
diff --git a/crates/am/src/precedence/mod.rs b/crates/am/src/precedence/mod.rs
new file mode 100644
index 00000000..0245140e
--- /dev/null
+++ b/crates/am/src/precedence/mod.rs
@@ -0,0 +1,16 @@
+//! Precedence resolution: merge global/profile/project alias layers against
+//! the current shell's loaded state to produce a diff that tells the shell
+//! exactly what to load, reload, or unload.
+//!
+//! Split into three submodules:
+//!   * [`env_state`] — `_AM_ALIASES` / `_AM_SUBCOMMANDS` wire format.
+//!   * [`diff`] — the `PrecedenceDiff` output and how it renders to shell code.
+//!   * [`engine`] — the `Precedence` builder and `resolve()` logic.
+
+mod diff;
+mod engine;
+mod env_state;
+
+pub use diff::{EffectiveEntry, EntryKind, PrecedenceDiff};
+pub use engine::Precedence;
+pub use env_state::{AliasWithHash, AliasWithHashList};

From 5b9262c60d5ae77b6728d1d9fabecdf1614bf5e6 Mon Sep 17 00:00:00 2001
From: Sven Kanoldt 
Date: Thu, 23 Apr 2026 13:15:31 +0200
Subject: [PATCH 37/38] refactor: share format_change_summary between
 diff.change_summary and profile toggle
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

PrecedenceDiff::change_summary and profile_toggle_message had the same
inner shape: filter non-empty sections, render each as 'N verb: names',
join with ' | ', prefix with a head + ' — '. Only the head and verbs
differ.

Extract the inner logic to a single pub(crate) helper in precedence/diff.rs:

    format_change_summary(head, &[(verb, &names), ...]) -> Option

- PrecedenceDiff::change_summary now calls through it with the fixed
  head 'am: aliases changed' and diff-shaped sections.
- profile_toggle_message imports it via
  `use crate::precedence::format_change_summary;` and passes its own
  head + shadow-aware sections.
- update.rs drops its duplicate helper.

The profile path still keeps its special 'all N shadowed by .aliases'
phrasing inline — sync has no equivalent shape.
---
 crates/am/src/precedence/diff.rs | 42 +++++++++++++++++++++-----------
 crates/am/src/precedence/mod.rs  |  1 +
 crates/am/src/update.rs          | 21 +---------------
 3 files changed, 30 insertions(+), 34 deletions(-)

diff --git a/crates/am/src/precedence/diff.rs b/crates/am/src/precedence/diff.rs
index ba73be68..47e99b7d 100644
--- a/crates/am/src/precedence/diff.rs
+++ b/crates/am/src/precedence/diff.rs
@@ -36,6 +36,26 @@ pub struct PrecedenceDiff {
     pub unchanged: Vec,
 }
 
+/// Build a change-summary line like
+/// `" — N verb1: a, b | M verb2: c"`.
+///
+/// Empty sections are skipped; returns `None` when every section is empty so
+/// callers can stay silent. Shared between [`PrecedenceDiff::change_summary`]
+/// (fixed head, shell-state diff verbs) and the profile-toggle message in
+/// `update.rs` (dynamic head, shadow-aware verbs).
+pub(crate) fn format_change_summary(head: &str, sections: &[(&str, &[&str])]) -> Option {
+    let parts: Vec = sections
+        .iter()
+        .filter(|(_, names)| !names.is_empty())
+        .map(|(verb, names)| format!("{} {verb}: {}", names.len(), names.join(", ")))
+        .collect();
+    if parts.is_empty() {
+        None
+    } else {
+        Some(format!("{head} — {}", parts.join(" | ")))
+    }
+}
+
 impl PrecedenceDiff {
     /// Human-readable summary of what changed, suitable for echoing to the
     /// user (e.g. `"am: aliases changed — 1 loaded: b | 1 unloaded: t"`).
@@ -45,20 +65,14 @@ impl PrecedenceDiff {
         let added: Vec<&str> = self.added.iter().map(|e| e.name.as_str()).collect();
         let changed: Vec<&str> = self.changed.iter().map(|e| e.name.as_str()).collect();
         let removed: Vec<&str> = self.removed.iter().map(|s| s.as_str()).collect();
-        let parts: Vec = [
-            ("loaded", &added[..]),
-            ("updated", &changed[..]),
-            ("unloaded", &removed[..]),
-        ]
-        .iter()
-        .filter(|(_, names)| !names.is_empty())
-        .map(|(verb, names)| format!("{} {verb}: {}", names.len(), names.join(", ")))
-        .collect();
-        if parts.is_empty() {
-            None
-        } else {
-            Some(format!("am: aliases changed — {}", parts.join(" | ")))
-        }
+        format_change_summary(
+            "am: aliases changed",
+            &[
+                ("loaded", &added),
+                ("updated", &changed),
+                ("unloaded", &removed),
+            ],
+        )
     }
 
     /// Render this diff into shell code using the given adapter.
diff --git a/crates/am/src/precedence/mod.rs b/crates/am/src/precedence/mod.rs
index 0245140e..0a6d7092 100644
--- a/crates/am/src/precedence/mod.rs
+++ b/crates/am/src/precedence/mod.rs
@@ -11,6 +11,7 @@ mod diff;
 mod engine;
 mod env_state;
 
+pub(crate) use diff::format_change_summary;
 pub use diff::{EffectiveEntry, EntryKind, PrecedenceDiff};
 pub use engine::Precedence;
 pub use env_state::{AliasWithHash, AliasWithHashList};
diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs
index a25d5049..63ed1a81 100644
--- a/crates/am/src/update.rs
+++ b/crates/am/src/update.rs
@@ -4,7 +4,7 @@ use crate::display::render_listing;
 use crate::effects::Effect;
 use crate::env_vars;
 use crate::init::generate_init;
-use crate::precedence::Precedence;
+use crate::precedence::{format_change_summary, Precedence};
 use crate::project::ProjectAliases;
 use crate::shell::bash;
 use crate::shell::zsh;
@@ -1005,25 +1005,6 @@ fn profile_toggle_message(
     .expect("profile_aliases non-empty but produced no sections")
 }
 
-/// Build a full change-summary line like
-/// `" — N verb1: a, b | M verb2: c"`. Empty sections are skipped.
-/// Returns `None` when every section is empty (caller decides to stay silent).
-///
-/// Kept as a private helper for `profile_toggle_message`. The sync handler's
-/// equivalent lives on the diff itself as [`PrecedenceDiff::change_summary`].
-fn format_change_summary(head: &str, sections: &[(&str, &[&str])]) -> Option {
-    let parts: Vec = sections
-        .iter()
-        .filter(|(_, names)| !names.is_empty())
-        .map(|(verb, names)| format!("{} {verb}: {}", names.len(), names.join(", ")))
-        .collect();
-    if parts.is_empty() {
-        None
-    } else {
-        Some(format!("{head} — {}", parts.join(" | ")))
-    }
-}
-
 fn resolve_profile<'a>(
     model: &'a AppModel,
     target: &AliasTarget,

From c30bc9c63c688dfa2b5a4a74b46f46da298b79c5 Mon Sep 17 00:00:00 2001
From: Sven Kanoldt 
Date: Thu, 23 Apr 2026 13:20:45 +0200
Subject: [PATCH 38/38] fix: drop rustdoc intra-doc links to private submodules
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

`precedence/mod.rs` had `[\`env_state\`]` / `[\`diff\`]` / `[\`engine\`]`
intra-doc links pointing at private submodules, which
`rustdoc::private-intra-doc-links` flags when the lint is escalated to
deny. Use plain backticks for code-style formatting — the module names
aren't hyperlink targets in a public API listing anyway.
---
 crates/am/src/precedence/mod.rs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/crates/am/src/precedence/mod.rs b/crates/am/src/precedence/mod.rs
index 0a6d7092..1d6d4160 100644
--- a/crates/am/src/precedence/mod.rs
+++ b/crates/am/src/precedence/mod.rs
@@ -3,9 +3,9 @@
 //! exactly what to load, reload, or unload.
 //!
 //! Split into three submodules:
-//!   * [`env_state`] — `_AM_ALIASES` / `_AM_SUBCOMMANDS` wire format.
-//!   * [`diff`] — the `PrecedenceDiff` output and how it renders to shell code.
-//!   * [`engine`] — the `Precedence` builder and `resolve()` logic.
+//!   * `env_state` — `_AM_ALIASES` / `_AM_SUBCOMMANDS` wire format.
+//!   * `diff` — the `PrecedenceDiff` output and how it renders to shell code.
+//!   * `engine` — the `Precedence` builder and `resolve()` logic.
 
 mod diff;
 mod engine;