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
This commit is contained in:
+139
-10
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user