diff --git a/crates/cli/src/plugin_install/host.rs b/crates/cli/src/plugin_install/host.rs index a6f5a87e9..3bc2355fe 100644 --- a/crates/cli/src/plugin_install/host.rs +++ b/crates/cli/src/plugin_install/host.rs @@ -220,17 +220,23 @@ fn codex_plugin_registered( options: &PluginInstallOptions, runner: &dyn CommandRunner, ) -> Result { - let output = run_capture_command( - "codex", - &["plugin".into(), "list".into(), "--json".into()], - options, - runner, - )?; - let plugins = parse_json_command_output("codex plugin list --json", output)?; - Ok(plugins - .get("installed") - .and_then(Value::as_array) - .is_some_and(|plugins| plugins.iter().any(plugin_entry_matches))) + // Codex `plugin list` has no `--json` flag (unlike Claude Code). + let output = run_capture_command("codex", &["plugin".into(), "list".into()], options, runner)?; + let plugin_id = format!("{PLUGIN_NAME}@{MARKETPLACE_NAME}"); + Ok(output + .stdout + .lines() + .any(|line| codex_plugin_line_installed(line, &plugin_id))) +} + +fn codex_plugin_line_installed(line: &str, plugin_id: &str) -> bool { + let mut columns = line.split_whitespace(); + if columns.next() != Some(plugin_id) { + return false; + } + columns + .next() + .is_some_and(|status| status.starts_with("installed")) } fn codex_marketplace_registered( diff --git a/crates/cli/tests/coverage/plugin_install_tests.rs b/crates/cli/tests/coverage/plugin_install_tests.rs index 4d6535190..dc8b706d3 100644 --- a/crates/cli/tests/coverage/plugin_install_tests.rs +++ b/crates/cli/tests/coverage/plugin_install_tests.rs @@ -560,20 +560,7 @@ fn host_command_helpers_cover_dry_run_missing_failure_and_reporting() { let runner = MockRunner::default() .with_executable("codex", "/bin/codex") - .with_capture_output("/bin/codex plugin list --json", "not json") - .with_capture_output("/bin/codex plugin marketplace list", ""); - assert!( - host_registration_report(PluginHost::Codex, &normal, &runner) - .unwrap_err() - .contains("failed to parse") - ); - - let runner = MockRunner::default() - .with_executable("codex", "/bin/codex") - .with_capture_output( - "/bin/codex plugin list --json", - json!({"installed": []}).to_string(), - ) + .with_capture_output("/bin/codex plugin list", "PLUGIN STATUS VERSION PATH\n") .with_capture_output("/bin/codex plugin marketplace list", "MARKETPLACE ROOT\n"); let error = validate_host_registration(PluginHost::Codex, &normal, &runner).unwrap_err(); assert!( @@ -618,30 +605,38 @@ fn host_registration_report_accepts_claude_and_codex_shape_variants() { assert!(report.host_marketplace_registered); } - for plugin_entry in [ - json!({"id": plugin_id.clone()}), - json!({"pluginId": plugin_id.clone()}), - json!({"name": PLUGIN_NAME, "marketplaceName": MARKETPLACE_NAME}), - ] { - let runner = MockRunner::default() - .with_executable("codex", "/bin/codex") - .with_capture_output( - "/bin/codex plugin list --json", - json!({"installed": [plugin_entry]}).to_string(), - ) - .with_capture_output( - "/bin/codex plugin marketplace list", - format!("{MARKETPLACE_NAME} /tmp/nemo-relay-local\n"), - ); - let report = host_registration_report(PluginHost::Codex, &normal, &runner).unwrap(); - assert!(report.ok()); - } + let runner = MockRunner::default() + .with_executable("codex", "/bin/codex") + .with_capture_output( + "/bin/codex plugin list", + format!("{plugin_id} installed, enabled 0.4.0 /tmp/nemo-relay-plugin\n"), + ) + .with_capture_output( + "/bin/codex plugin marketplace list", + format!("{MARKETPLACE_NAME} /tmp/nemo-relay-local\n"), + ); + let report = host_registration_report(PluginHost::Codex, &normal, &runner).unwrap(); + assert!(report.ok()); + + let runner = MockRunner::default() + .with_executable("codex", "/bin/codex") + .with_capture_output( + "/bin/codex plugin list", + format!("{plugin_id} not installed\n"), + ) + .with_capture_output( + "/bin/codex plugin marketplace list", + format!("{MARKETPLACE_NAME} /tmp/nemo-relay-local\n"), + ); + let report = host_registration_report(PluginHost::Codex, &normal, &runner).unwrap(); + assert!(!report.host_plugin_registered); + assert!(report.host_marketplace_registered); let runner = MockRunner::default() .with_executable("codex", "/bin/codex") .with_capture_output( - "/bin/codex plugin list --json", - json!({"installed": [{"name": PLUGIN_NAME, "marketplaceName": "other"}]}).to_string(), + "/bin/codex plugin list", + format!("{PLUGIN_NAME}@other installed, enabled 0.4.0 /tmp/other\n"), ) .with_capture_output("/bin/codex plugin marketplace list", "other /tmp/other\n"); let report = host_registration_report(PluginHost::Codex, &normal, &runner).unwrap(); @@ -655,6 +650,15 @@ fn host_registration_report_surfaces_capture_status_and_stderr_variants() { let dir = tempdir().unwrap(); let normal = options(dir.path()); + let runner = MockRunner::default() + .with_executable("claude", "/bin/claude") + .with_capture_output("/bin/claude plugin list --json", "not json"); + assert!( + host_registration_report(PluginHost::ClaudeCode, &normal, &runner) + .unwrap_err() + .contains("failed to parse") + ); + let runner = MockRunner::default() .with_executable("claude", "/bin/claude") .with_capture_status( @@ -1328,13 +1332,9 @@ fn doctor_json_uses_quiet_plugin_report() { .with_executable("nemo-relay", "/bin/nemo-relay") .with_executable("codex", "/bin/codex") .with_capture_output( - "/bin/codex plugin list --json", - json!({ - "installed": [ - { "pluginId": "nemo-relay-plugin@nemo-relay-local" } - ] - }) - .to_string(), + "/bin/codex plugin list", + "PLUGIN STATUS VERSION PATH\n\ + nemo-relay-plugin@nemo-relay-local installed, enabled 0.4.0 /tmp/nemo-relay-plugin\n", ) .with_capture_output( "/bin/codex plugin marketplace list", @@ -1357,7 +1357,7 @@ fn doctor_json_uses_quiet_plugin_report() { assert_eq!( runner.capture_commands(), vec![ - "/bin/codex plugin list --json", + "/bin/codex plugin list", "/bin/codex plugin marketplace list" ] );