diff --git a/src/cli/cli.c b/src/cli/cli.c index d981af266..3d2405610 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -7376,6 +7376,12 @@ static const char *detect_arch(void) { /* ── Agent config install/refresh (shared by install + update) ── */ +/* Set by `install --clients=...` after validation and consumed by both the + * legacy detector and the registry-backed client path (#1558, #1798). */ +static const char *g_client_selection = NULL; +static bool cli_clients_apply_selection(const char *spec, cbm_detected_agents_t *detected); +static bool cli_clients_selects_registry_client(const char *spec, cbm_agent_client_id_t client_id); +static void cli_clients_print_list(FILE *out); static void print_detected_registry_agents(const char *home, bool *any); /* Print detected agent names on a single line. */ @@ -8169,7 +8175,10 @@ static void print_detected_registry_agents(const char *home, bool *any) { cbm_init_agent_registry_context(home, ®istry); for (size_t index = 0U; index < cbm_agent_client_count(); index++) { const cbm_agent_client_profile_t *profile = cbm_agent_client_at(index); - if (profile && cbm_agent_client_detect(profile->id, ®istry.options)) { + if (profile && + (!g_client_selection || + cli_clients_selects_registry_client(g_client_selection, profile->id)) && + cbm_agent_client_detect(profile->id, ®istry.options)) { printf(" %s", profile->display_name); *any = true; } @@ -8525,7 +8534,10 @@ static void install_agent_client_registry(const char *home, const char *binary_p cbm_init_agent_registry_context(home, ®istry); for (size_t index = 0U; index < cbm_agent_client_count(); index++) { const cbm_agent_client_profile_t *profile = cbm_agent_client_at(index); - if (!profile || !cbm_agent_client_detect(profile->id, ®istry.options)) { + if (!profile || + (g_client_selection && + !cli_clients_selects_registry_client(g_client_selection, profile->id)) || + !cbm_agent_client_detect(profile->id, ®istry.options)) { continue; } if (!g_install_plan) { @@ -9365,13 +9377,6 @@ static void install_additional_agent_configs(const cbm_detected_agents_t *agents } } -/* #1558: set by `install --clients=...` after validation, consumed at the one - * place detection happens. Validation runs in cbm_cmd_install so an unknown - * token fails before anything is written, not midway through configuring. */ -static const char *g_client_selection = NULL; -static bool cli_clients_apply_selection(const char *spec, cbm_detected_agents_t *detected); -static void cli_clients_print_list(FILE *out); - int cbm_install_agent_configs(const char *home, const char *binary_path, bool force, bool dry_run) { g_agent_install_errors = 0; cbm_detected_agents_t agents = cbm_detect_agents(home); @@ -9499,9 +9504,9 @@ int cbm_install_handle_existing_indexes(const char *home, bool reset, bool dry_r * Codex had to revert the OpenCode and Cursor integrations by hand, and the * next install silently recreated them. `--clients=claude,codex` restricts it. * - * The table is the flag's vocabulary AND its documentation: 26 clients ship - * here, with tokens nobody would guess (factory-droid, mistral-vibe, - * copilot-cli), so `--clients=help` prints every token. A selector whose + * The tables are the flag's vocabulary AND its documentation, with tokens + * nobody would guess (factory-droid, mistral-vibe, copilot-cli), so + * `--clients=help` prints every token. A selector whose * accepted values can only be learned from the source is not a usable * selector, and an unrecognised token fails loudly with the list rather than * silently configuring nothing. */ @@ -9549,10 +9554,40 @@ static void cli_clients_print_list(FILE *out) { for (size_t i = 0; i < CLI_CLIENT_COUNT; i++) { (void)fprintf(out, " %-16s %s\n", CLI_CLIENTS[i].token, CLI_CLIENTS[i].display); } + for (size_t i = 0; i < cbm_agent_client_count(); i++) { + const cbm_agent_client_profile_t *profile = cbm_agent_client_at(i); + if (profile) { + (void)fprintf(out, " %-16s %s\n", profile->stable_id, profile->display_name); + } + } (void)fprintf(out, "\nExample: --clients=claude,codex\n" "Omit --clients to configure every detected client.\n"); } +static bool cli_clients_spec_contains(const char *spec, const char *wanted_token) { + char buf[CLI_BUF_1K]; + snprintf(buf, sizeof(buf), "%s", spec ? spec : ""); + char *save = NULL; + for (char *tok = strtok_r(buf, ",", &save); tok; tok = strtok_r(NULL, ",", &save)) { + while (*tok == ' ') { + tok++; + } + size_t len = strlen(tok); + while (len > 0 && tok[len - 1] == ' ') { + tok[--len] = '\0'; + } + if (strcmp(tok, wanted_token) == 0) { + return true; + } + } + return false; +} + +static bool cli_clients_selects_registry_client(const char *spec, cbm_agent_client_id_t client_id) { + const cbm_agent_client_profile_t *profile = cbm_agent_client_by_id(client_id); + return profile && cli_clients_spec_contains(spec, profile->stable_id); +} + /* Restrict `detected` to the comma-separated token list. Returns false (after * printing the vocabulary) when a token is unknown, so a typo can never be * mistaken for "that client was not installed". */ @@ -9581,6 +9616,9 @@ static bool cli_clients_apply_selection(const char *spec, cbm_detected_agents_t break; } } + if (!matched) { + matched = cbm_agent_client_by_stable_id(tok) != NULL; + } if (!matched) { (void)fprintf(stderr, "error: unknown client: %s\n\n", tok); cli_clients_print_list(stderr); @@ -9606,6 +9644,9 @@ size_t cbm_cli_clients_count_for_testing(void) { const char *cbm_cli_clients_token_for_testing(size_t index) { return index < CLI_CLIENT_COUNT ? CLI_CLIENTS[index].token : NULL; } +void cbm_cli_set_client_selection_for_testing(const char *spec) { + g_client_selection = spec; +} #endif /* ── Subcommand: install ──────────────────────────────────────── */ @@ -10018,7 +10059,7 @@ int cbm_cmd_install(int argc, char **argv) { } } else if (strcmp(argv[i], "--clients") == 0) { /* Bare `--clients` (and `--clients=help`/`list`) print the - * vocabulary. 26 clients ship with tokens nobody would guess. */ + * vocabulary, including tokens users would not readily guess. */ cli_clients_print_list(stdout); return 0; } else if (strcmp(argv[i], "--skip-binary") == 0) { diff --git a/src/cli/cli.h b/src/cli/cli.h index c2121a940..fe115c5ac 100644 --- a/src/cli/cli.h +++ b/src/cli/cli.h @@ -208,6 +208,7 @@ int cbm_install_agent_configs(const char *home, const char *binary_path, bool fo bool cbm_cli_clients_apply_selection_for_testing(const char *spec, cbm_detected_agents_t *detected); size_t cbm_cli_clients_count_for_testing(void); const char *cbm_cli_clients_token_for_testing(size_t index); +void cbm_cli_set_client_selection_for_testing(const char *spec); #endif #ifdef CBM_CLI_ENABLE_TEST_API diff --git a/tests/test_cli.c b/tests/test_cli.c index 6295479f2..abcbf8b66 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -12794,7 +12794,7 @@ TEST(cli_external_manager_detection_needs_positive_evidence_issue1566) { * Claude and Codex had to revert OpenCode and Cursor by hand — and the next * install silently recreated them. The selector restricts it. * - * The vocabulary is the part that makes it usable: 26 clients ship, with tokens + * The vocabulary is the part that makes it usable: clients ship with tokens * nobody would guess (factory-droid, mistral-vibe, copilot-cli). A selector * whose accepted values can only be learned by reading our source is not a * usable selector, so this pins that every client is listed and that an unknown @@ -12823,6 +12823,12 @@ TEST(cli_clients_selector_vocabulary_is_complete_and_strict_issue1558) { cbm_detected_agents_t typo = all; ASSERT_FALSE(cbm_cli_clients_apply_selection_for_testing("claude,codx", &typo)); + /* Registry-backed clients share the public selector vocabulary. */ + cbm_detected_agents_t registry = all; + ASSERT_TRUE(cbm_cli_clients_apply_selection_for_testing("qoder", ®istry)); + ASSERT_FALSE(registry.claude_code); + ASSERT_FALSE(registry.cursor); + /* Every token in the table must resolve — a client added to detection but * forgotten here is invisible to the selector. */ for (size_t i = 0; i < cbm_cli_clients_count_for_testing(); i++) { @@ -12834,6 +12840,34 @@ TEST(cli_clients_selector_vocabulary_is_complete_and_strict_issue1558) { PASS(); } +TEST(cli_clients_selector_filters_registry_installs_issue1798) { + char *tmpdir = th_mktempdir("cbm_cli_clients_registry"); + ASSERT_NOT_NULL(tmpdir); + + char qoder_dir[512]; + char settings_path[512]; + ASSERT(snprintf(qoder_dir, sizeof(qoder_dir), "%s/.qoder", tmpdir) > 0); + ASSERT(snprintf(settings_path, sizeof(settings_path), "%s/settings.json", qoder_dir) > 0); + ASSERT_EQ(test_mkdirp(qoder_dir), 0); + + cbm_cli_set_client_selection_for_testing("cursor"); + int excluded_rc = cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + struct stat settings_state; + bool excluded = stat(settings_path, &settings_state) != 0; + + cbm_cli_set_client_selection_for_testing("qoder"); + int included_rc = cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + bool included = stat(settings_path, &settings_state) == 0; + cbm_cli_set_client_selection_for_testing(NULL); + + test_rmdir_r(tmpdir); + ASSERT_EQ(excluded_rc, 0); + ASSERT_TRUE(excluded); + ASSERT_EQ(included_rc, 0); + ASSERT_TRUE(included); + PASS(); +} + TEST(cli_update_accepts_retired_variant_flags_issue1544) { char *ui_argv[] = {"--dry-run", "--ui", "--yes"}; char *standard_argv[] = {"--dry-run", "--standard", "--yes"}; @@ -12981,6 +13015,7 @@ SUITE(cli) { RUN_TEST(cli_skill_frontmatter_scalars_with_colons_are_quoted_issue1554); RUN_TEST(cli_external_manager_detection_needs_positive_evidence_issue1566); RUN_TEST(cli_clients_selector_vocabulary_is_complete_and_strict_issue1558); + RUN_TEST(cli_clients_selector_filters_registry_installs_issue1798); RUN_TEST(cli_update_accepts_retired_variant_flags_issue1544); RUN_TEST(cli_update_agent_configs_finish_before_guard_release); RUN_TEST(cli_uninstall_quiesces_active_cohort_before_removing_binary_and_index);