init + contexte : am init et détection de projet (issue #8) #23
@@ -195,6 +195,12 @@ pub enum Command {
|
|||||||
/// Agent name or alias
|
/// Agent name or alias
|
||||||
agent: String,
|
agent: String,
|
||||||
},
|
},
|
||||||
|
/// Generate a local agent-manager.yaml for the current directory
|
||||||
|
Init {
|
||||||
|
/// Overwrite an existing agent-manager.yaml
|
||||||
|
#[arg(long)]
|
||||||
|
force: bool,
|
||||||
|
},
|
||||||
/// Display help: the overview, a command, the command list, or a search
|
/// Display help: the overview, a command, the command list, or a search
|
||||||
Help {
|
Help {
|
||||||
/// Command name (also accepts an agent name or alias); "commands" lists every command
|
/// Command name (also accepts an agent name or alias); "commands" lists every command
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
//! init: generate a local agent-manager.yaml for the current directory,
|
||||||
|
//! based on the detected technology stack.
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::context;
|
||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// Generate a local config file in dir; refuses to overwrite unless force.
|
||||||
|
/// Returns the written path.
|
||||||
|
pub fn generate(dir: &Path, force: bool) -> Result<PathBuf> {
|
||||||
|
let target = dir.join(crate::config::LOCAL_CONFIG_NAME);
|
||||||
|
if target.exists() && !force {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"{} already exists — use --force to overwrite it",
|
||||||
|
target.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let ctx = context::detect(dir);
|
||||||
|
let agents = context::recommended_agents(&ctx.stack);
|
||||||
|
let members = agents
|
||||||
|
.iter()
|
||||||
|
.map(|a| a.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
let stack = if ctx.stack.is_empty() {
|
||||||
|
"aucune détectée".to_string()
|
||||||
|
} else {
|
||||||
|
ctx.stack.join(", ")
|
||||||
|
};
|
||||||
|
let content = format!(
|
||||||
|
"# Généré par 'am init' le {date}
|
||||||
|
# Pile détectée : {stack}
|
||||||
|
version: \"1.0\"
|
||||||
|
groups:
|
||||||
|
dev: [{members}]
|
||||||
|
agents: []
|
||||||
|
",
|
||||||
|
date = crate::installers::now_rfc3339(),
|
||||||
|
);
|
||||||
|
std::fs::write(&target, content)
|
||||||
|
.with_context(|| format!("cannot write {}", target.display()))?;
|
||||||
|
Ok(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(app: &App, force: bool) -> Result<i32> {
|
||||||
|
if app.dry_run() {
|
||||||
|
app.log.dry(format!(
|
||||||
|
"would generate {} in the current directory",
|
||||||
|
crate::config::LOCAL_CONFIG_NAME
|
||||||
|
));
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
let cwd = std::env::current_dir().context("cannot determine the current directory")?;
|
||||||
|
let ctx = context::detect(&cwd);
|
||||||
|
let path = generate(&cwd, force)?;
|
||||||
|
app.log.success(&format!(
|
||||||
|
"wrote {} (stack: {})",
|
||||||
|
path.display(),
|
||||||
|
if ctx.stack.is_empty() {
|
||||||
|
"aucune détectée".to_string()
|
||||||
|
} else {
|
||||||
|
ctx.stack.join(", ")
|
||||||
|
}
|
||||||
|
));
|
||||||
|
app.log.info(&format!(
|
||||||
|
"group 'dev' prêt : am start group:dev — ajustez ensuite la config générée"
|
||||||
|
));
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ pub mod export_import;
|
|||||||
pub mod help_cmd;
|
pub mod help_cmd;
|
||||||
pub mod history_cmd;
|
pub mod history_cmd;
|
||||||
pub mod info_cmd;
|
pub mod info_cmd;
|
||||||
|
pub mod init_cmd;
|
||||||
pub mod install_cmd;
|
pub mod install_cmd;
|
||||||
pub mod list_cmd;
|
pub mod list_cmd;
|
||||||
pub mod log_cmd;
|
pub mod log_cmd;
|
||||||
@@ -96,6 +97,7 @@ pub fn execute_command(app: &App, cmd: &Command) -> Result<i32> {
|
|||||||
*limit,
|
*limit,
|
||||||
),
|
),
|
||||||
Command::Info { agent } => info_cmd::run(app, agent),
|
Command::Info { agent } => info_cmd::run(app, agent),
|
||||||
|
Command::Init { force } => init_cmd::run(app, *force),
|
||||||
Command::Help { command, find } => help_cmd::run(app, command.as_deref(), find.as_deref()),
|
Command::Help { command, find } => help_cmd::run(app, command.as_deref(), find.as_deref()),
|
||||||
Command::Version => version_cmd::run(app),
|
Command::Version => version_cmd::run(app),
|
||||||
Command::Config(sub) => config_cmd::run(app, sub),
|
Command::Config(sub) => config_cmd::run(app, sub),
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ fn start_one(
|
|||||||
let exec = resolve_exec(app, agent, extra_args, extra_env)?;
|
let exec = resolve_exec(app, agent, extra_args, extra_env)?;
|
||||||
let bin = PathBuf::from(&exec.program);
|
let bin = PathBuf::from(&exec.program);
|
||||||
let is_managed = app.state.get(&agent.name).ok().flatten().is_some();
|
let is_managed = app.state.get(&agent.name).ok().flatten().is_some();
|
||||||
|
let ctx = crate::context::detect(&std::env::current_dir().unwrap_or_default());
|
||||||
if background {
|
if background {
|
||||||
let pid = process::spawn_background(app, &agent.name, &bin, &exec.args, &exec.env)?;
|
let pid = process::spawn_background(app, &agent.name, &bin, &exec.args, &exec.env)?;
|
||||||
if app.dry_run() {
|
if app.dry_run() {
|
||||||
@@ -59,6 +60,7 @@ fn start_one(
|
|||||||
.with_pid(pid)
|
.with_pid(pid)
|
||||||
.with_session(crate::repl::session_id())
|
.with_session(crate::repl::session_id())
|
||||||
.with_cwd(cwd_string())
|
.with_cwd(cwd_string())
|
||||||
|
.with_project(ctx.root.clone())
|
||||||
.with_args(exec.args.clone())
|
.with_args(exec.args.clone())
|
||||||
.with_env_keys(events::env_keys(&exec.env)),
|
.with_env_keys(events::env_keys(&exec.env)),
|
||||||
);
|
);
|
||||||
@@ -71,6 +73,7 @@ fn start_one(
|
|||||||
&agent.name,
|
&agent.name,
|
||||||
pid,
|
pid,
|
||||||
cwd_string(),
|
cwd_string(),
|
||||||
|
ctx.root.clone(),
|
||||||
exec.args.clone(),
|
exec.args.clone(),
|
||||||
events::env_keys(&exec.env),
|
events::env_keys(&exec.env),
|
||||||
Some(log.display().to_string()),
|
Some(log.display().to_string()),
|
||||||
@@ -107,6 +110,7 @@ fn start_one(
|
|||||||
&Event::now(EventKind::Run)
|
&Event::now(EventKind::Run)
|
||||||
.with_agent(agent.name.clone())
|
.with_agent(agent.name.clone())
|
||||||
.with_cwd(cwd_string())
|
.with_cwd(cwd_string())
|
||||||
|
.with_project(ctx.root.clone())
|
||||||
.with_args(exec.args.clone())
|
.with_args(exec.args.clone())
|
||||||
.with_env_keys(events::env_keys(&exec.env))
|
.with_env_keys(events::env_keys(&exec.env))
|
||||||
.with_exit_code(code)
|
.with_exit_code(code)
|
||||||
|
|||||||
+153
@@ -0,0 +1,153 @@
|
|||||||
|
//! Project context detection: git root + branch and the technology
|
||||||
|
//! stack of a directory (marker files). Cached per process so repeated
|
||||||
|
//! calls (event emission, sessions) never re-run subprocesses.
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
|
||||||
|
pub struct ProjectContext {
|
||||||
|
/// Root of the enclosing git repository (or None).
|
||||||
|
pub root: Option<String>,
|
||||||
|
/// Current branch (or None).
|
||||||
|
pub branch: Option<String>,
|
||||||
|
/// Detected technology stack, e.g. ["rust", "node"].
|
||||||
|
pub stack: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marker file -> stack label. Checked walking up from the directory to the
|
||||||
|
/// filesystem root; the first directory containing a marker wins.
|
||||||
|
const MARKERS: &[(&str, &str)] = &[
|
||||||
|
("Cargo.toml", "rust"),
|
||||||
|
("package.json", "node"),
|
||||||
|
("pyproject.toml", "python"),
|
||||||
|
("requirements.txt", "python"),
|
||||||
|
("setup.py", "python"),
|
||||||
|
("go.mod", "go"),
|
||||||
|
("bun.lockb", "bun"),
|
||||||
|
("bun.lock", "bun"),
|
||||||
|
("Gemfile", "ruby"),
|
||||||
|
];
|
||||||
|
|
||||||
|
static CACHE: OnceLock<Mutex<HashMap<PathBuf, ProjectContext>>> = OnceLock::new();
|
||||||
|
|
||||||
|
fn cache() -> &'static Mutex<HashMap<PathBuf, ProjectContext>> {
|
||||||
|
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect the project context of a directory (cached per process).
|
||||||
|
pub fn detect(cwd: &Path) -> ProjectContext {
|
||||||
|
if let Some(hit) = cache().lock().unwrap().get(cwd).cloned() {
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
let ctx = detect_uncached(cwd);
|
||||||
|
cache().lock().unwrap().insert(cwd.to_path_buf(), ctx.clone());
|
||||||
|
ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_uncached(cwd: &Path) -> ProjectContext {
|
||||||
|
let (root, branch) = git_info(cwd);
|
||||||
|
let stack = detect_stack(cwd);
|
||||||
|
ProjectContext {
|
||||||
|
root,
|
||||||
|
branch,
|
||||||
|
stack,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Git root and branch via git subprocesses (best effort, silent).
|
||||||
|
fn git_info(cwd: &Path) -> (Option<String>, Option<String>) {
|
||||||
|
let root = std::process::Command::new("git")
|
||||||
|
.args(["rev-parse", "--show-toplevel"])
|
||||||
|
.current_dir(cwd)
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.filter(|o| o.status.success())
|
||||||
|
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string());
|
||||||
|
let branch = std::process::Command::new("git")
|
||||||
|
.args(["branch", "--show-current"])
|
||||||
|
.current_dir(cwd)
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.filter(|o| o.status.success())
|
||||||
|
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||||
|
.filter(|b| !b.is_empty());
|
||||||
|
(root, branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk up from cwd collecting stack labels from marker files.
|
||||||
|
pub fn detect_stack(cwd: &Path) -> Vec<String> {
|
||||||
|
let mut dir = Some(cwd);
|
||||||
|
let mut out: Vec<String> = Vec::new();
|
||||||
|
while let Some(d) = dir {
|
||||||
|
for (marker, label) in MARKERS {
|
||||||
|
if d.join(marker).is_file() && !out.iter().any(|s| s == label) {
|
||||||
|
out.push(label.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dir = d.parent();
|
||||||
|
}
|
||||||
|
out.sort();
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Agents recommended for a detected stack (catalog slugs).
|
||||||
|
pub fn recommended_agents(stack: &[String]) -> Vec<&'static str> {
|
||||||
|
let mut out: Vec<&'static str> = Vec::new();
|
||||||
|
if stack.contains(&"rust".to_string()) {
|
||||||
|
out.push("smelt");
|
||||||
|
}
|
||||||
|
if stack.contains(&"python".to_string()) {
|
||||||
|
out.push("aider");
|
||||||
|
}
|
||||||
|
if stack.contains(&"node".to_string()) {
|
||||||
|
out.push("claude-code");
|
||||||
|
}
|
||||||
|
if out.is_empty() {
|
||||||
|
out.push("claude-code");
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detects_stack_from_markers() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
std::fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
|
||||||
|
std::fs::write(dir.path().join("package.json"), "{}").unwrap();
|
||||||
|
let stack = detect_stack(dir.path());
|
||||||
|
assert_eq!(stack, vec!["node".to_string(), "rust".to_string()]);
|
||||||
|
assert_eq!(recommended_agents(&stack), vec!["smelt", "claude-code"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn walks_up_to_parent_directories() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
std::fs::write(dir.path().join("go.mod"), "").unwrap();
|
||||||
|
let sub = dir.path().join("src/deep");
|
||||||
|
std::fs::create_dir_all(&sub).unwrap();
|
||||||
|
let stack = detect_stack(&sub);
|
||||||
|
assert_eq!(stack, vec!["go".to_string()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_dir_recommends_default() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let stack = detect_stack(dir.path());
|
||||||
|
assert!(stack.is_empty());
|
||||||
|
assert_eq!(recommended_agents(&stack), vec!["claude-code"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cache_returns_same_context() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let a = detect(dir.path());
|
||||||
|
let b = detect(dir.path());
|
||||||
|
assert_eq!(a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -113,8 +113,8 @@ impl Event {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_project(mut self, project: impl Into<String>) -> Self {
|
pub fn with_project(mut self, project: Option<String>) -> Self {
|
||||||
self.project = Some(project.into());
|
self.project = project;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
@@ -296,6 +296,23 @@ pub static HELP_SPECS: &[HelpSpec] = &[
|
|||||||
HelpExample { desc: "Use an alias.", code: "info cc" },
|
HelpExample { desc: "Use an alias.", code: "info cc" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
HelpSpec {
|
||||||
|
name: "init",
|
||||||
|
category: "Commands",
|
||||||
|
usage: "init {flags}",
|
||||||
|
about: "Generate a local agent-manager.yaml for the current directory, based on the detected stack.",
|
||||||
|
search_terms: &["project", "setup", "scaffold"],
|
||||||
|
flags: &[
|
||||||
|
HelpFlag { short: "", long: "--force", value: "", desc: "Overwrite an existing agent-manager.yaml" },
|
||||||
|
],
|
||||||
|
subcommands: &[],
|
||||||
|
parameters: &[],
|
||||||
|
io: None,
|
||||||
|
examples: &[
|
||||||
|
HelpExample { desc: "Generate a project config.", code: "init" },
|
||||||
|
HelpExample { desc: "Regenerate over an existing one.", code: "init --force" },
|
||||||
|
],
|
||||||
|
},
|
||||||
HelpSpec {
|
HelpSpec {
|
||||||
name: "install",
|
name: "install",
|
||||||
category: "Commands",
|
category: "Commands",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ pub mod catalog;
|
|||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod context;
|
||||||
pub mod deps;
|
pub mod deps;
|
||||||
pub mod doctor;
|
pub mod doctor;
|
||||||
pub mod download;
|
pub mod download;
|
||||||
|
|||||||
+28
-1
@@ -64,6 +64,7 @@ const COMMAND_DESCRIPTIONS: &[(&str, &str)] = &[
|
|||||||
("stats", "usage statistics"),
|
("stats", "usage statistics"),
|
||||||
("log", "read the event journal"),
|
("log", "read the event journal"),
|
||||||
("history", "search command history"),
|
("history", "search command history"),
|
||||||
|
("init", "generate a project config"),
|
||||||
("search", "search the catalog"),
|
("search", "search the catalog"),
|
||||||
("info", "agent details"),
|
("info", "agent details"),
|
||||||
("install", "install an agent"),
|
("install", "install an agent"),
|
||||||
@@ -140,7 +141,7 @@ impl AmCompleter {
|
|||||||
Self {
|
Self {
|
||||||
theme: std::cell::Cell::new(crate::theme::default_theme()),
|
theme: std::cell::Cell::new(crate::theme::default_theme()),
|
||||||
commands: vec![
|
commands: vec![
|
||||||
"list", "status", "sessions", "stats", "log", "history", "search", "info", "install", "uninstall", "update",
|
"list", "status", "sessions", "stats", "log", "history", "init", "search", "info", "install", "uninstall", "update",
|
||||||
"start", "stop", "restart", "run", "doctor", "config", "completion",
|
"start", "stop", "restart", "run", "doctor", "config", "completion",
|
||||||
"self-update", "self-uninstall", "export", "import", "shell", "theme",
|
"self-update", "self-uninstall", "export", "import", "shell", "theme",
|
||||||
"ls", "dir", "cd", "ps", "where", "get", "help", "version", "exit",
|
"ls", "dir", "cd", "ps", "where", "get", "help", "version", "exit",
|
||||||
@@ -1199,6 +1200,31 @@ fn handle_line(
|
|||||||
"status" => Command::Status {
|
"status" => Command::Status {
|
||||||
agent: rest.first().cloned(),
|
agent: rest.first().cloned(),
|
||||||
},
|
},
|
||||||
|
"sessions" => Command::Sessions {
|
||||||
|
agent: rest.first().cloned(),
|
||||||
|
project: opt_value("--project"),
|
||||||
|
status: opt_value("--status"),
|
||||||
|
show: opt_value("--show"),
|
||||||
|
},
|
||||||
|
"stats" => Command::Stats {
|
||||||
|
agent: rest.first().cloned(),
|
||||||
|
period: opt_value("--period"),
|
||||||
|
},
|
||||||
|
"log" => Command::Log {
|
||||||
|
agent: rest.first().cloned(),
|
||||||
|
kind: opt_value("--kind"),
|
||||||
|
since: opt_value("--since"),
|
||||||
|
limit: opt_value("--limit").and_then(|v| v.parse().ok()).unwrap_or(50),
|
||||||
|
},
|
||||||
|
"history" => Command::History {
|
||||||
|
kind: opt_value("--kind"),
|
||||||
|
cwd: opt_value("--cwd"),
|
||||||
|
search: opt_value("--search"),
|
||||||
|
failed: flag("--failed"),
|
||||||
|
session: opt_value("--session"),
|
||||||
|
limit: opt_value("--limit").and_then(|v| v.parse().ok()).unwrap_or(100),
|
||||||
|
},
|
||||||
|
"init" => Command::Init { force: flag("--force") },
|
||||||
"search" => Command::Search {
|
"search" => Command::Search {
|
||||||
keyword: rest.join(" "),
|
keyword: rest.join(" "),
|
||||||
category: None,
|
category: None,
|
||||||
@@ -1308,6 +1334,7 @@ fn is_am_command(word: &str) -> bool {
|
|||||||
| "stats"
|
| "stats"
|
||||||
| "log"
|
| "log"
|
||||||
| "history"
|
| "history"
|
||||||
|
| "init"
|
||||||
| "search"
|
| "search"
|
||||||
| "info"
|
| "info"
|
||||||
| "install"
|
| "install"
|
||||||
|
|||||||
+6
-5
@@ -107,6 +107,7 @@ pub fn start_agent(
|
|||||||
agent: &str,
|
agent: &str,
|
||||||
pid: u32,
|
pid: u32,
|
||||||
cwd: Option<String>,
|
cwd: Option<String>,
|
||||||
|
project: Option<String>,
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
env_keys: Vec<String>,
|
env_keys: Vec<String>,
|
||||||
log: Option<String>,
|
log: Option<String>,
|
||||||
@@ -121,7 +122,7 @@ pub fn start_agent(
|
|||||||
agent: Some(agent.to_string()),
|
agent: Some(agent.to_string()),
|
||||||
pid: Some(pid),
|
pid: Some(pid),
|
||||||
cwd,
|
cwd,
|
||||||
project: None,
|
project,
|
||||||
started_at: crate::installers::now_rfc3339(),
|
started_at: crate::installers::now_rfc3339(),
|
||||||
ended_at: None,
|
ended_at: None,
|
||||||
exit_code: None,
|
exit_code: None,
|
||||||
@@ -283,7 +284,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn agent_session_lifecycle() {
|
fn agent_session_lifecycle() {
|
||||||
let app = test_app("sess");
|
let app = test_app("sess");
|
||||||
let id = start_agent(&app, "claude-code", 4242, Some("/tmp".into()), vec![], vec![], None).unwrap();
|
let id = start_agent(&app, "claude-code", 4242, Some("/tmp".into()), None, vec![], vec![], None).unwrap();
|
||||||
let sf = load(&app).unwrap();
|
let sf = load(&app).unwrap();
|
||||||
let r = sf.sessions.get(&id).unwrap();
|
let r = sf.sessions.get(&id).unwrap();
|
||||||
assert_eq!(r.status, "running");
|
assert_eq!(r.status, "running");
|
||||||
@@ -302,7 +303,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn reconcile_marks_dead_pid_interrupted() {
|
fn reconcile_marks_dead_pid_interrupted() {
|
||||||
let app = test_app("recon");
|
let app = test_app("recon");
|
||||||
let id = start_agent(&app, "aider", u32::MAX - 1, None, vec![], vec![], None).unwrap();
|
let id = start_agent(&app, "aider", u32::MAX - 1, None, None, vec![], vec![], None).unwrap();
|
||||||
let changed = reconcile(&app).unwrap();
|
let changed = reconcile(&app).unwrap();
|
||||||
assert_eq!(changed, 1);
|
assert_eq!(changed, 1);
|
||||||
let sf = load(&app).unwrap();
|
let sf = load(&app).unwrap();
|
||||||
@@ -321,8 +322,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn finish_matches_by_pid_first() {
|
fn finish_matches_by_pid_first() {
|
||||||
let app = test_app("multi");
|
let app = test_app("multi");
|
||||||
start_agent(&app, "jcode", 100, None, vec![], vec![], None).unwrap();
|
start_agent(&app, "jcode", 100, None, None, vec![], vec![], None).unwrap();
|
||||||
start_agent(&app, "jcode", 200, None, vec![], vec![], None).unwrap();
|
start_agent(&app, "jcode", 200, None, None, vec![], vec![], None).unwrap();
|
||||||
finish_agent(&app, "jcode", Some(200), 1, false).unwrap();
|
finish_agent(&app, "jcode", Some(200), 1, false).unwrap();
|
||||||
let sf = load(&app).unwrap();
|
let sf = load(&app).unwrap();
|
||||||
let stopped: Vec<_> = sf
|
let stopped: Vec<_> = sf
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
//! init + context detection.
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
|
||||||
|
use agent_manager::commands::init_cmd;
|
||||||
|
use agent_manager::context;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn context_detects_stack_from_markers() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
std::fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
|
||||||
|
std::fs::write(dir.path().join("package.json"), "{}").unwrap();
|
||||||
|
let stack = context::detect_stack(dir.path());
|
||||||
|
assert_eq!(stack, vec!["node".to_string(), "rust".to_string()]);
|
||||||
|
assert_eq!(context::recommended_agents(&stack), vec!["smelt", "claude-code"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_writes_valid_config_and_refuses_overwrite() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
std::fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
|
||||||
|
let path = init_cmd::generate(dir.path(), false).unwrap();
|
||||||
|
assert!(path.ends_with("agent-manager.yaml"));
|
||||||
|
let text = std::fs::read_to_string(&path).unwrap();
|
||||||
|
assert!(text.contains(r#"version: "1.0""#), "config valide: {text}");
|
||||||
|
assert!(text.contains("smelt"), "agent recommande pour rust: {text}");
|
||||||
|
// Un second appel sans --force échoue.
|
||||||
|
assert!(init_cmd::generate(dir.path(), false).is_err());
|
||||||
|
// Avec --force, il écrase.
|
||||||
|
assert!(init_cmd::generate(dir.path(), true).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_uses_default_for_empty_dir() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = init_cmd::generate(dir.path(), false).unwrap();
|
||||||
|
let text = std::fs::read_to_string(&path).unwrap();
|
||||||
|
assert!(text.contains("claude-code"), "default agent: {text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_runs_dry() {
|
||||||
|
let app = common::test_app("initdry", &["init", "--dry-run"]);
|
||||||
|
let code = init_cmd::run(&app, false).unwrap();
|
||||||
|
assert_eq!(code, 0);
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ fn agent_session_roundtrip_and_counter() {
|
|||||||
"claude-code",
|
"claude-code",
|
||||||
4242,
|
4242,
|
||||||
Some("/tmp".to_string()),
|
Some("/tmp".to_string()),
|
||||||
|
None,
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
None,
|
None,
|
||||||
@@ -56,7 +57,7 @@ fn agent_session_roundtrip_and_counter() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn command_lists_sessions() {
|
fn command_lists_sessions() {
|
||||||
let app = common::test_app("sesscmd", &["sessions"]);
|
let app = common::test_app("sesscmd", &["sessions"]);
|
||||||
sessions::start_agent(&app, "aider", 4242, None, vec![], vec![], None).unwrap();
|
sessions::start_agent(&app, "aider", 4242, None, None, vec![], vec![], None).unwrap();
|
||||||
let code = sessions_cmd::run(&app, None, None, None, None).unwrap();
|
let code = sessions_cmd::run(&app, None, None, None, None).unwrap();
|
||||||
assert_eq!(code, 0);
|
assert_eq!(code, 0);
|
||||||
// --json path returns the row set without error.
|
// --json path returns the row set without error.
|
||||||
@@ -66,7 +67,7 @@ fn command_lists_sessions() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn show_one_accepts_known_id() {
|
fn show_one_accepts_known_id() {
|
||||||
let app = common::test_app("sessshow", &["sessions"]);
|
let app = common::test_app("sessshow", &["sessions"]);
|
||||||
let id = sessions::start_agent(&app, "jcode", 99, None, vec![], vec![], None).unwrap();
|
let id = sessions::start_agent(&app, "jcode", 99, None, None, vec![], vec![], None).unwrap();
|
||||||
let code = sessions_cmd::run(&app, None, None, None, Some(&id)).unwrap();
|
let code = sessions_cmd::run(&app, None, None, None, Some(&id)).unwrap();
|
||||||
assert_eq!(code, 0);
|
assert_eq!(code, 0);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user