v0.4.1 : Phase 1 complete (17/17) - dashboard TUI (#33), favoris/notes/tags (#39), profils d'environnement (#40), completions dynamiques + man pages (#41), packaging officiel + CI (#42), self-update Gitea, state v3, 212 tests
release / linux (push) Failing after 23s
release / windows (push) Canceled after 0s
release / macos (push) Canceled after 0s

This commit is contained in:
2026-08-17 10:50:13 -04:00
parent 4f4cd034b2
commit 56732d672b
93 changed files with 9412 additions and 1270 deletions
+139 -10
View File
@@ -46,8 +46,12 @@ pub struct AmCompleter {
gateway_shells: Vec<&'static str>,
options: Vec<&'static str>,
agents: Vec<String>,
/// Whole catalog (install/info/search only).
catalog_agents: Vec<String>,
aliases: Vec<String>,
groups: Vec<String>,
/// 'profile' subcommands.
profile_sub: Vec<&'static str>,
/// Short descriptions shown next to candidates in the Tab menu
/// (Nushell-style).
descriptions: BTreeMap<String, String>,
@@ -90,6 +94,15 @@ const COMMAND_DESCRIPTIONS: &[(&str, &str)] = &[
("self-uninstall", "remove agent-manager"),
("export", "backup config + state"),
("import", "restore a backup"),
("dashboard", "live TUI dashboard of the fleet"),
("favorite", "mark an agent as a favorite"),
("unfavorite", "remove the favorite star"),
("note", "attach a note to an agent"),
("tag", "tag an agent (personal tags)"),
("untag", "remove a personal tag"),
("tags", "list personal tags"),
("profile", "manage environment profiles"),
("man", "generate man pages"),
("shell", "show or switch the system shell"),
("theme", "show or switch the color theme"),
("ls", "list files and directories (Nushell-style)"),
@@ -105,8 +118,30 @@ const COMMAND_DESCRIPTIONS: &[(&str, &str)] = &[
impl AmCompleter {
pub fn new(app: &App) -> Self {
let completer =
Self::from_catalog(&app.catalog, &app.config.aliases, &app.config.groups);
// Issue #41: Tab proposes the installed agents (managed + external),
// aliases and groups — not the whole catalog.
let installed: std::collections::BTreeSet<String> = app
.state
.all()
.unwrap_or_default()
.keys()
.cloned()
.collect();
let mut completer = Self::from_catalog(
&app.catalog,
&app.config.aliases,
&app.config.groups,
Some(&installed),
);
// External agents found on PATH are launchable too.
let probe = crate::probe::detect_externals(app);
for a in app.catalog.agents() {
if probe.path_of(&a.name).is_some() {
completer.agents.push(a.name.clone());
}
}
completer.agents.sort();
completer.agents.dedup();
completer.theme.set(app.theme());
completer
}
@@ -120,14 +155,23 @@ impl AmCompleter {
catalog: &crate::catalog::Catalog,
aliases: &BTreeMap<String, String>,
groups: &BTreeMap<String, Vec<String>>,
installed: Option<&std::collections::BTreeSet<String>>,
) -> Self {
let mut agents: Vec<String> = catalog
.agents()
.iter()
.filter(|a| !a.hidden)
.filter(|a| installed.map(|set| set.contains(&a.name)).unwrap_or(true))
.map(|a| a.name.clone())
.collect();
agents.sort();
// The whole catalog stays available for install/info/search.
let catalog_agents: Vec<String> = catalog
.agents()
.iter()
.filter(|a| !a.hidden)
.map(|a| a.name.clone())
.collect();
let mut alias_list: Vec<String> = aliases.keys().cloned().collect();
alias_list.sort();
let mut group_list: Vec<String> = groups.keys().map(|g| format!("group:{g}")).collect();
@@ -153,9 +197,12 @@ impl AmCompleter {
"list", "status", "sessions", "stats", "top", "report", "projects", "timeline", "log", "logs", "history", "init", "alias", "secret", "open", "watch", "search", "info", "install", "uninstall", "update",
"start", "stop", "restart", "run", "doctor", "config", "completion",
"self-update", "self-uninstall", "export", "import", "shell", "theme",
"dashboard", "favorite", "unfavorite", "note", "tag", "untag", "tags",
"profile", "man",
"ls", "dir", "cd", "ps", "where", "get", "help", "version", "exit",
],
config_sub: vec!["show", "path", "edit", "validate", "add"],
profile_sub: vec!["list", "show"],
shells: vec!["bash", "zsh", "fish", "powershell", "elvish"],
gateway_shells: vec![
"list", "bash", "zsh", "fish", "powershell", "pwsh", "cmd", "sh", "nu",
@@ -165,8 +212,10 @@ impl AmCompleter {
"--background", "--force", "--purge", "--method", "--all",
"--category", "--args", "--env", "--notify", "--timeout", "--yes",
"--verbose", "--quiet", "--json", "--dry-run", "--no-color",
"--profile", "--installed", "--tag", "--output",
],
agents,
catalog_agents,
aliases: alias_list,
groups: group_list,
descriptions,
@@ -194,6 +243,9 @@ impl AmCompleter {
"config" => {
pool = self.config_sub.iter().map(|s| s.to_string()).collect();
}
"profile" => {
pool = self.profile_sub.iter().map(|s| s.to_string()).collect();
}
"completion" => {
pool = self.shells.iter().map(|s| s.to_string()).collect();
}
@@ -216,8 +268,11 @@ impl AmCompleter {
let cwd = current_dir();
pool = crate::nav::path_candidates(&cwd, word, false);
}
"install" | "uninstall" | "update" | "start" | "stop" | "restart" | "run"
| "info" | "status" => {
// Issue #41: operational commands propose installed agents,
// aliases and groups only.
"start" | "stop" | "restart" | "run" | "status" | "logs" | "open"
| "watch" | "favorite" | "unfavorite" | "note" | "tag" | "untag"
| "sessions" | "stats" | "log" | "update" | "uninstall" => {
if word.starts_with("group:") {
pool = self.groups.clone();
} else if previous.len() == 1 {
@@ -231,6 +286,16 @@ impl AmCompleter {
pool = self.options.iter().map(|s| s.to_string()).collect();
}
}
"install" | "info" | "search" => {
if word.starts_with('-') {
pool = self.options.iter().map(|s| s.to_string()).collect();
} else if previous.len() == 1 {
pool = self.catalog_agents.clone();
pool.extend(self.aliases.iter().cloned());
} else {
pool = self.options.iter().map(|s| s.to_string()).collect();
}
}
_ => {
if word.starts_with('-') {
pool = self.options.iter().map(|s| s.to_string()).collect();
@@ -1270,6 +1335,47 @@ fn handle_line(
return Ok(false);
}
},
"dashboard" => Command::Dashboard,
"favorite" => Command::Favorite {
agent: need("agent name")?,
},
"unfavorite" => Command::Unfavorite {
agent: need("agent name")?,
},
"note" => Command::Note {
agent: need("agent name")?,
text: rest[1..].to_vec(),
},
"tag" => Command::Tag {
agent: need("agent name")?,
words: rest[1..].to_vec(),
},
"untag" => Command::Untag {
agent: need("agent name")?,
tag: need("tag")?,
},
"tags" => Command::Tags {
agent: rest.first().cloned(),
},
"profile" => match rest.first().map(|s| s.as_str()) {
Some("show") if rest.len() >= 2 => {
Command::Profile(crate::cli::ProfileCmd::Show {
name: rest[1].clone(),
})
}
Some("list") | None => Command::Profile(crate::cli::ProfileCmd::List),
other => {
app.log.error(&format!(
"usage: profile list | profile show <name>{}",
other.map(|o| format!(" (unknown 'profile {o}')")).unwrap_or_default()
));
return Ok(false);
}
},
"man" => Command::Man {
command: rest.first().cloned(),
output: opt_value("--output").map(PathBuf::from),
},
"status" => Command::Status {
agent: rest.first().cloned(),
},
@@ -1318,16 +1424,26 @@ fn handle_line(
rerun: opt_value("--rerun").and_then(|v| v.parse().ok()),
},
"init" => Command::Init { force: flag("--force") },
"search" => Command::Search {
keyword: rest.join(" "),
category: None,
},
"search" => {
let words: Vec<String> = rest
.iter()
.filter(|t| !t.starts_with("--"))
.cloned()
.collect();
let kw = words.join(" ");
Command::Search {
keyword: if kw.is_empty() { None } else { Some(kw) },
category: None,
tag: opt_value("--tag"),
}
}
"info" => Command::Info {
agent: need("agent name")?,
},
"start" => Command::Start(StartArgs {
agent: rest.first().cloned(),
background: flag("--background") || flag("-b"),
profile: opt_value("--profile"),
..Default::default()
}),
"stop" => Command::Stop {
@@ -1339,6 +1455,7 @@ fn handle_line(
start: StartArgs {
agent: rest.first().cloned(),
background: flag("--background") || flag("-b"),
profile: opt_value("--profile"),
..Default::default()
},
force: flag("--force"),
@@ -1380,7 +1497,10 @@ fn handle_line(
let shell: clap_complete::Shell = name
.parse()
.map_err(|e: String| anyhow!("unknown shell '{name}': {e}"))?;
Command::Completion { shell }
Command::Completion {
shell,
installed: flag("--installed"),
}
}
"self-update" => Command::SelfUpdate {
check: flag("--check"),
@@ -1453,6 +1573,15 @@ fn is_am_command(word: &str) -> bool {
| "self-uninstall"
| "export"
| "import"
| "dashboard"
| "favorite"
| "unfavorite"
| "note"
| "tag"
| "untag"
| "tags"
| "profile"
| "man"
)
}
@@ -1703,7 +1832,7 @@ mod tests {
cfg.aliases.clone(),
cfg.groups.clone(),
);
AmCompleter::from_catalog(&catalog, &cfg.aliases, &cfg.groups)
AmCompleter::from_catalog(&catalog, &cfg.aliases, &cfg.groups, None)
}
fn test_session() -> ShellSession {