Skip to content
3 changes: 2 additions & 1 deletion Makefile.cbm
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ SANITIZE = -fsanitize=address,undefined -fno-omit-frame-pointer
EDITOR_TEST_DEFINES = -DCBM_JSON_LIKE_ENABLE_TEST_API=1 \
-DCBM_TOML_EDIT_ENABLE_TEST_API=1 -DCBM_YAML_ENABLE_TEST_API=1 \
-DCBM_TEXT_EDIT_ENABLE_TEST_API=1 -DCBM_CLI_ENABLE_TEST_API=1 \
-DCBM_CONFIG_EDIT_PATH_ENABLE_TEST_API=1 \
-DCBM_DIAGNOSTICS_ENABLE_TEST_API=1 -DCBM_ENABLE_TEST_SEAMS=1
# The build system is the single source of truth for "is this binary
# instrumented": compiler-specific probes (__SANITIZE_ADDRESS__) miss
Expand Down Expand Up @@ -409,7 +410,7 @@ GIT_SRCS = src/git/git_context.c
CLI_SRCS = src/cli/cli.c src/cli/progress_sink.c src/cli/hook_augment.c src/cli/client_adapter.c \
src/cli/agent_clients.c src/cli/agent_profiles.c \
src/cli/config_json_like.c src/cli/config_toml_edit.c src/cli/config_yaml_edit.c \
src/cli/config_text_edit.c src/cli/activation_transaction.c
src/cli/config_text_edit.c src/cli/config_edit_path.c src/cli/activation_transaction.c

# UI module (graph visualization)
UI_SRCS = \
Expand Down
139 changes: 131 additions & 8 deletions src/cli/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "cli/agent_profiles.h"
#include "cli/cli.h"
#include "cli/activation_transaction.h"
#include "cli/config_edit_path.h"
#include "cli/config_json_like.h"
#include "cli/config_text_edit.h"
#include "cli/config_toml_edit.h"
Expand Down Expand Up @@ -7977,6 +7978,90 @@ static cbm_install_plan_t *g_install_plan = NULL;
static int g_agent_install_errors = 0;
static int g_agent_uninstall_errors = 0;

/* Every agent configuration uninstall could not clean, kept for the closing
* summary. A cleanup failure no longer stops executable and index removal
* (#1954: one symlinked ~/.cursor/mcp.json left a 300 MB binary plus the whole
* cache behind), so the user needs ONE list of what is still theirs to fix,
* with the observed reason next to each file. */
typedef struct {
char agent[64];
char operation[48];
char path[CLI_BUF_1K];
char reason[160];
char detail[160];
} cbm_agent_config_failure_t;

static cbm_agent_config_failure_t *g_agent_uninstall_failures = NULL;
static int g_agent_uninstall_failure_count = 0;
static int g_agent_uninstall_failure_cap = 0;

static void agent_uninstall_failures_reset(void) {
free(g_agent_uninstall_failures);
g_agent_uninstall_failures = NULL;
g_agent_uninstall_failure_count = 0;
g_agent_uninstall_failure_cap = 0;
}

static void agent_uninstall_failure_record(const char *agent, const char *operation,
const char *path, const char *reason,
const char *detail) {
if (g_agent_uninstall_failure_count >= g_agent_uninstall_failure_cap) {
int ncap = g_agent_uninstall_failure_cap ? g_agent_uninstall_failure_cap * 2 : CLI_BUF_16;
cbm_agent_config_failure_t *grown =
realloc(g_agent_uninstall_failures, (size_t)ncap * sizeof(*grown));
if (!grown) {
return;
}
g_agent_uninstall_failures = grown;
g_agent_uninstall_failure_cap = ncap;
}
cbm_agent_config_failure_t *entry =
&g_agent_uninstall_failures[g_agent_uninstall_failure_count++];
(void)snprintf(entry->agent, sizeof(entry->agent), "%s", agent ? agent : "unknown");
(void)snprintf(entry->operation, sizeof(entry->operation), "%s",
operation ? operation : "unknown");
(void)snprintf(entry->path, sizeof(entry->path), "%s", path ? path : "unknown");
(void)snprintf(entry->reason, sizeof(entry->reason), "%s", reason ? reason : "");
(void)snprintf(entry->detail, sizeof(entry->detail), "%s", detail ? detail : "");
}

/* The agent-configuration writers opt in to following user-owned symlinked
* config files under the user's configuration roots (#1954, decision C);
* every other caller of the config editors keeps refusing links. Cleared by
* the same command when its configuration work is done. */
static void cli_config_follow_begin(const char *home) {
cbm_config_edit_path_follow_clear();
if (home && home[0]) {
(void)cbm_config_edit_path_follow_add_root(home);
}
const char *xdg_config = getenv("XDG_CONFIG_HOME");
if (xdg_config && xdg_config[0]) {
(void)cbm_config_edit_path_follow_add_root(xdg_config);
}
}

/* The closing list of what uninstall could not clean. Printed AFTER the
* executable and the indexes are gone, so nothing in it is a reason to keep
* the installation around — each line is one file the user removes an entry
* from by hand. */
static void agent_uninstall_failures_report(bool dry_run) {
if (g_agent_uninstall_failure_count == 0) {
return;
}
(void)fprintf(stderr, "\nerror: uninstall %s with %d agent configuration(s) left uncleaned:\n",
dry_run ? "dry-run finished" : "finished", g_agent_uninstall_failure_count);
for (int i = 0; i < g_agent_uninstall_failure_count; i++) {
const cbm_agent_config_failure_t *entry = &g_agent_uninstall_failures[i];
(void)fprintf(stderr, " %s (%s): %s", entry->agent, entry->operation, entry->path);
if (entry->reason[0]) {
(void)fprintf(stderr, " reason=%s", entry->reason);
}
(void)fputs(entry->detail, stderr);
(void)fputc('\n', stderr);
}
(void)fputs("Remove the codebase-memory-mcp entries from these files by hand.\n", stderr);
}

static void plan_record(const char *agent, const char *kind, const char *path) {
if (!g_install_plan || !path || !path[0]) {
return;
Expand Down Expand Up @@ -8032,6 +8117,14 @@ static void describe_agent_config_target(const char *path, char *out, size_t out
: info.is_directory ? "directory"
: info.is_regular ? "regular file"
: "special file";
/* A refused symlink names the rule that refused it (#1954): the user
* then knows whether to fix ownership, the target, or the parent. */
char refusal[160];
if (info.is_symlink && cbm_config_edit_path_refusal(path, refusal, sizeof(refusal))) {
(void)snprintf(out, out_size, " (target: symlink, %lld bytes; not followed: %s)",
(long long)info.size, refusal);
return;
}
(void)snprintf(out, out_size, " (target: %s, %lld bytes)", kind, (long long)info.size);
}

Expand All @@ -8049,6 +8142,9 @@ static void record_agent_config_error_with_reason(bool uninstalling, const char
}
(void)fputs(detail, stderr);
(void)fputc('\n', stderr);
if (uninstalling) {
agent_uninstall_failure_record(agent, operation, path, reason, detail);
}
}

static void record_agent_config_error(bool uninstalling, const char *agent, const char *operation,
Expand Down Expand Up @@ -10083,12 +10179,24 @@ static void install_additional_agent_configs(const cbm_detected_agents_t *agents
}
}

static int cbm_install_agent_configs_in_scope(const char *home, const char *binary_path, bool force,
bool dry_run, cbm_detected_agents_t *agents_in);

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);
if (g_client_selection && !cli_clients_apply_selection(g_client_selection, &agents)) {
return CLI_ERR;
}
cli_config_follow_begin(home);
int result = cbm_install_agent_configs_in_scope(home, binary_path, force, dry_run, &agents);
cbm_config_edit_path_follow_clear();
return result;
}

static int cbm_install_agent_configs_in_scope(const char *home, const char *binary_path, bool force,
bool dry_run, cbm_detected_agents_t *agents_in) {
cbm_detected_agents_t agents = *agents_in;
if (!g_install_plan) {
print_detected_agents(&agents, home);
}
Expand Down Expand Up @@ -12398,21 +12506,22 @@ static int cli_uninstall_activate(void *opaque) {
return CLI_TRUE;
}

cli_config_follow_begin(activation->home);
if (activation->agents.claude_code) {
uninstall_claude_code(activation->home, activation->bin_path, activation->dry_run);
}
uninstall_cli_agents(&activation->agents, activation->home, activation->dry_run);
uninstall_editor_agents(&activation->agents, activation->home, activation->dry_run);
uninstall_additional_agents(&activation->agents, activation->home, activation->dry_run);
uninstall_agent_client_registry(activation->home, activation->dry_run);
cbm_config_edit_path_follow_clear();

if (g_agent_uninstall_errors != 0) {
cli_activation_transaction_abort_or_fail_stop(&activation->binary_transaction,
"uninstall_transaction_config_cleanup_abort");
(void)fprintf(stderr, "error: one or more agent cleanup operations failed; executable "
"and index removal were not started\n");
return CLI_ACTIVATION_PARTIAL;
}
/* Agent-config failures are collected, never a gate: an entry the editors
* refuse to touch (a symlinked config, a foreign file, a malformed
* document) is the user's to fix by hand, and leaving a 300 MB executable
* plus every index behind because of it is the data-loss shape of #1954.
* The indexes and the executable go now; the failures are listed at the
* end and decide the exit code. */

#ifdef _WIN32
/* #2117: install registers the install directory in the persistent
Expand Down Expand Up @@ -12550,6 +12659,7 @@ int cbm_cmd_uninstall(int argc, char **argv) {
printf("codebase-memory-mcp uninstall\n\n");

g_agent_uninstall_errors = 0;
agent_uninstall_failures_reset();
cbm_detected_agents_t agents = cbm_detect_agents(home);

/* Confirm index removal outside the startup lock, but defer the mutation
Expand Down Expand Up @@ -12619,6 +12729,19 @@ int cbm_cmd_uninstall(int argc, char **argv) {
(void)cli_activation_transaction_abort(&activation.binary_transaction);
}
if (activation_rc != CLI_OK) {
agent_uninstall_failures_reset();
return CLI_TRUE;
}

if (g_agent_uninstall_errors != 0) {
agent_uninstall_failures_report(dry_run);
agent_uninstall_failures_reset();
printf("\nUninstall finished with errors; the files listed above still hold "
"codebase-memory-mcp entries. Please restart your coding-agent sessions "
"to properly take this into account.\n");
if (dry_run) {
printf("(dry-run — no files were modified)\n");
}
return CLI_TRUE;
}

Expand All @@ -12627,7 +12750,7 @@ int cbm_cmd_uninstall(int argc, char **argv) {
if (dry_run) {
printf("(dry-run — no files were modified)\n");
}
return g_agent_uninstall_errors == 0 ? 0 : CLI_TRUE;
return 0;
}

/* ── Subcommand: update ───────────────────────────────────────── */
Expand Down
Loading
Loading