diff --git a/Cargo.lock b/Cargo.lock
index c0ac1d1df..6ce320900 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2882,6 +2882,7 @@ dependencies = [
"flate2",
"futures",
"git2",
+ "glob",
"globset",
"handlebars",
"hex",
diff --git a/apps/framework-cli-e2e/test/cli-doctor.test.ts b/apps/framework-cli-e2e/test/cli-doctor.test.ts
new file mode 100644
index 000000000..7ce641d66
--- /dev/null
+++ b/apps/framework-cli-e2e/test/cli-doctor.test.ts
@@ -0,0 +1,299 @@
+///
+///
+///
+/**
+ * E2E tests for moose doctor command (ENG-1252)
+ *
+ * Tests the doctor command functionality:
+ * 1. Execute diagnostics with default options
+ * 2. Filter by severity level
+ * 3. Output as JSON
+ * 4. Use verbosity flags
+ * 5. Filter by component pattern
+ */
+
+import { spawn, ChildProcess } from "child_process";
+import { expect } from "chai";
+import * as path from "path";
+import { promisify } from "util";
+
+import { TIMEOUTS } from "./constants";
+import {
+ waitForServerStart,
+ createTempTestDirectory,
+ cleanupTestSuite,
+ setupTypeScriptProject,
+} from "./utils";
+
+const execAsync = promisify(require("child_process").exec);
+
+const CLI_PATH = path.resolve(__dirname, "../../../target/debug/moose-cli");
+const MOOSE_TS_LIB_PATH = path.resolve(
+ __dirname,
+ "../../../packages/ts-moose-lib",
+);
+
+describe("moose doctor command", () => {
+ let devProcess: ChildProcess;
+ let testProjectDir: string;
+
+ before(async function () {
+ this.timeout(TIMEOUTS.TEST_SETUP_MS);
+
+ console.log("\n=== Starting Doctor Command Test ===");
+
+ // Create temp test directory
+ testProjectDir = createTempTestDirectory("doctor-cmd-test");
+ console.log("Test project dir:", testProjectDir);
+
+ // Setup TypeScript project
+ await setupTypeScriptProject(
+ testProjectDir,
+ "typescript-empty",
+ CLI_PATH,
+ MOOSE_TS_LIB_PATH,
+ "test-doctor-cmd",
+ "npm",
+ );
+
+ // Start moose dev
+ console.log("\nStarting moose dev...");
+ devProcess = spawn(CLI_PATH, ["dev"], {
+ stdio: "pipe",
+ cwd: testProjectDir,
+ });
+
+ await waitForServerStart(
+ devProcess,
+ TIMEOUTS.SERVER_STARTUP_MS,
+ "development server started",
+ "http://localhost:4000",
+ );
+
+ console.log("✓ Infrastructure ready");
+ });
+
+ after(async function () {
+ this.timeout(TIMEOUTS.CLEANUP_MS);
+ console.log("\n=== Cleaning up Doctor Command Test ===");
+
+ await cleanupTestSuite(devProcess, testProjectDir, "doctor-cmd-test", {
+ logPrefix: "Doctor Command Test",
+ });
+ });
+
+ it("should execute doctor with default options", async function () {
+ this.timeout(TIMEOUTS.MIGRATION_MS);
+
+ console.log("\n--- Testing doctor with default options ---");
+
+ const { stdout } = await execAsync(`"${CLI_PATH}" doctor`, {
+ cwd: testProjectDir,
+ });
+
+ console.log("Doctor output:", stdout);
+
+ // Should show summary even with no issues
+ expect(stdout).to.include("Summary:");
+ expect(stdout).to.match(/\d+ errors?, \d+ warnings?, \d+ info messages?/);
+
+ console.log("✓ Doctor command runs with defaults");
+ });
+
+ it("should execute doctor with JSON output", async function () {
+ this.timeout(TIMEOUTS.MIGRATION_MS);
+
+ console.log("\n--- Testing doctor with JSON output ---");
+
+ const { stdout } = await execAsync(`"${CLI_PATH}" doctor --json`, {
+ cwd: testProjectDir,
+ });
+
+ console.log("Doctor JSON output:", stdout.substring(0, 200));
+
+ // Parse JSON to ensure it's valid
+ const output = JSON.parse(stdout);
+
+ expect(output).to.have.property("issues");
+ expect(output).to.have.property("summary");
+ expect(output.summary).to.have.property("total_issues");
+ expect(output.summary).to.have.property("by_severity");
+ expect(output.summary).to.have.property("by_component");
+
+ console.log("✓ JSON output is valid");
+ });
+
+ it("should filter by severity level", async function () {
+ this.timeout(TIMEOUTS.MIGRATION_MS);
+
+ console.log("\n--- Testing severity filtering ---");
+
+ // Test with info severity (should include everything)
+ const { stdout: infoOutput } = await execAsync(
+ `"${CLI_PATH}" doctor --severity info`,
+ {
+ cwd: testProjectDir,
+ },
+ );
+
+ expect(infoOutput).to.include("Summary:");
+
+ // Test with error severity (default)
+ const { stdout: errorOutput } = await execAsync(
+ `"${CLI_PATH}" doctor --severity error`,
+ {
+ cwd: testProjectDir,
+ },
+ );
+
+ expect(errorOutput).to.include("Summary:");
+
+ console.log("✓ Severity filtering works");
+ });
+
+ it("should respect verbosity flags", async function () {
+ this.timeout(TIMEOUTS.MIGRATION_MS);
+
+ console.log("\n--- Testing verbosity flags ---");
+
+ // Test with -v
+ const { stdout: verboseOutput } = await execAsync(
+ `"${CLI_PATH}" doctor -v`,
+ {
+ cwd: testProjectDir,
+ },
+ );
+
+ expect(verboseOutput).to.include("Summary:");
+
+ // Test with -vv
+ const { stdout: veryVerboseOutput } = await execAsync(
+ `"${CLI_PATH}" doctor -vv`,
+ {
+ cwd: testProjectDir,
+ },
+ );
+
+ expect(veryVerboseOutput).to.include("Summary:");
+
+ // Test with -vvv
+ const { stdout: maxVerboseOutput } = await execAsync(
+ `"${CLI_PATH}" doctor -vvv`,
+ {
+ cwd: testProjectDir,
+ },
+ );
+
+ expect(maxVerboseOutput).to.include("Summary:");
+
+ console.log("✓ Verbosity flags work");
+ });
+
+ it("should filter by component pattern", async function () {
+ this.timeout(TIMEOUTS.MIGRATION_MS);
+
+ console.log("\n--- Testing component filtering ---");
+
+ // Use a glob pattern that won't match anything
+ const { stdout } = await execAsync(
+ `"${CLI_PATH}" doctor --component "nonexistent_*"`,
+ {
+ cwd: testProjectDir,
+ },
+ );
+
+ expect(stdout).to.include("Summary:");
+ // With no matching components, should show 0 issues
+ expect(stdout).to.match(/0 errors?, 0 warnings?, 0 info messages?/);
+
+ console.log("✓ Component filtering works");
+ });
+
+ it("should respect since parameter", async function () {
+ this.timeout(TIMEOUTS.MIGRATION_MS);
+
+ console.log("\n--- Testing since parameter ---");
+
+ // Test with different time windows
+ const { stdout: hours1 } = await execAsync(
+ `"${CLI_PATH}" doctor --since "1 hour"`,
+ {
+ cwd: testProjectDir,
+ },
+ );
+
+ expect(hours1).to.include("Summary:");
+
+ const { stdout: days1 } = await execAsync(
+ `"${CLI_PATH}" doctor --since "1 day"`,
+ {
+ cwd: testProjectDir,
+ },
+ );
+
+ expect(days1).to.include("Summary:");
+
+ const { stdout: minutes30 } = await execAsync(
+ `"${CLI_PATH}" doctor --since "30m"`,
+ {
+ cwd: testProjectDir,
+ },
+ );
+
+ expect(minutes30).to.include("Summary:");
+
+ console.log("✓ Since parameter works");
+ });
+
+ it("should combine multiple options", async function () {
+ this.timeout(TIMEOUTS.MIGRATION_MS);
+
+ console.log("\n--- Testing combined options ---");
+
+ const { stdout } = await execAsync(
+ `"${CLI_PATH}" doctor --severity warning --json -v`,
+ {
+ cwd: testProjectDir,
+ },
+ );
+
+ // Should be valid JSON
+ const output = JSON.parse(stdout);
+ expect(output).to.have.property("issues");
+ expect(output).to.have.property("summary");
+
+ console.log("✓ Combined options work");
+ });
+
+ it("should handle invalid severity gracefully", async function () {
+ this.timeout(TIMEOUTS.MIGRATION_MS);
+
+ console.log("\n--- Testing invalid severity handling ---");
+
+ try {
+ await execAsync(`"${CLI_PATH}" doctor --severity invalid`, {
+ cwd: testProjectDir,
+ });
+ expect.fail("Should have thrown an error");
+ } catch (error: any) {
+ expect(error.message).to.match(/Failed to parse severity|must be one of/);
+ console.log("✓ Invalid severity handled gracefully");
+ }
+ });
+
+ it("should handle invalid duration gracefully", async function () {
+ this.timeout(TIMEOUTS.MIGRATION_MS);
+
+ console.log("\n--- Testing invalid duration handling ---");
+
+ try {
+ await execAsync(`"${CLI_PATH}" doctor --since "invalid"`, {
+ cwd: testProjectDir,
+ });
+ expect.fail("Should have thrown an error");
+ } catch (error: any) {
+ expect(error.message).to.match(/Failed to parse time duration/);
+ console.log("✓ Invalid duration handled gracefully");
+ }
+ });
+});
diff --git a/apps/framework-cli/Cargo.toml b/apps/framework-cli/Cargo.toml
index 99c977e26..8e323ca6e 100644
--- a/apps/framework-cli/Cargo.toml
+++ b/apps/framework-cli/Cargo.toml
@@ -67,6 +67,7 @@ pbkdf2 = { version = "0.12", features = ["simple"] }
sha2 = "0.10.8"
hex = "0.4.2"
constant_time_eq = "0.3.0"
+glob = "0.3"
tokio-stream = "0.1.16"
redis = { version = "0.29.1", features = [
diff --git a/apps/framework-cli/src/cli.rs b/apps/framework-cli/src/cli.rs
index 4d3e306ca..1d27ad303 100644
--- a/apps/framework-cli/src/cli.rs
+++ b/apps/framework-cli/src/cli.rs
@@ -1386,6 +1386,44 @@ pub async fn top_command_handler(
wait_for_usage_capture(capture_handle).await;
+ result
+ }
+ Commands::Doctor {
+ severity,
+ component,
+ since,
+ json,
+ verbose,
+ clickhouse_url,
+ redis_url,
+ } => {
+ info!("Running doctor command");
+
+ let project = load_project(commands)?;
+ let project_arc = Arc::new(project);
+
+ let capture_handle = crate::utilities::capture::capture_usage(
+ ActivityType::DoctorCommand,
+ Some(project_arc.name()),
+ &settings,
+ machine_id.clone(),
+ HashMap::new(),
+ );
+
+ let result = routines::doctor::diagnose_infrastructure(
+ project_arc,
+ severity.clone(),
+ component.clone(),
+ since.clone(),
+ *json,
+ *verbose,
+ clickhouse_url.clone(),
+ redis_url.clone(),
+ )
+ .await;
+
+ wait_for_usage_capture(capture_handle).await;
+
result
}
}
diff --git a/apps/framework-cli/src/cli/commands.rs b/apps/framework-cli/src/cli/commands.rs
index 5a76bce88..3e95b7cb3 100644
--- a/apps/framework-cli/src/cli/commands.rs
+++ b/apps/framework-cli/src/cli/commands.rs
@@ -206,6 +206,36 @@ pub enum Commands {
#[arg(short, long, default_value = "10000")]
limit: u64,
},
+ /// Diagnose infrastructure health and surface issues
+ Doctor {
+ /// Minimum severity level to report (error, warning, info)
+ #[arg(long, default_value = "error")]
+ severity: String,
+
+ /// Filter by component name pattern (glob pattern, e.g., "users_*")
+ #[arg(long)]
+ component: Option,
+
+ /// Time window to check (e.g., "6 hours", "1 day", "30m")
+ #[arg(long, default_value = "6 hours")]
+ since: String,
+
+ /// Output results in JSON format
+ #[arg(long, default_value = "false")]
+ json: bool,
+
+ /// Increase verbosity level (can be used multiple times: -v, -vv, -vvv)
+ #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
+ verbose: u8,
+
+ /// ClickHouse connection URL (optional, falls back to project config)
+ #[arg(long)]
+ clickhouse_url: Option,
+
+ /// Redis connection URL (optional, falls back to project config)
+ #[arg(long)]
+ redis_url: Option,
+ },
}
#[derive(Debug, Args)]
diff --git a/apps/framework-cli/src/cli/routines/doctor.rs b/apps/framework-cli/src/cli/routines/doctor.rs
new file mode 100644
index 000000000..05fd97eb9
--- /dev/null
+++ b/apps/framework-cli/src/cli/routines/doctor.rs
@@ -0,0 +1,328 @@
+//! # Doctor Routine
+//!
+//! Diagnostic routine for infrastructure health checking. This command surfaces
+//! errors and issues in ClickHouse, Redis, and other infrastructure components.
+//!
+//! ## Features
+//! - Proactive diagnostics using infrastructure map
+//! - Configurable severity filtering (error, warning, info)
+//! - Component filtering with glob patterns
+//! - Time-based filtering (e.g., "6 hours", "1 day")
+//! - Multiple output formats (human-readable, JSON)
+//! - Verbosity control for detailed metadata
+
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use log::{debug, info};
+
+use crate::cli::display::Message;
+use crate::cli::routines::{setup_redis_client, RoutineFailure, RoutineSuccess};
+use crate::framework::core::infrastructure_map::InfrastructureMap;
+use crate::infrastructure::olap::clickhouse::config::parse_clickhouse_connection_string;
+use crate::infrastructure::olap::clickhouse::diagnostics::{
+ Component, DiagnosticOptions, DiagnosticOutput, DiagnosticRequest, Severity,
+};
+use crate::project::Project;
+
+/// Error types for doctor routine operations
+#[derive(Debug, thiserror::Error)]
+#[allow(dead_code)]
+pub enum DoctorError {
+ #[error("Failed to parse severity '{0}': must be one of: error, warning, info")]
+ InvalidSeverity(String),
+
+ #[error("Failed to parse time duration '{0}': {1}")]
+ InvalidDuration(String, String),
+
+ #[error("Failed to load infrastructure map: {0}")]
+ InfraMapLoad(String),
+
+ #[error("Failed to parse ClickHouse connection string: {0}")]
+ ClickHouseConfig(String),
+
+ #[error("Failed to setup Redis client: {0}")]
+ RedisClient(String),
+
+ #[error("Failed to run diagnostics: {0}")]
+ DiagnosticFailed(String),
+
+ #[error("Failed to compile glob pattern '{pattern}': {error}")]
+ InvalidGlob { pattern: String, error: String },
+}
+
+/// Parse severity string into Severity enum
+fn parse_severity(severity_str: &str) -> Result {
+ match severity_str.to_lowercase().as_str() {
+ "error" => Ok(Severity::Error),
+ "warning" => Ok(Severity::Warning),
+ "info" => Ok(Severity::Info),
+ _ => Err(DoctorError::InvalidSeverity(severity_str.to_string())),
+ }
+}
+
+/// Parse humantime duration string into ClickHouse interval format
+/// Examples: "6 hours" -> "-6h", "1 day" -> "-1d", "30m" -> "-30m"
+fn parse_since(since_str: &str) -> Result {
+ // Try to parse with humantime crate
+ let duration = humantime::parse_duration(since_str)
+ .map_err(|e| DoctorError::InvalidDuration(since_str.to_string(), e.to_string()))?;
+
+ // Convert to ClickHouse interval format (negative for relative to now)
+ let total_seconds = duration.as_secs();
+
+ // Choose appropriate unit for readability
+ if total_seconds % 3600 == 0 {
+ let hours = total_seconds / 3600;
+ Ok(format!("-{}h", hours))
+ } else if total_seconds % 60 == 0 {
+ let minutes = total_seconds / 60;
+ Ok(format!("-{}m", minutes))
+ } else {
+ Ok(format!("-{}s", total_seconds))
+ }
+}
+
+/// Main doctor routine entry point
+#[allow(clippy::too_many_arguments)]
+pub async fn diagnose_infrastructure(
+ project: Arc,
+ severity_str: String,
+ component_pattern: Option,
+ since_str: String,
+ json_output: bool,
+ verbosity: u8,
+ clickhouse_url: Option,
+ redis_url: Option,
+) -> Result {
+ info!("Starting infrastructure diagnostics");
+
+ // Parse severity
+ let severity = parse_severity(&severity_str).map_err(|e| {
+ RoutineFailure::error(Message {
+ action: "Doctor".to_string(),
+ details: e.to_string(),
+ })
+ })?;
+
+ // Parse since duration
+ let since = parse_since(&since_str).map_err(|e| {
+ RoutineFailure::error(Message {
+ action: "Doctor".to_string(),
+ details: e.to_string(),
+ })
+ })?;
+
+ debug!("Parsed severity: {:?}, since: {}", severity, since);
+
+ // Setup Redis client
+ let redis_client = if let Some(ref url) = redis_url {
+ // TODO: Create redis client from custom URL
+ // For now, fall back to project config
+ debug!("Custom Redis URL provided: {}", url);
+ setup_redis_client(project.clone()).await.map_err(|e| {
+ RoutineFailure::error(Message {
+ action: "Doctor".to_string(),
+ details: format!("Failed to setup redis client: {:?}", e),
+ })
+ })?
+ } else {
+ setup_redis_client(project.clone()).await.map_err(|e| {
+ RoutineFailure::error(Message {
+ action: "Doctor".to_string(),
+ details: format!("Failed to setup redis client: {:?}", e),
+ })
+ })?
+ };
+
+ // Setup ClickHouse config
+ let clickhouse_config = if let Some(ref url) = clickhouse_url {
+ debug!("Using custom ClickHouse URL");
+ parse_clickhouse_connection_string(url).map_err(|e| {
+ RoutineFailure::error(Message {
+ action: "Doctor".to_string(),
+ details: format!("Failed to parse ClickHouse connection string: {:?}", e),
+ })
+ })?
+ } else {
+ debug!("Using project ClickHouse config");
+ project.clickhouse_config.clone()
+ };
+
+ // Load infrastructure map
+ debug!("Loading infrastructure map from Redis");
+ let infra_map = InfrastructureMap::load_from_redis(&redis_client)
+ .await
+ .map_err(|e| {
+ RoutineFailure::error(Message {
+ action: "Doctor".to_string(),
+ details: format!("Failed to load infrastructure map: {:?}", e),
+ })
+ })?
+ .ok_or_else(|| {
+ RoutineFailure::error(Message {
+ action: "Doctor".to_string(),
+ details: "No infrastructure map found. The dev server may not be running."
+ .to_string(),
+ })
+ })?;
+
+ // Filter tables based on component pattern
+ let tables_to_check: Vec<_> = if let Some(ref pattern) = component_pattern {
+ debug!("Filtering tables with pattern: {}", pattern);
+ infra_map
+ .tables
+ .iter()
+ .filter(|(_map_key, table)| {
+ // Use glob matching
+ let glob_pattern =
+ glob::Pattern::new(pattern).map_err(|e| DoctorError::InvalidGlob {
+ pattern: pattern.clone(),
+ error: e.to_string(),
+ });
+
+ match glob_pattern {
+ Ok(glob) => glob.matches(&table.name),
+ Err(_) => false,
+ }
+ })
+ .collect()
+ } else {
+ infra_map.tables.iter().collect()
+ };
+
+ info!("Checking {} tables for issues", tables_to_check.len());
+
+ // Build diagnostic request with components from infrastructure map
+ let components: Vec<_> = tables_to_check
+ .iter()
+ .map(|(_map_key, table)| {
+ let mut metadata = HashMap::new();
+ metadata.insert("database".to_string(), clickhouse_config.db_name.clone());
+
+ let component = Component {
+ component_type: "table".to_string(),
+ name: table.name.clone(),
+ metadata,
+ };
+
+ (component, table.engine.clone())
+ })
+ .collect();
+
+ let request = DiagnosticRequest {
+ components,
+ options: DiagnosticOptions {
+ diagnostic_names: Vec::new(), // Run all diagnostics
+ min_severity: severity,
+ since: Some(since),
+ },
+ };
+
+ // Run diagnostics
+ let output = crate::infrastructure::olap::clickhouse::diagnostics::run_diagnostics(
+ request,
+ &clickhouse_config,
+ )
+ .await
+ .map_err(|e| {
+ RoutineFailure::error(Message {
+ action: "Doctor".to_string(),
+ details: format!("Diagnostics failed: {}", e),
+ })
+ })?;
+
+ info!(
+ "Infrastructure diagnostics complete. Found {} issues.",
+ output.issues.len()
+ );
+
+ // Format and display output
+ if json_output {
+ format_json_output(&output)
+ } else {
+ format_human_readable_output(&output, verbosity, &infra_map)
+ }
+}
+
+/// Format output as JSON
+fn format_json_output(output: &DiagnosticOutput) -> Result {
+ let json_str = serde_json::to_string_pretty(output).map_err(|e| {
+ RoutineFailure::error(Message {
+ action: "Doctor".to_string(),
+ details: format!("Failed to format output as JSON: {}", e),
+ })
+ })?;
+
+ Ok(RoutineSuccess::highlight(Message {
+ action: "Doctor".to_string(),
+ details: json_str,
+ }))
+}
+
+/// Format output as human-readable text
+fn format_human_readable_output(
+ output: &DiagnosticOutput,
+ verbosity: u8,
+ _infra_map: &InfrastructureMap,
+) -> Result {
+ let mut result = String::new();
+
+ // Show each issue
+ for issue in &output.issues {
+ result.push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
+ result.push_str(&format!("{:?}: {}\n", issue.severity, issue.source));
+ result.push_str(&format!("Component: {}\n", issue.component.name));
+ result.push_str(&format!("Message: {}\n", issue.message));
+
+ if !issue.suggested_action.is_empty() {
+ result.push_str(&format!("Suggested Action: {}\n", issue.suggested_action));
+ }
+
+ // Show details based on verbosity
+ if verbosity >= 1 {
+ // -v: Add component metadata
+ if !issue.component.metadata.is_empty() {
+ result.push_str("Metadata:\n");
+ for (key, value) in &issue.component.metadata {
+ result.push_str(&format!(" {}: {}\n", key, value));
+ }
+ }
+ }
+
+ if verbosity >= 3 {
+ // -vvv: Add all details
+ if !issue.details.is_empty() {
+ result.push_str("Details:\n");
+ for (key, value) in &issue.details {
+ result.push_str(&format!(" {}: {}\n", key, value));
+ }
+ }
+ }
+
+ result.push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n");
+ }
+
+ // Show summary
+ let error_count = output.summary.by_severity.get("Error").unwrap_or(&0);
+ let warning_count = output.summary.by_severity.get("Warning").unwrap_or(&0);
+ let info_count = output.summary.by_severity.get("Info").unwrap_or(&0);
+
+ result.push_str(&format!(
+ "Summary: {} errors, {} warnings, {} info messages\n",
+ error_count, warning_count, info_count
+ ));
+
+ // Show breakdown by component (verbosity >= 2)
+ if verbosity >= 2 && !output.summary.by_component.is_empty() {
+ result.push_str("\nIssues by component:\n");
+ for (component, count) in &output.summary.by_component {
+ result.push_str(&format!(" - {}: {} issue(s)\n", component, count));
+ }
+ }
+
+ Ok(RoutineSuccess::highlight(Message {
+ action: "Doctor".to_string(),
+ details: result,
+ }))
+}
diff --git a/apps/framework-cli/src/cli/routines/mod.rs b/apps/framework-cli/src/cli/routines/mod.rs
index e508cf621..41704df8e 100644
--- a/apps/framework-cli/src/cli/routines/mod.rs
+++ b/apps/framework-cli/src/cli/routines/mod.rs
@@ -161,6 +161,7 @@ pub mod clean;
pub mod code_generation;
pub mod dev;
pub mod docker_packager;
+pub mod doctor;
pub mod kafka_pull;
pub mod logs;
pub mod ls;
diff --git a/apps/framework-cli/src/utilities/capture.rs b/apps/framework-cli/src/utilities/capture.rs
index 106f3a16d..589b06846 100644
--- a/apps/framework-cli/src/utilities/capture.rs
+++ b/apps/framework-cli/src/utilities/capture.rs
@@ -67,6 +67,8 @@ pub enum ActivityType {
PeekCommand,
#[serde(rename = "queryCommand")]
QueryCommand,
+ #[serde(rename = "doctorCommand")]
+ DoctorCommand,
#[serde(rename = "workflowCommand")]
WorkflowCommand,
#[serde(rename = "workflowInitCommand")]
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 20d90858f..fde755e67 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -15357,7 +15357,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)):
+ eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
@@ -15408,7 +15408,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.1(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1))
+ eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3