mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Fix remaining install/update gaps + in-memory zip extraction
Install command: - Add cbm_kill_other_instances() to kill stale MCP servers - Add cbm_macos_adhoc_sign() to sign binary if placed unsigned Update command: - Replace skills-only reinstall with full cbm_install_agent_configs() - Replace external unzip with cbm_extract_binary_from_zip() via zlib Refactor: extract 10-agent config loop into cbm_install_agent_configs() called by both install and update. New: cbm_extract_binary_from_zip() — in-memory zip extraction with stored + deflate support, path traversal rejection, bounds checks. 4 unit tests. Smoke: add install.ps1 E2E for Windows.
This commit is contained in:
1 parent
006e6dbf20
commit
8951647d3e
4 files changed
+586
-251
No files matched your search
+46
-1
@@ -1199,8 +1199,53 @@ if [ "$DL_OS" != "windows" ] && [ -f "$REPO_ROOT/install.sh" ]; then
|
||||
fi
|
||||
|
||||
rm -rf "$INSTALL_TEST_HOME" "$INSTALL_TEST_DIR"
|
||||
|
||||
elif [ -f "$REPO_ROOT/install.ps1" ] && command -v powershell.exe &>/dev/null; then
|
||||
echo "--- Phase 13: install.ps1 E2E (Windows) ---"
|
||||
PS1_TEST_HOME=$(mktemp -d)
|
||||
PS1_TEST_DIR=$(mktemp -d)
|
||||
mkdir -p "$PS1_TEST_HOME/.claude"
|
||||
|
||||
# Convert MSYS paths to Windows paths for PowerShell
|
||||
if command -v cygpath &>/dev/null; then
|
||||
WIN_DIR=$(cygpath -w "$PS1_TEST_DIR")
|
||||
WIN_URL="$SMOKE_DOWNLOAD_URL"
|
||||
WIN_SCRIPT=$(cygpath -w "$REPO_ROOT/install.ps1")
|
||||
WIN_HOME=$(cygpath -w "$PS1_TEST_HOME")
|
||||
else
|
||||
WIN_DIR="$PS1_TEST_DIR"
|
||||
WIN_URL="$SMOKE_DOWNLOAD_URL"
|
||||
WIN_SCRIPT="$REPO_ROOT/install.ps1"
|
||||
WIN_HOME="$PS1_TEST_HOME"
|
||||
fi
|
||||
|
||||
# 13f: run install.ps1
|
||||
HOME="$PS1_TEST_HOME" CBM_DOWNLOAD_URL="$WIN_URL" \
|
||||
powershell.exe -ExecutionPolicy ByPass -File "$WIN_SCRIPT" "--dir=$WIN_DIR" 2>&1 || true
|
||||
|
||||
# 13g: binary placed
|
||||
PS1_BIN="$PS1_TEST_DIR/codebase-memory-mcp.exe"
|
||||
if [ ! -f "$PS1_BIN" ] && [ -f "$PS1_TEST_DIR/codebase-memory-mcp" ]; then
|
||||
PS1_BIN="$PS1_TEST_DIR/codebase-memory-mcp"
|
||||
fi
|
||||
if [ -f "$PS1_BIN" ]; then
|
||||
echo "OK 13g: binary placed by install.ps1"
|
||||
else
|
||||
echo "FAIL 13g: binary not placed by install.ps1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 13h: binary runs
|
||||
if "$PS1_BIN" --version > /dev/null 2>&1; then
|
||||
echo "OK 13h: binary runs"
|
||||
else
|
||||
echo "FAIL 13h: installed binary doesn't run"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$PS1_TEST_HOME" "$PS1_TEST_DIR"
|
||||
else
|
||||
echo "SKIP Phase 13: install script not available for this platform"
|
||||
echo "SKIP Phase 13: no install script available for this platform"
|
||||
fi
|
||||
|
||||
else
|
||||
|
||||
+413
-250
@@ -1092,6 +1092,7 @@ int cbm_upsert_instructions(const char *path, const char *content) {
|
||||
} else {
|
||||
/* Append section */
|
||||
size_t new_len = existing_len + 1 + strlen(section);
|
||||
// NOLINTNEXTLINE(clang-analyzer-optin.taint.TaintedAlloc)
|
||||
result = malloc(new_len + 1);
|
||||
if (!result) {
|
||||
free(existing);
|
||||
@@ -1736,6 +1737,128 @@ unsigned char *cbm_extract_binary_from_targz(const unsigned char *data, int data
|
||||
return NULL; /* binary not found */
|
||||
}
|
||||
|
||||
/* ── Zip extraction (in-memory, replaces external unzip) ──────── */
|
||||
|
||||
unsigned char *cbm_extract_binary_from_zip(const unsigned char *data, int data_len, int *out_len) {
|
||||
if (!data || data_len <= 0 || !out_len) {
|
||||
return NULL;
|
||||
}
|
||||
*out_len = 0;
|
||||
|
||||
/* Zip local file header layout */
|
||||
enum {
|
||||
ZIP_SIG_0 = 0x50,
|
||||
ZIP_SIG_1 = 0x4B,
|
||||
ZIP_SIG_2 = 0x03,
|
||||
ZIP_SIG_3 = 0x04,
|
||||
ZIP_HDR_SZ = 30,
|
||||
ZIP_OFF_METHOD = 8,
|
||||
ZIP_OFF_COMP = 18,
|
||||
ZIP_OFF_UNCOMP = 22,
|
||||
ZIP_OFF_NAMELEN = 26,
|
||||
ZIP_OFF_EXTRALEN = 28,
|
||||
ZIP_STORED = 0,
|
||||
ZIP_DEFLATE = 8
|
||||
};
|
||||
static const uint32_t ZIP_MAX_UNCOMP = 500U * 1024U * 1024U;
|
||||
|
||||
int pos = 0;
|
||||
while (pos + ZIP_HDR_SZ <= data_len) {
|
||||
if (data[pos] != ZIP_SIG_0 || data[pos + 1] != ZIP_SIG_1 || data[pos + 2] != ZIP_SIG_2 ||
|
||||
data[pos + 3] != ZIP_SIG_3) {
|
||||
break;
|
||||
}
|
||||
|
||||
uint16_t method =
|
||||
(uint16_t)(data[pos + ZIP_OFF_METHOD] | (data[pos + ZIP_OFF_METHOD + 1] << 8));
|
||||
uint32_t comp_size =
|
||||
(uint32_t)(data[pos + ZIP_OFF_COMP] | (data[pos + ZIP_OFF_COMP + 1] << 8) |
|
||||
(data[pos + ZIP_OFF_COMP + 2] << 16) | (data[pos + ZIP_OFF_COMP + 3] << 24));
|
||||
uint32_t uncomp_size =
|
||||
(uint32_t)(data[pos + ZIP_OFF_UNCOMP] | (data[pos + ZIP_OFF_UNCOMP + 1] << 8) |
|
||||
(data[pos + ZIP_OFF_UNCOMP + 2] << 16) |
|
||||
(data[pos + ZIP_OFF_UNCOMP + 3] << 24));
|
||||
uint16_t name_len =
|
||||
(uint16_t)(data[pos + ZIP_OFF_NAMELEN] | (data[pos + ZIP_OFF_NAMELEN + 1] << 8));
|
||||
uint16_t extra_len =
|
||||
(uint16_t)(data[pos + ZIP_OFF_EXTRALEN] | (data[pos + ZIP_OFF_EXTRALEN + 1] << 8));
|
||||
|
||||
int header_end = pos + ZIP_HDR_SZ + name_len + extra_len;
|
||||
if (header_end + (int)comp_size > data_len) {
|
||||
break; /* Truncated */
|
||||
}
|
||||
|
||||
/* Extract filename */
|
||||
char fname[512] = {0};
|
||||
int fn_copy = name_len < (int)sizeof(fname) - 1 ? name_len : (int)sizeof(fname) - 1;
|
||||
memcpy(fname, data + pos + 30, (size_t)fn_copy);
|
||||
fname[fn_copy] = '\0';
|
||||
|
||||
/* Security: reject path traversal */
|
||||
if (strstr(fname, "..")) {
|
||||
pos = header_end + (int)comp_size;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Find the binary — basename match */
|
||||
const char *basename = strrchr(fname, '/');
|
||||
basename = basename ? basename + 1 : fname;
|
||||
|
||||
if (strcmp(basename, "codebase-memory-mcp") == 0 ||
|
||||
strcmp(basename, "codebase-memory-mcp.exe") == 0) {
|
||||
|
||||
const unsigned char *file_data = data + header_end;
|
||||
|
||||
if (method == ZIP_STORED) {
|
||||
if (comp_size > ZIP_MAX_UNCOMP) {
|
||||
return NULL;
|
||||
}
|
||||
unsigned char *out = malloc(comp_size);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
memcpy(out, file_data, comp_size);
|
||||
*out_len = (int)comp_size;
|
||||
return out;
|
||||
}
|
||||
if (method == ZIP_DEFLATE) {
|
||||
if (uncomp_size > ZIP_MAX_UNCOMP) {
|
||||
return NULL;
|
||||
}
|
||||
unsigned char *out = malloc(uncomp_size);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
z_stream strm = {0};
|
||||
strm.next_in = (unsigned char *)file_data;
|
||||
strm.avail_in = comp_size;
|
||||
strm.next_out = out;
|
||||
strm.avail_out = uncomp_size;
|
||||
|
||||
/* -MAX_WBITS = raw deflate (no zlib/gzip header) */
|
||||
if (inflateInit2(&strm, -MAX_WBITS) != Z_OK) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
int ret = inflate(&strm, Z_FINISH);
|
||||
inflateEnd(&strm);
|
||||
if (ret != Z_STREAM_END) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
*out_len = (int)strm.total_out;
|
||||
return out;
|
||||
}
|
||||
/* Unknown compression method */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pos = header_end + (int)comp_size;
|
||||
}
|
||||
|
||||
return NULL; /* binary not found */
|
||||
}
|
||||
|
||||
/* ── Index management ─────────────────────────────────────────── */
|
||||
|
||||
static const char *get_cache_dir(const char *home_dir) {
|
||||
@@ -2232,6 +2355,237 @@ static const char *detect_arch(void) {
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ── Agent config install/refresh (shared by install + update) ── */
|
||||
|
||||
static void cbm_install_agent_configs(const char *home, const char *binary_path, bool force,
|
||||
bool dry_run) {
|
||||
cbm_detected_agents_t agents = cbm_detect_agents(home);
|
||||
printf("Detected agents:");
|
||||
if (agents.claude_code) {
|
||||
printf(" Claude-Code");
|
||||
}
|
||||
if (agents.codex) {
|
||||
printf(" Codex");
|
||||
}
|
||||
if (agents.gemini) {
|
||||
printf(" Gemini-CLI");
|
||||
}
|
||||
if (agents.zed) {
|
||||
printf(" Zed");
|
||||
}
|
||||
if (agents.opencode) {
|
||||
printf(" OpenCode");
|
||||
}
|
||||
if (agents.antigravity) {
|
||||
printf(" Antigravity");
|
||||
}
|
||||
if (agents.aider) {
|
||||
printf(" Aider");
|
||||
}
|
||||
if (agents.kilocode) {
|
||||
printf(" KiloCode");
|
||||
}
|
||||
if (agents.vscode) {
|
||||
printf(" VS-Code");
|
||||
}
|
||||
if (agents.openclaw) {
|
||||
printf(" OpenClaw");
|
||||
}
|
||||
if (!agents.claude_code && !agents.codex && !agents.gemini && !agents.zed && !agents.opencode &&
|
||||
!agents.antigravity && !agents.aider && !agents.kilocode && !agents.vscode &&
|
||||
!agents.openclaw) {
|
||||
printf(" (none)");
|
||||
}
|
||||
printf("\n\n");
|
||||
|
||||
/* Claude Code: skills + MCP + hooks */
|
||||
if (agents.claude_code) {
|
||||
char skills_dir[1024];
|
||||
snprintf(skills_dir, sizeof(skills_dir), "%s/.claude/skills", home);
|
||||
printf("Claude Code:\n");
|
||||
|
||||
int skill_count = cbm_install_skills(skills_dir, force, dry_run);
|
||||
printf(" skills: %d installed\n", skill_count);
|
||||
|
||||
if (cbm_remove_old_monolithic_skill(skills_dir, dry_run)) {
|
||||
printf(" removed old monolithic skill\n");
|
||||
}
|
||||
|
||||
char mcp_path[1024];
|
||||
snprintf(mcp_path, sizeof(mcp_path), "%s/.claude/.mcp.json", home);
|
||||
if (!dry_run) {
|
||||
cbm_install_editor_mcp(binary_path, mcp_path);
|
||||
}
|
||||
printf(" mcp: %s\n", mcp_path);
|
||||
|
||||
char mcp_path2[1024];
|
||||
snprintf(mcp_path2, sizeof(mcp_path2), "%s/.claude.json", home);
|
||||
if (!dry_run) {
|
||||
cbm_install_editor_mcp(binary_path, mcp_path2);
|
||||
}
|
||||
printf(" mcp: %s\n", mcp_path2);
|
||||
|
||||
char settings_path[1024];
|
||||
snprintf(settings_path, sizeof(settings_path), "%s/.claude/settings.json", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_claude_hooks(settings_path);
|
||||
cbm_install_hook_gate_script(home);
|
||||
}
|
||||
printf(" hooks: PreToolUse (code discovery gate)\n");
|
||||
}
|
||||
|
||||
/* Codex CLI */
|
||||
if (agents.codex) {
|
||||
printf("Codex CLI:\n");
|
||||
char config_path[1024];
|
||||
snprintf(config_path, sizeof(config_path), "%s/.codex/config.toml", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_codex_mcp(binary_path, config_path);
|
||||
}
|
||||
printf(" mcp: %s\n", config_path);
|
||||
|
||||
char instr_path[1024];
|
||||
snprintf(instr_path, sizeof(instr_path), "%s/.codex/AGENTS.md", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_instructions(instr_path, agent_instructions_content);
|
||||
}
|
||||
printf(" instructions: %s\n", instr_path);
|
||||
}
|
||||
|
||||
/* Gemini CLI */
|
||||
if (agents.gemini) {
|
||||
printf("Gemini CLI:\n");
|
||||
char config_path[1024];
|
||||
snprintf(config_path, sizeof(config_path), "%s/.gemini/settings.json", home);
|
||||
if (!dry_run) {
|
||||
cbm_install_editor_mcp(binary_path, config_path);
|
||||
}
|
||||
printf(" mcp: %s\n", config_path);
|
||||
|
||||
char instr_path[1024];
|
||||
snprintf(instr_path, sizeof(instr_path), "%s/.gemini/GEMINI.md", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_instructions(instr_path, agent_instructions_content);
|
||||
}
|
||||
printf(" instructions: %s\n", instr_path);
|
||||
|
||||
if (!dry_run) {
|
||||
cbm_upsert_gemini_hooks(config_path);
|
||||
}
|
||||
printf(" hooks: BeforeTool (grep/file search reminder)\n");
|
||||
}
|
||||
|
||||
/* Zed */
|
||||
if (agents.zed) {
|
||||
printf("Zed:\n");
|
||||
char config_path[1024];
|
||||
#ifdef __APPLE__
|
||||
snprintf(config_path, sizeof(config_path),
|
||||
"%s/Library/Application Support/Zed/settings.json", home);
|
||||
#else
|
||||
snprintf(config_path, sizeof(config_path), "%s/.config/zed/settings.json", home);
|
||||
#endif
|
||||
if (!dry_run) {
|
||||
cbm_install_zed_mcp(binary_path, config_path);
|
||||
}
|
||||
printf(" mcp: %s\n", config_path);
|
||||
}
|
||||
|
||||
/* OpenCode */
|
||||
if (agents.opencode) {
|
||||
printf("OpenCode:\n");
|
||||
char config_path[1024];
|
||||
snprintf(config_path, sizeof(config_path), "%s/.config/opencode/opencode.json", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_opencode_mcp(binary_path, config_path);
|
||||
}
|
||||
printf(" mcp: %s\n", config_path);
|
||||
|
||||
char instr_path[1024];
|
||||
snprintf(instr_path, sizeof(instr_path), "%s/.config/opencode/AGENTS.md", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_instructions(instr_path, agent_instructions_content);
|
||||
}
|
||||
printf(" instructions: %s\n", instr_path);
|
||||
}
|
||||
|
||||
/* Antigravity */
|
||||
if (agents.antigravity) {
|
||||
printf("Antigravity:\n");
|
||||
char config_path[1024];
|
||||
snprintf(config_path, sizeof(config_path), "%s/.gemini/antigravity/mcp_config.json", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_antigravity_mcp(binary_path, config_path);
|
||||
}
|
||||
printf(" mcp: %s\n", config_path);
|
||||
|
||||
char instr_path[1024];
|
||||
snprintf(instr_path, sizeof(instr_path), "%s/.gemini/antigravity/AGENTS.md", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_instructions(instr_path, agent_instructions_content);
|
||||
}
|
||||
printf(" instructions: %s\n", instr_path);
|
||||
}
|
||||
|
||||
/* Aider */
|
||||
if (agents.aider) {
|
||||
printf("Aider:\n");
|
||||
char instr_path[1024];
|
||||
snprintf(instr_path, sizeof(instr_path), "%s/CONVENTIONS.md", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_instructions(instr_path, agent_instructions_content);
|
||||
}
|
||||
printf(" instructions: %s\n", instr_path);
|
||||
}
|
||||
|
||||
/* KiloCode */
|
||||
if (agents.kilocode) {
|
||||
printf("KiloCode:\n");
|
||||
char config_path[1024];
|
||||
snprintf(config_path, sizeof(config_path),
|
||||
"%s/.config/Code/User/globalStorage/kilocode.kilo-code/settings/mcp_settings.json",
|
||||
home);
|
||||
if (!dry_run) {
|
||||
cbm_install_editor_mcp(binary_path, config_path);
|
||||
}
|
||||
printf(" mcp: %s\n", config_path);
|
||||
|
||||
char instr_path[1024];
|
||||
snprintf(instr_path, sizeof(instr_path), "%s/.kilocode/rules/codebase-memory-mcp.md", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_instructions(instr_path, agent_instructions_content);
|
||||
}
|
||||
printf(" instructions: %s\n", instr_path);
|
||||
}
|
||||
|
||||
/* VS Code */
|
||||
if (agents.vscode) {
|
||||
printf("VS Code:\n");
|
||||
char config_path[1024];
|
||||
#ifdef __APPLE__
|
||||
snprintf(config_path, sizeof(config_path),
|
||||
"%s/Library/Application Support/Code/User/mcp.json", home);
|
||||
#else
|
||||
snprintf(config_path, sizeof(config_path), "%s/.config/Code/User/mcp.json", home);
|
||||
#endif
|
||||
if (!dry_run) {
|
||||
cbm_install_vscode_mcp(binary_path, config_path);
|
||||
}
|
||||
printf(" mcp: %s\n", config_path);
|
||||
}
|
||||
|
||||
/* OpenClaw */
|
||||
if (agents.openclaw) {
|
||||
printf("OpenClaw:\n");
|
||||
char config_path[1024];
|
||||
snprintf(config_path, sizeof(config_path), "%s/.openclaw/openclaw.json", home);
|
||||
if (!dry_run) {
|
||||
cbm_install_editor_mcp(binary_path, config_path);
|
||||
}
|
||||
printf(" mcp: %s\n", config_path);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Subcommand: install ──────────────────────────────────────── */
|
||||
|
||||
int cbm_cmd_install(int argc, char **argv) {
|
||||
@@ -2286,244 +2640,34 @@ int cbm_cmd_install(int argc, char **argv) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Step 1b: Kill running MCP server instances so agents pick up new config */
|
||||
if (!dry_run) {
|
||||
int killed = cbm_kill_other_instances();
|
||||
if (killed > 0) {
|
||||
printf("Stopped %d running MCP server instance(s).\n\n", killed);
|
||||
}
|
||||
}
|
||||
|
||||
/* Step 1c: macOS ad-hoc signing (in case binary was placed without signing) */
|
||||
#ifdef __APPLE__
|
||||
{
|
||||
char sign_path[1024];
|
||||
snprintf(sign_path, sizeof(sign_path), "%s/.local/bin/codebase-memory-mcp", home);
|
||||
struct stat sign_st;
|
||||
if (stat(sign_path, &sign_st) == 0) {
|
||||
(void)cbm_macos_adhoc_sign(sign_path);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Step 2: Binary path */
|
||||
char self_path[1024];
|
||||
snprintf(self_path, sizeof(self_path), "%s/.local/bin/codebase-memory-mcp", home);
|
||||
|
||||
/* Step 3: Detect agents */
|
||||
cbm_detected_agents_t agents = cbm_detect_agents(home);
|
||||
printf("Detected agents:");
|
||||
if (agents.claude_code) {
|
||||
printf(" Claude-Code");
|
||||
}
|
||||
if (agents.codex) {
|
||||
printf(" Codex");
|
||||
}
|
||||
if (agents.gemini) {
|
||||
printf(" Gemini-CLI");
|
||||
}
|
||||
if (agents.zed) {
|
||||
printf(" Zed");
|
||||
}
|
||||
if (agents.opencode) {
|
||||
printf(" OpenCode");
|
||||
}
|
||||
if (agents.antigravity) {
|
||||
printf(" Antigravity");
|
||||
}
|
||||
if (agents.aider) {
|
||||
printf(" Aider");
|
||||
}
|
||||
if (agents.kilocode) {
|
||||
printf(" KiloCode");
|
||||
}
|
||||
if (agents.vscode) {
|
||||
printf(" VS-Code");
|
||||
}
|
||||
if (agents.openclaw) {
|
||||
printf(" OpenClaw");
|
||||
}
|
||||
if (!agents.claude_code && !agents.codex && !agents.gemini && !agents.zed && !agents.opencode &&
|
||||
!agents.antigravity && !agents.aider && !agents.kilocode && !agents.vscode &&
|
||||
!agents.openclaw) {
|
||||
printf(" (none)");
|
||||
}
|
||||
printf("\n\n");
|
||||
/* Step 3: Install/refresh all agent configs */
|
||||
cbm_install_agent_configs(home, self_path, force, dry_run);
|
||||
|
||||
/* Step 4: Install Claude Code skills + hooks */
|
||||
if (agents.claude_code) {
|
||||
char skills_dir[1024];
|
||||
snprintf(skills_dir, sizeof(skills_dir), "%s/.claude/skills", home);
|
||||
printf("Claude Code:\n");
|
||||
|
||||
int skill_count = cbm_install_skills(skills_dir, force, dry_run);
|
||||
printf(" skills: %d installed\n", skill_count);
|
||||
|
||||
if (cbm_remove_old_monolithic_skill(skills_dir, dry_run)) {
|
||||
printf(" removed old monolithic skill\n");
|
||||
}
|
||||
|
||||
/* MCP config — write to both locations for compatibility.
|
||||
* Claude Code <=2.1.x reads ~/.claude/.mcp.json
|
||||
* Claude Code >=2.1.80 reads ~/.claude.json */
|
||||
char mcp_path[1024];
|
||||
snprintf(mcp_path, sizeof(mcp_path), "%s/.claude/.mcp.json", home);
|
||||
if (!dry_run) {
|
||||
cbm_install_editor_mcp(self_path, mcp_path);
|
||||
}
|
||||
printf(" mcp: %s\n", mcp_path);
|
||||
|
||||
char mcp_path2[1024];
|
||||
snprintf(mcp_path2, sizeof(mcp_path2), "%s/.claude.json", home);
|
||||
if (!dry_run) {
|
||||
cbm_install_editor_mcp(self_path, mcp_path2);
|
||||
}
|
||||
printf(" mcp: %s\n", mcp_path2);
|
||||
|
||||
/* PreToolUse hook + gate script */
|
||||
char settings_path[1024];
|
||||
snprintf(settings_path, sizeof(settings_path), "%s/.claude/settings.json", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_claude_hooks(settings_path);
|
||||
cbm_install_hook_gate_script(home);
|
||||
}
|
||||
printf(" hooks: PreToolUse (code discovery gate)\n");
|
||||
}
|
||||
|
||||
/* Step 5: Install Codex CLI */
|
||||
if (agents.codex) {
|
||||
printf("Codex CLI:\n");
|
||||
char config_path[1024];
|
||||
snprintf(config_path, sizeof(config_path), "%s/.codex/config.toml", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_codex_mcp(self_path, config_path);
|
||||
}
|
||||
printf(" mcp: %s\n", config_path);
|
||||
|
||||
char instr_path[1024];
|
||||
snprintf(instr_path, sizeof(instr_path), "%s/.codex/AGENTS.md", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_instructions(instr_path, agent_instructions_content);
|
||||
}
|
||||
printf(" instructions: %s\n", instr_path);
|
||||
}
|
||||
|
||||
/* Step 6: Install Gemini CLI */
|
||||
if (agents.gemini) {
|
||||
printf("Gemini CLI:\n");
|
||||
char config_path[1024];
|
||||
snprintf(config_path, sizeof(config_path), "%s/.gemini/settings.json", home);
|
||||
if (!dry_run) {
|
||||
cbm_install_editor_mcp(self_path, config_path);
|
||||
}
|
||||
printf(" mcp: %s\n", config_path);
|
||||
|
||||
char instr_path[1024];
|
||||
snprintf(instr_path, sizeof(instr_path), "%s/.gemini/GEMINI.md", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_instructions(instr_path, agent_instructions_content);
|
||||
}
|
||||
printf(" instructions: %s\n", instr_path);
|
||||
|
||||
/* BeforeTool hook (shared with Antigravity) */
|
||||
if (!dry_run) {
|
||||
cbm_upsert_gemini_hooks(config_path);
|
||||
}
|
||||
printf(" hooks: BeforeTool (grep/file search reminder)\n");
|
||||
}
|
||||
|
||||
/* Step 7: Install Zed */
|
||||
if (agents.zed) {
|
||||
printf("Zed:\n");
|
||||
char config_path[1024];
|
||||
#ifdef __APPLE__
|
||||
snprintf(config_path, sizeof(config_path),
|
||||
"%s/Library/Application Support/Zed/settings.json", home);
|
||||
#else
|
||||
snprintf(config_path, sizeof(config_path), "%s/.config/zed/settings.json", home);
|
||||
#endif
|
||||
if (!dry_run) {
|
||||
cbm_install_zed_mcp(self_path, config_path);
|
||||
}
|
||||
printf(" mcp: %s\n", config_path);
|
||||
}
|
||||
|
||||
/* Step 8: Install OpenCode */
|
||||
if (agents.opencode) {
|
||||
printf("OpenCode:\n");
|
||||
char config_path[1024];
|
||||
snprintf(config_path, sizeof(config_path), "%s/.config/opencode/opencode.json", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_opencode_mcp(self_path, config_path);
|
||||
}
|
||||
printf(" mcp: %s\n", config_path);
|
||||
|
||||
char instr_path[1024];
|
||||
snprintf(instr_path, sizeof(instr_path), "%s/.config/opencode/AGENTS.md", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_instructions(instr_path, agent_instructions_content);
|
||||
}
|
||||
printf(" instructions: %s\n", instr_path);
|
||||
}
|
||||
|
||||
/* Step 9: Install Antigravity */
|
||||
if (agents.antigravity) {
|
||||
printf("Antigravity:\n");
|
||||
char config_path[1024];
|
||||
snprintf(config_path, sizeof(config_path), "%s/.gemini/antigravity/mcp_config.json", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_antigravity_mcp(self_path, config_path);
|
||||
}
|
||||
printf(" mcp: %s\n", config_path);
|
||||
|
||||
char instr_path[1024];
|
||||
snprintf(instr_path, sizeof(instr_path), "%s/.gemini/antigravity/AGENTS.md", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_instructions(instr_path, agent_instructions_content);
|
||||
}
|
||||
printf(" instructions: %s\n", instr_path);
|
||||
}
|
||||
|
||||
/* Step 10: Install Aider */
|
||||
if (agents.aider) {
|
||||
printf("Aider:\n");
|
||||
char instr_path[1024];
|
||||
snprintf(instr_path, sizeof(instr_path), "%s/CONVENTIONS.md", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_instructions(instr_path, agent_instructions_content);
|
||||
}
|
||||
printf(" instructions: %s\n", instr_path);
|
||||
}
|
||||
|
||||
/* Step 11: Install KiloCode */
|
||||
if (agents.kilocode) {
|
||||
printf("KiloCode:\n");
|
||||
char config_path[1024];
|
||||
snprintf(config_path, sizeof(config_path),
|
||||
"%s/.config/Code/User/globalStorage/kilocode.kilo-code/settings/mcp_settings.json",
|
||||
home);
|
||||
if (!dry_run) {
|
||||
cbm_install_editor_mcp(self_path, config_path);
|
||||
}
|
||||
printf(" mcp: %s\n", config_path);
|
||||
|
||||
/* KiloCode uses ~/.kilocode/rules/ for global instructions */
|
||||
char instr_path[1024];
|
||||
snprintf(instr_path, sizeof(instr_path), "%s/.kilocode/rules/codebase-memory-mcp.md", home);
|
||||
if (!dry_run) {
|
||||
cbm_upsert_instructions(instr_path, agent_instructions_content);
|
||||
}
|
||||
printf(" instructions: %s\n", instr_path);
|
||||
}
|
||||
|
||||
/* Step 12: Install VS Code */
|
||||
if (agents.vscode) {
|
||||
printf("VS Code:\n");
|
||||
char config_path[1024];
|
||||
#ifdef __APPLE__
|
||||
snprintf(config_path, sizeof(config_path),
|
||||
"%s/Library/Application Support/Code/User/mcp.json", home);
|
||||
#else
|
||||
snprintf(config_path, sizeof(config_path), "%s/.config/Code/User/mcp.json", home);
|
||||
#endif
|
||||
if (!dry_run) {
|
||||
cbm_install_vscode_mcp(self_path, config_path);
|
||||
}
|
||||
printf(" mcp: %s\n", config_path);
|
||||
}
|
||||
|
||||
/* Step 13: Install OpenClaw */
|
||||
if (agents.openclaw) {
|
||||
printf("OpenClaw:\n");
|
||||
char config_path[1024];
|
||||
snprintf(config_path, sizeof(config_path), "%s/.openclaw/openclaw.json", home);
|
||||
if (!dry_run) {
|
||||
cbm_install_editor_mcp(self_path, config_path);
|
||||
}
|
||||
printf(" mcp: %s\n", config_path);
|
||||
}
|
||||
|
||||
/* Step 14: Ensure PATH */
|
||||
/* Step 4: Ensure PATH */
|
||||
char bin_dir[1024];
|
||||
snprintf(bin_dir, sizeof(bin_dir), "%s/.local/bin", home);
|
||||
const char *rc = cbm_detect_shell_rc(home);
|
||||
@@ -2979,21 +3123,42 @@ int cbm_cmd_update(int argc, char **argv) {
|
||||
}
|
||||
free(bin_data);
|
||||
} else {
|
||||
/* Zip extraction: exec unzip directly without shell interpretation */
|
||||
const char *unzip_argv[] = {"unzip", "-o", "-d", bin_dir, tmp_archive, NULL};
|
||||
rc = cbm_exec_no_shell(unzip_argv);
|
||||
cbm_unlink(tmp_archive);
|
||||
if (rc != 0) {
|
||||
fprintf(stderr, "error: extraction failed\n");
|
||||
/* Zip extraction: in-memory (no external unzip dependency) */
|
||||
FILE *f = fopen(tmp_archive, "rb");
|
||||
if (!f) {
|
||||
fprintf(stderr, "error: cannot open %s\n", tmp_archive);
|
||||
return 1;
|
||||
}
|
||||
/* Rename variant binary if needed */
|
||||
if (want_ui) {
|
||||
char ui_bin[1024];
|
||||
snprintf(ui_bin, sizeof(ui_bin), "%s/codebase-memory-mcp-ui.exe", bin_dir);
|
||||
snprintf(bin_dest, sizeof(bin_dest), "%s/codebase-memory-mcp.exe", bin_dir);
|
||||
rename(ui_bin, bin_dest);
|
||||
fseek(f, 0, SEEK_END);
|
||||
long fsize = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
|
||||
unsigned char *data = malloc((size_t)fsize);
|
||||
if (!data) {
|
||||
fclose(f);
|
||||
cbm_unlink(tmp_archive);
|
||||
return 1;
|
||||
}
|
||||
fread(data, 1, (size_t)fsize, f);
|
||||
fclose(f);
|
||||
|
||||
int bin_len = 0;
|
||||
unsigned char *bin_data = cbm_extract_binary_from_zip(data, (int)fsize, &bin_len);
|
||||
free(data);
|
||||
cbm_unlink(tmp_archive);
|
||||
|
||||
if (!bin_data || bin_len <= 0) {
|
||||
fprintf(stderr, "error: binary not found in zip archive\n");
|
||||
free(bin_data);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (cbm_replace_binary(bin_dest, bin_data, bin_len, 0755) != 0) {
|
||||
fprintf(stderr, "error: cannot write to %s\n", bin_dest);
|
||||
free(bin_data);
|
||||
return 1;
|
||||
}
|
||||
free(bin_data);
|
||||
}
|
||||
|
||||
/* Step 5b: macOS ad-hoc signing (required for arm64, harmless for x86_64) */
|
||||
@@ -3003,11 +3168,9 @@ int cbm_cmd_update(int argc, char **argv) {
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Step 6: Reinstall skills (force to pick up new content) */
|
||||
char skills_dir[1024];
|
||||
snprintf(skills_dir, sizeof(skills_dir), "%s/.claude/skills", home);
|
||||
int skill_count = cbm_install_skills(skills_dir, true, false);
|
||||
printf("Updated %d skill(s).\n", skill_count);
|
||||
/* Step 6: Refresh all agent configs (skills, MCP entries, hooks) */
|
||||
printf("Refreshing agent configurations...\n");
|
||||
cbm_install_agent_configs(home, bin_dest, true, false);
|
||||
|
||||
/* Step 7: Verify new version (exec directly, no shell interpretation) */
|
||||
printf("\nUpdate complete. Verifying:\n");
|
||||
|
||||
@@ -199,6 +199,11 @@ const char *cbm_get_codex_instructions(void);
|
||||
* Returns NULL on error. Caller must free. */
|
||||
unsigned char *cbm_extract_binary_from_targz(const unsigned char *data, int data_len, int *out_len);
|
||||
|
||||
/* Extract the codebase-memory-mcp binary from a zip archive in memory.
|
||||
* Returns malloc'd binary content and sets *out_len.
|
||||
* Returns NULL on error. Caller must free. */
|
||||
unsigned char *cbm_extract_binary_from_zip(const unsigned char *data, int data_len, int *out_len);
|
||||
|
||||
/* ── Index management ─────────────────────────────────────────── */
|
||||
|
||||
/* List .db files in the cache directory (~/.cache/codebase-memory-mcp/).
|
||||
|
||||
@@ -1023,6 +1023,124 @@ TEST(cli_extract_binary_from_targz_invalid_data) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
* Zip extraction tests
|
||||
* ═══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Build a minimal zip file with one stored (uncompressed) entry. */
|
||||
static unsigned char *create_test_zip_stored(const char *filename, const unsigned char *content,
|
||||
int content_len, int *out_len) {
|
||||
/* Local file header (30 bytes) + filename + content + central dir + EOCD */
|
||||
int name_len = (int)strlen(filename);
|
||||
int local_hdr_sz = 30 + name_len;
|
||||
int cd_hdr_sz = 46 + name_len;
|
||||
int eocd_sz = 22;
|
||||
int total = local_hdr_sz + content_len + cd_hdr_sz + eocd_sz;
|
||||
unsigned char *zip = calloc(1, (size_t)total);
|
||||
if (!zip) return NULL;
|
||||
int pos = 0;
|
||||
|
||||
/* Local file header */
|
||||
zip[pos] = 0x50; zip[pos+1] = 0x4B; zip[pos+2] = 0x03; zip[pos+3] = 0x04; /* signature */
|
||||
zip[pos+4] = 20; zip[pos+5] = 0; /* version needed = 2.0 */
|
||||
zip[pos+8] = 0; zip[pos+9] = 0; /* compression = stored */
|
||||
zip[pos+18] = (unsigned char)(content_len & 0xFF);
|
||||
zip[pos+19] = (unsigned char)((content_len >> 8) & 0xFF);
|
||||
zip[pos+20] = (unsigned char)((content_len >> 16) & 0xFF);
|
||||
zip[pos+21] = (unsigned char)((content_len >> 24) & 0xFF);
|
||||
zip[pos+22] = zip[pos+18]; zip[pos+23] = zip[pos+19];
|
||||
zip[pos+24] = zip[pos+20]; zip[pos+25] = zip[pos+21];
|
||||
zip[pos+26] = (unsigned char)(name_len & 0xFF);
|
||||
zip[pos+27] = (unsigned char)((name_len >> 8) & 0xFF);
|
||||
memcpy(zip + pos + 30, filename, (size_t)name_len);
|
||||
pos += 30 + name_len;
|
||||
memcpy(zip + pos, content, (size_t)content_len);
|
||||
pos += content_len;
|
||||
|
||||
int cd_start = pos;
|
||||
/* Central directory header */
|
||||
zip[pos] = 0x50; zip[pos+1] = 0x4B; zip[pos+2] = 0x01; zip[pos+3] = 0x02;
|
||||
zip[pos+10] = 0; zip[pos+11] = 0; /* compression = stored */
|
||||
zip[pos+20] = (unsigned char)(content_len & 0xFF);
|
||||
zip[pos+21] = (unsigned char)((content_len >> 8) & 0xFF);
|
||||
zip[pos+22] = (unsigned char)((content_len >> 16) & 0xFF);
|
||||
zip[pos+23] = (unsigned char)((content_len >> 24) & 0xFF);
|
||||
zip[pos+24] = zip[pos+20]; zip[pos+25] = zip[pos+21];
|
||||
zip[pos+26] = zip[pos+22]; zip[pos+27] = zip[pos+23];
|
||||
zip[pos+28] = (unsigned char)(name_len & 0xFF);
|
||||
zip[pos+29] = (unsigned char)((name_len >> 8) & 0xFF);
|
||||
pos += 46 + name_len;
|
||||
|
||||
/* EOCD */
|
||||
zip[pos] = 0x50; zip[pos+1] = 0x4B; zip[pos+2] = 0x05; zip[pos+3] = 0x06;
|
||||
zip[pos+8] = 1; /* num entries this disk */
|
||||
zip[pos+10] = 1; /* total entries */
|
||||
int cd_size = pos - cd_start;
|
||||
zip[pos+12] = (unsigned char)(cd_size & 0xFF);
|
||||
zip[pos+13] = (unsigned char)((cd_size >> 8) & 0xFF);
|
||||
zip[pos+16] = (unsigned char)(cd_start & 0xFF);
|
||||
zip[pos+17] = (unsigned char)((cd_start >> 8) & 0xFF);
|
||||
|
||||
*out_len = total;
|
||||
return zip;
|
||||
}
|
||||
|
||||
TEST(cli_extract_binary_from_zip) {
|
||||
const char *content = "#!/bin/sh\necho test\n";
|
||||
int zip_len = 0;
|
||||
unsigned char *zip =
|
||||
create_test_zip_stored("codebase-memory-mcp", (const unsigned char *)content,
|
||||
(int)strlen(content), &zip_len);
|
||||
ASSERT_NOT_NULL(zip);
|
||||
|
||||
int out_len = 0;
|
||||
unsigned char *extracted = cbm_extract_binary_from_zip(zip, zip_len, &out_len);
|
||||
ASSERT_NOT_NULL(extracted);
|
||||
ASSERT_EQ(out_len, (int)strlen(content));
|
||||
ASSERT_MEM_EQ(extracted, content, (size_t)out_len);
|
||||
free(extracted);
|
||||
free(zip);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(cli_extract_binary_from_zip_not_found) {
|
||||
const char *content = "data";
|
||||
int zip_len = 0;
|
||||
unsigned char *zip =
|
||||
create_test_zip_stored("other-file.txt", (const unsigned char *)content,
|
||||
(int)strlen(content), &zip_len);
|
||||
ASSERT_NOT_NULL(zip);
|
||||
|
||||
int out_len = 0;
|
||||
unsigned char *extracted = cbm_extract_binary_from_zip(zip, zip_len, &out_len);
|
||||
ASSERT_NULL(extracted);
|
||||
free(zip);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(cli_extract_binary_from_zip_path_traversal) {
|
||||
const char *content = "malicious";
|
||||
int zip_len = 0;
|
||||
unsigned char *zip =
|
||||
create_test_zip_stored("../../etc/codebase-memory-mcp", (const unsigned char *)content,
|
||||
(int)strlen(content), &zip_len);
|
||||
ASSERT_NOT_NULL(zip);
|
||||
|
||||
int out_len = 0;
|
||||
unsigned char *extracted = cbm_extract_binary_from_zip(zip, zip_len, &out_len);
|
||||
ASSERT_NULL(extracted);
|
||||
free(zip);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(cli_extract_binary_from_zip_invalid) {
|
||||
const unsigned char bad_data[] = "not a zip file";
|
||||
int out_len = 0;
|
||||
unsigned char *extracted = cbm_extract_binary_from_zip(bad_data, sizeof(bad_data), &out_len);
|
||||
ASSERT_NULL(extracted);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
* Skill dry-run tests
|
||||
* ═══════════════════════════════════════════════════════════════════ */
|
||||
@@ -2152,6 +2270,10 @@ SUITE(cli) {
|
||||
RUN_TEST(cli_extract_binary_from_targz);
|
||||
RUN_TEST(cli_extract_binary_from_targz_not_found);
|
||||
RUN_TEST(cli_extract_binary_from_targz_invalid_data);
|
||||
RUN_TEST(cli_extract_binary_from_zip);
|
||||
RUN_TEST(cli_extract_binary_from_zip_not_found);
|
||||
RUN_TEST(cli_extract_binary_from_zip_path_traversal);
|
||||
RUN_TEST(cli_extract_binary_from_zip_invalid);
|
||||
|
||||
/* Dry-run lifecycle (2 tests) */
|
||||
RUN_TEST(cli_install_dry_run);
|
||||
|
||||
Reference in new issue
Block a user