Summarize node readiness in doctor

This commit is contained in:
Michel Paulissen 2026-07-04 11:52:44 +02:00
parent 1c4b045a6a
commit ab84fd382d
4 changed files with 148 additions and 11 deletions

View file

@ -749,6 +749,16 @@ fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result<Value> {
.and_then(|config| config.coordinator.clone())
});
let coordinator_reachability = coordinator_reachability(coordinator.as_deref());
let dependencies = json!({
"cargo": command_available("cargo"),
"git": command_available("git"),
"podman": command_available("podman"),
"disasmer-node": command_available("disasmer-node") || sibling_binary("disasmer-node").is_some(),
"disasmer-coordinator": command_available("disasmer-coordinator") || sibling_binary("disasmer-coordinator").is_some(),
"disasmer-debug-dap": command_available("disasmer-debug-dap") || sibling_binary("disasmer-debug-dap").is_some(),
});
let node_readiness = NodeCapabilities::detect_current();
let node_readiness_summary = node_readiness_summary(&node_readiness, &dependencies);
Ok(json!({
"command": "doctor",
"cwd": cwd,
@ -756,15 +766,9 @@ fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result<Value> {
"coordinator_reachability": coordinator_reachability,
"auth": auth_state_value(&cwd)?,
"project": config,
"dependencies": {
"cargo": command_available("cargo"),
"git": command_available("git"),
"podman": command_available("podman"),
"disasmer-node": command_available("disasmer-node") || sibling_binary("disasmer-node").is_some(),
"disasmer-coordinator": command_available("disasmer-coordinator") || sibling_binary("disasmer-coordinator").is_some(),
"disasmer-debug-dap": command_available("disasmer-debug-dap") || sibling_binary("disasmer-debug-dap").is_some(),
},
"node_readiness": NodeCapabilities::detect_current(),
"dependencies": dependencies,
"node_readiness": node_readiness,
"node_readiness_summary": node_readiness_summary,
"next_actions": [
"disasmer login --browser",
"disasmer project init",
@ -774,6 +778,78 @@ fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result<Value> {
}))
}
fn node_readiness_summary(capabilities: &NodeCapabilities, dependencies: &Value) -> Value {
let has_command = capabilities.capabilities.contains(&Capability::Command);
let has_wasmtime = capabilities.capabilities.contains(&Capability::Wasmtime);
let has_vfs_artifacts = capabilities
.capabilities
.contains(&Capability::VfsArtifacts);
let has_source_filesystem = capabilities
.capabilities
.contains(&Capability::SourceFilesystem);
let has_container_backend = capabilities
.environment_backends
.contains(&disasmer_core::EnvironmentBackend::Container);
let podman_available = dependencies
.get("podman")
.and_then(Value::as_bool)
.unwrap_or(false);
let git_available = dependencies
.get("git")
.and_then(Value::as_bool)
.unwrap_or(false);
let disasmer_node_available = dependencies
.get("disasmer-node")
.and_then(Value::as_bool)
.unwrap_or(false);
let mut missing = Vec::new();
if has_container_backend && !podman_available {
missing.push("podman");
}
if capabilities.source_providers.contains("git") && !git_available {
missing.push("git");
}
if !disasmer_node_available {
missing.push("disasmer-node");
}
let basic_runtime_ready =
has_command && has_wasmtime && has_vfs_artifacts && has_source_filesystem;
let local_dependencies_ready = missing.is_empty();
let status = if basic_runtime_ready && local_dependencies_ready {
"ready_to_attach"
} else if basic_runtime_ready {
"local_dependencies_missing"
} else {
"limited_capabilities"
};
let next_actions = if status == "ready_to_attach" {
vec!["disasmer node enroll", "disasmer node attach --worker"]
} else {
vec![
"install missing local dependencies",
"rerun disasmer doctor",
]
};
json!({
"status": status,
"explicit_attach_required": true,
"command_execution_capability": has_command,
"wasmtime_capability": has_wasmtime,
"artifact_capability": has_vfs_artifacts,
"source_filesystem_capability": has_source_filesystem,
"container_backend_reported": has_container_backend,
"podman_binary_available": podman_available,
"git_binary_available": git_available,
"node_binary_available": disasmer_node_available,
"missing_local_dependencies": missing,
"source_providers": capabilities.source_providers.iter().collect::<Vec<_>>(),
"next_actions": next_actions,
})
}
fn coordinator_reachability(coordinator: Option<&str>) -> Value {
let Some(coordinator) = coordinator else {
return json!({
@ -2537,6 +2613,24 @@ fn human_report(value: &Value) -> String {
}
}
}
if let Some(summary) = value.get("node_readiness_summary") {
push_nested_string_field(&mut lines, summary, "status", "node readiness");
if let Some(missing) = summary
.get("missing_local_dependencies")
.and_then(Value::as_array)
{
let missing = missing.iter().filter_map(Value::as_str).collect::<Vec<_>>();
if !missing.is_empty() {
lines.push(format!("node missing dependencies: {}", missing.join(", ")));
}
}
if let Some(actions) = summary.get("next_actions").and_then(Value::as_array) {
let actions = actions.iter().filter_map(Value::as_str).collect::<Vec<_>>();
if !actions.is_empty() {
lines.push(format!("node next: {}", actions.join("; ")));
}
}
}
if let Some(disclosures) = value
.get("grant_disclosures")
.and_then(Value::as_array)
@ -6887,6 +6981,28 @@ mod tests {
report["coordinator_reachability"]["status"],
"not_configured"
);
assert!(matches!(
report["node_readiness_summary"]["status"].as_str(),
Some("ready_to_attach")
| Some("local_dependencies_missing")
| Some("limited_capabilities")
));
assert_eq!(
report["node_readiness_summary"]["explicit_attach_required"],
true
);
assert_eq!(
report["node_readiness_summary"]["command_execution_capability"],
true
);
assert!(report["node_readiness_summary"]["missing_local_dependencies"].is_array());
assert!(
report["node_readiness_summary"]["next_actions"]
.as_array()
.unwrap()
.len()
>= 2
);
}
#[test]