Merge pull request #2822 from divakantpandey/fix/2722-install-managed-binary-path

fix(cli): use managed binary path for agent configs when externally managed (#2722)
This commit is contained in:
Martin Vogel authored and GitHub committed 2026-10-31 13:44:54 +01:00
commit 70f6de8a2f
3 files changed
+117 -20

No files matched your search

+55 -20
View File
@@ -10988,7 +10988,20 @@ void cbm_cli_set_client_selection_for_testing(const char *spec) {
/* Detect the running binary's path at runtime. The return value distinguishes
* OS-reported identity (safe ownership evidence) from the ~/.local/bin fallback
* used only as an install copy source. */
#ifdef CBM_CLI_ENABLE_TEST_API
static const char *g_cli_self_path_override_for_test = NULL;
void cbm_cli_set_self_path_override_for_test(const char *self_path) {
g_cli_self_path_override_for_test = self_path;
}
#endif
static bool cbm_detect_self_path(char *buf, size_t buf_sz, const char *home) {
#ifdef CBM_CLI_ENABLE_TEST_API
if (g_cli_self_path_override_for_test) {
snprintf(buf, buf_sz, "%s", g_cli_self_path_override_for_test);
return true;
}
#endif
buf[0] = '\0';
bool exact = false;
#ifdef _WIN32
@@ -11250,6 +11263,7 @@ char *cbm_build_install_plan_json(const char *home, const char *binary_path) {
typedef struct {
const char *bin_target;
const char *config_binary;
const char *bin_dir;
const char *home;
const char *shell_rc;
@@ -11274,6 +11288,8 @@ static int cli_install_activate(void *opaque) {
!activation->shell_rc) {
return CLI_TRUE;
}
const char *config_binary =
activation->config_binary ? activation->config_binary : activation->bin_target;
char previous_managed_binary[CLI_BUF_1K] = {0};
bool previous_managed_binary_exact = cbm_detect_self_path(
previous_managed_binary, sizeof(previous_managed_binary), activation->home);
@@ -11322,10 +11338,13 @@ static int cli_install_activate(void *opaque) {
* including same-binary and non-force installs. */
int agent_config_rc = CLI_OK;
if (!activation->skip_config) {
const char *prev = previous_managed_binary_exact ? previous_managed_binary : NULL;
if (config_binary != activation->bin_target &&
(!prev || strcmp(prev, config_binary) == 0)) {
prev = activation->bin_target;
}
agent_config_rc = cbm_install_agent_configs_with_previous(
activation->home, activation->bin_target,
previous_managed_binary_exact ? previous_managed_binary : NULL, activation->force,
activation->dry_run);
activation->home, config_binary, prev, activation->force, activation->dry_run);
}
if (agent_config_rc != CLI_OK) {
cli_activation_transaction_finalize_committed_or_fail_stop(
@@ -11472,11 +11491,40 @@ int cbm_cmd_install(int argc, char **argv) {
return CLI_TRUE;
}
char self_path[CLI_BUF_1K] = {0};
bool self_path_exact = cbm_detect_self_path(self_path, sizeof(self_path), home);
/* #1566: a binary owned by mise/Homebrew/nix is not ours to relocate. Infer
* it, and let either flag override the inference in either direction. */
bool externally_managed =
!force_binary && cli_binary_is_externally_managed(self_path, self_path_exact, bin_dir);
bool explicit_skip_binary = skip_binary;
if (externally_managed) {
skip_binary = true;
}
/* NOT stat(): on Windows it goes through the ANSI code page, so an
* extended-length or non-ASCII target reports "absent" and a non-force
* install silently overwrites bytes the user asked to keep. */
cbm_path_info_t target_status;
bool target_exists = cbm_path_info_utf8(bin_target, &target_status) == 0;
bool same_binary = cbm_same_file(self_path, bin_target);
/* #2722: when a binary is managed externally (mise, brew, nix, etc.),
* do not write the non-existent default target into agent configurations.
* Use the running managed binary path instead. */
const char *config_binary = bin_target;
if (externally_managed && self_path_exact && self_path[0]) {
config_binary = self_path;
} else if (skip_binary && !target_exists && self_path_exact && self_path[0]) {
config_binary = self_path;
}
/* --plan: emit the machine-readable install receipt and exit WITHOUT
* mutating anything (no config writes, no index deletion, no network) so
* an agent can inspect exactly what install would touch first (#388). */
if (plan) {
char *json = cbm_build_install_plan_json_options(home, bin_target, skip_config);
char *json = cbm_build_install_plan_json_options(home, config_binary, skip_config);
if (!json) {
(void)fprintf(stderr, "error: failed to build install plan\n");
return CLI_TRUE;
@@ -11498,16 +11546,9 @@ int cbm_cmd_install(int argc, char **argv) {
printf("codebase-memory-mcp install %s\n\n", CBM_VERSION);
char self_path[CLI_BUF_1K] = {0};
bool self_path_exact = cbm_detect_self_path(self_path, sizeof(self_path), home);
/* #1566: a binary owned by mise/Homebrew/nix is not ours to relocate. Infer
* it, and let either flag override the inference in either direction. */
bool externally_managed =
!force_binary && cli_binary_is_externally_managed(self_path, self_path_exact, bin_dir);
if (externally_managed || skip_binary) {
if (externally_managed || explicit_skip_binary) {
const char *manager = cli_external_manager_name(self_path);
if (skip_binary) {
if (explicit_skip_binary) {
printf("Skipping binary placement (--skip-binary); configuring agents only.\n\n");
} else {
printf("Binary is managed elsewhere%s%s:\n %s\n"
@@ -11515,15 +11556,8 @@ int cbm_cmd_install(int argc, char **argv) {
" (use --force-binary to install a copy into %s anyway)\n\n",
manager ? " by " : "", manager ? manager : "", self_path, bin_dir);
}
skip_binary = true;
}
/* NOT stat(): on Windows it goes through the ANSI code page, so an
* extended-length or non-ASCII target reports "absent" and a non-force
* install silently overwrites bytes the user asked to keep. */
cbm_path_info_t target_status;
bool target_exists = cbm_path_info_utf8(bin_target, &target_status) == 0;
bool same_binary = cbm_same_file(self_path, bin_target);
bool do_copy = !skip_binary && !same_binary && (!target_exists || force);
/* (#607) Default: preserve existing indexes. `--reset-indexes` opts into
@@ -11690,6 +11724,7 @@ int cbm_cmd_install(int argc, char **argv) {
#endif
cli_install_activation_t activation = {
.bin_target = bin_target,
.config_binary = config_binary,
.bin_dir = bin_dir,
.home = home,
.shell_rc = shell_rc,
+2
View File
@@ -250,6 +250,8 @@ void cbm_cli_set_client_selection_for_testing(const char *spec);
* not recognisably foreign. Detection must be positive evidence — see the
* implementation comment. */
const char *cbm_cli_external_manager_name_for_testing(const char *self_path);
/* #2722: override runtime binary self-path detection for testing. */
void cbm_cli_set_self_path_override_for_test(const char *self_path);
/* #1558: expose the config-key table so a test can prove a key is discoverable
* (`config list`/`set` both walk it). */
size_t cbm_cli_config_key_count_for_testing(void);
+60
View File
@@ -18673,6 +18673,65 @@ TEST(cli_external_manager_detection_needs_positive_evidence_issue1566) {
PASS();
}
/* #2722: when a binary is managed externally (e.g. mise, brew, nix), `install`
* skips placing a binary at ~/.local/bin, but must write the running managed
* binary path into agent configuration files instead of the non-existent
* default target. */
TEST(cli_install_managed_binary_config_path_issue2722) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-install-managed-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("cbm_mkdtemp failed");
}
char *old_home = NULL;
char *old_cache = NULL;
cli_activation_save_env(&old_home, &old_cache);
cbm_setenv("HOME", tmpdir, 1);
char cache_dir[512];
snprintf(cache_dir, sizeof(cache_dir), "%s/cache", tmpdir);
cbm_setenv("CBM_CACHE_DIR", cache_dir, 1);
test_mark_claude_code_installed(tmpdir);
const char *managed_path =
"/Users/x/.local/share/mise/installs/cbm/0.12.1/bin/codebase-memory-mcp";
cbm_cli_set_self_path_override_for_test(managed_path);
cli_activation_fake_t fake = {
.mutation_reserve_result = 1,
};
cbm_cli_activation_ops_t ops = cli_activation_fake_ops(&fake);
cbm_cli_set_activation_ops_for_test(&ops);
char *argv[] = {"--clients=claude"};
int rc = cli_test_cmd_install(1, argv);
cbm_cli_set_activation_ops_for_test(NULL);
cbm_cli_set_self_path_override_for_test(NULL);
char bin_target[512];
snprintf(bin_target, sizeof(bin_target), "%s/.local/bin/codebase-memory-mcp", tmpdir);
struct stat target_st;
bool target_created = stat(bin_target, &target_st) == 0;
char claude_json_path[512];
snprintf(claude_json_path, sizeof(claude_json_path), "%s/.claude.json", tmpdir);
char *claude_json = read_test_file_alloc(claude_json_path);
bool has_managed_path = claude_json && strstr(claude_json, managed_path) != NULL;
bool has_target_path =
claude_json && strstr(claude_json, ".local/bin/codebase-memory-mcp") != NULL;
free(claude_json);
cli_activation_restore_env(old_home, old_cache);
test_rmdir_r(tmpdir);
ASSERT_EQ(rc, 0);
ASSERT_FALSE(target_created);
ASSERT_TRUE(has_managed_path);
ASSERT_FALSE(has_target_path);
PASS();
}
/* #1558: `install` configured EVERY detected client, so a user who wanted
* Claude and Codex had to revert OpenCode and Cursor by hand — and the next
* install silently recreated them. The selector restricts it.
@@ -19090,6 +19149,7 @@ SUITE(cli) {
RUN_TEST(cli_ui_config_keys_are_discoverable_and_settable_issue1558);
RUN_TEST(cli_skill_frontmatter_scalars_with_colons_are_quoted_issue1554);
RUN_TEST(cli_external_manager_detection_needs_positive_evidence_issue1566);
RUN_TEST(cli_install_managed_binary_config_path_issue2722);
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);