huskies: merge 1153 story huskies projects chat command — list every registered project with port and status

This commit is contained in:
dave
2026-05-20 00:23:44 +00:00
parent 0c207981e9
commit 846b3e1b4c
5 changed files with 337 additions and 0 deletions
+54
View File
@@ -19,6 +19,7 @@ const GATEWAY_TOOLS: &[&str] = &[
"switch_project",
"gateway_status",
"gateway_health",
"list_projects",
"init_project",
"adopt_project",
"aggregate_pipeline_status",
@@ -65,6 +66,14 @@ pub(crate) fn gateway_tool_definitions() -> Vec<Value> {
"properties": {}
}
}),
json!({
"name": "list_projects",
"description": "List every registered gateway project with its name, url, ssh_port (if set), host_path (if set), and an adopted/built-in marker. The active project is prefixed with *. No liveness checks are performed.",
"inputSchema": {
"type": "object",
"properties": {}
}
}),
json!({
"name": "init_project",
"description": "Initialize a new huskies project at the given path by scaffolding .huskies/ and related files — the same as running `huskies init <path>`. Prefer this tool over asking the user to run the CLI. If `name` and `url` are supplied the project is also registered in projects.toml so switch_project can reach it immediately.",
@@ -423,6 +432,7 @@ async fn handle_gateway_tool(
"switch_project" => handle_switch_project_tool(params, state, id).await,
"gateway_status" => handle_gateway_status_tool(state, id).await,
"gateway_health" => handle_gateway_health_tool(state, id).await,
"list_projects" => handle_list_projects_tool(state, id).await,
"init_project" => handle_init_project_tool(params, state, id).await,
"adopt_project" => handle_adopt_project_tool(params, state, id).await,
"aggregate_pipeline_status" => handle_aggregate_pipeline_status_tool(state, id).await,
@@ -539,6 +549,50 @@ async fn handle_gateway_health_tool(state: &GatewayState, id: Option<Value>) ->
)
}
/// Handle the `list_projects` gateway tool.
///
/// Returns one row per registered project: name, url, ssh_port (if set),
/// host_path (if set), and an `[adopted]`/`[built-in]` marker. Output is
/// alphabetised by project name (BTreeMap order). The active project is
/// prefixed with `*`. No liveness checks are performed.
async fn handle_list_projects_tool(state: &GatewayState, id: Option<Value>) -> JsonRpcResponse {
let active = state.active_project.read().await.clone();
let projects = state.projects.read().await;
if projects.is_empty() {
return JsonRpcResponse::success(
id,
json!({ "content": [{ "type": "text", "text": "No projects registered." }] }),
);
}
let mut lines = Vec::with_capacity(projects.len() + 1);
lines.push(format!("Projects ({} registered):", projects.len()));
for (name, entry) in projects.iter() {
let marker = if *name == active { "*" } else { " " };
let mut parts = vec![name.clone()];
if let Some(ref url) = entry.url {
parts.push(url.clone());
}
if let Some(port) = entry.ssh_port {
parts.push(format!("ssh:{port}"));
}
if let Some(ref path) = entry.host_path {
parts.push(path.clone());
}
let adopted = if entry.host_path.is_some() {
"[adopted]"
} else {
"[built-in]"
};
parts.push(adopted.to_string());
lines.push(format!("{marker} {}", parts.join(" ")));
}
let text = lines.join("\n");
JsonRpcResponse::success(id, json!({ "content": [{ "type": "text", "text": text }] }))
}
async fn handle_init_project_tool(
params: &Value,
state: &GatewayState,