Skip to content

Commit 958a511

Browse files
committed
docs+cli: finish the install-script update contract
The update handoff moved out of the product on every platform, but two loose ends were left behind. README was actively WRONG, not merely incomplete. It still documented in-place updates for macOS/Linux, and it promised "The MCP server also checks for updates on startup and notifies on the first tool call" -- a claim that died with the MCP update thread and then the daemon's curl check. The section now states the one contract that holds everywhere (`update` validates flags and prints the install script command), why it works that way (on Windows a running image cannot replace itself; on POSIX a deliberate choice, because an in-process updater is structurally a downloader), and the affirmative property that replaced it: cbm makes no network request of its own accord and the archives carry no download URLs at all. `uninstall` now REPORTS the install script it finds beside the binary, printing the path and the `rm` command, and says "would leave" under --dry-run. It does not delete it. install.sh places itself there so `update` has something to hand off to, but that file may equally be the user's own copy, a symlink into a checkout, or package-manager managed -- an uninstaller must not delete a file it cannot prove it owns, and one printed line leaves the decision where it belongs. Windows checks install.ps1 too and tolerates either path separator. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1 parent 1f674c8 commit 958a511

2 files changed

Lines changed: 62 additions & 7 deletions

File tree

‎README.md‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,23 +155,25 @@ Watcher registration is controlled separately by `auto_watch` (default `true`).
155155

156156
### Keeping Up to Date
157157

158-
**macOS / Linux:**
158+
**Updates run from the install script on every platform, not from inside the running binary.** `codebase-memory-mcp update` validates your flags and then prints the exact command to run:
159159

160160
```bash
161-
codebase-memory-mcp update
161+
# macOS / Linux
162+
bash "<install-dir>/install.sh"
162163
```
163164

164-
**Windows** — updates run from `install.ps1`, not from the running binary, because a running executable cannot replace its own image on Windows. `codebase-memory-mcp update` prints the exact command; it is:
165-
166165
```powershell
166+
# Windows
167167
powershell -ExecutionPolicy Bypass -File "<install-dir>\install.ps1"
168168
```
169169

170-
`install.ps1` is idempotent, so re-running it *is* the update: it stops the daemon, retires the running binary, installs the new one, and cleans up. `install.ps1` sits next to the binary in your install directory. If PowerShell refuses to run it because the file came from the internet, `Unblock-File` it first.
170+
The install script is placed next to the binary at install time, so the printed path resolves beside the executable. It is idempotent, so re-running it *is* the update: it stops the daemon, retires the running binary, installs the new one, and cleans up.
171171

172-
Installed through **npm or pip**? Update with your package manager on every platform (`npm install -g codebase-memory-mcp@latest` / `pip install -U codebase-memory-mcp`).
172+
Why it works this way. On Windows it is a hard requirement — a running executable cannot replace its own image, so the swap has to happen from a process that is not the binary being replaced. On macOS and Linux it is a deliberate choice: an in-process updater is structurally a downloader (fetch an archive, verify it, unpack it, mark a file executable, run it), and shipping that composite in every binary to serve a command most people run a handful of times is a poor trade. The release archives now carry no download URLs at all, and **cbm makes no network request of its own accord** — it does not check for new versions in the background, and nothing phones home. You find out about releases from the install script, your package manager, or GitHub.
173173

174-
The MCP server also checks for updates on startup and notifies on the first tool call if a newer release is available.
174+
If PowerShell refuses to run the script because the file came from the internet, `Unblock-File` it first.
175+
176+
Installed through **npm or pip**? Update with your package manager on every platform (`npm install -g codebase-memory-mcp@latest` / `pip install -U codebase-memory-mcp`).
175177

176178
### Uninstall
177179

@@ -181,6 +183,8 @@ codebase-memory-mcp uninstall
181183

182184
Removes owned agent config entries, skills, hooks, instructions, and the installed binary. Existing graph indexes are listed and deleted only after confirmation.
183185

186+
The install script placed beside the binary is **reported, not deleted** — uninstall prints its path and the `rm` command for it. It is left alone on purpose: it may be your own copy, a symlink into a checkout, or managed by a package manager, and an uninstaller should not delete a file it cannot prove it owns.
187+
184188
## Features
185189

186190
### Graph & analysis

‎src/cli/cli.c‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10596,6 +10596,56 @@ typedef struct {
1059610596
bool dry_run;
1059710597
} cli_uninstall_activation_t;
1059810598

10599+
/* Report — never delete — the updater script sitting next to the binary.
10600+
*
10601+
* install.sh/install.ps1 copies itself beside the executable so `update` has
10602+
* something to hand off to. Uninstall does NOT remove it, deliberately: we
10603+
* cannot prove we own that file. The user may have put their own copy there, it
10604+
* may be a symlink into a checkout, or a package manager may manage it. Deleting
10605+
* a file we merely expect to find is how an uninstaller eats something it
10606+
* shouldn't. Telling the user the exact path costs one line and leaves the
10607+
* decision with them. */
10608+
static void cli_uninstall_report_leftover_installer(const char *bin_path, bool dry_run) {
10609+
if (!bin_path || !bin_path[0]) {
10610+
return;
10611+
}
10612+
const char *slash = strrchr(bin_path, '/');
10613+
#ifdef _WIN32
10614+
const char *backslash = strrchr(bin_path, '\\');
10615+
if (backslash && (!slash || backslash > slash)) {
10616+
slash = backslash;
10617+
}
10618+
static const char *const installer_names[] = {"install.ps1", "install.sh"};
10619+
#else
10620+
static const char *const installer_names[] = {"install.sh"};
10621+
#endif
10622+
if (!slash || slash == bin_path) {
10623+
return;
10624+
}
10625+
size_t dir_len = (size_t)(slash - bin_path);
10626+
for (size_t i = 0; i < sizeof(installer_names) / sizeof(installer_names[0]); i++) {
10627+
char installer_path[CLI_BUF_1K];
10628+
int written = snprintf(installer_path, sizeof(installer_path), "%.*s/%s", (int)dir_len,
10629+
bin_path, installer_names[i]);
10630+
if (written <= 0 || (size_t)written >= sizeof(installer_path)) {
10631+
continue;
10632+
}
10633+
struct stat installer_status;
10634+
if (stat(installer_path, &installer_status) != 0) {
10635+
continue;
10636+
}
10637+
if (dry_run) {
10638+
printf("Would leave %s in place (remove it yourself if you want it gone).\n",
10639+
installer_path);
10640+
} else {
10641+
printf("\nLeft in place: %s\n"
10642+
" This is the updater; uninstall does not delete it because it cannot\n"
10643+
" prove it owns it. To remove it as well:\n\n rm \"%s\"\n",
10644+
installer_path, installer_path);
10645+
}
10646+
}
10647+
}
10648+
1059910649
/* Uninstall is an activation too: removing the executable or its indexes
1060010650
* while a daemon generation is starting/running would leave live sessions on
1060110651
* a partially removed installation. Keep every filesystem mutation inside
@@ -10650,6 +10700,7 @@ static int cli_uninstall_activate(void *opaque) {
1065010700
if (activation->binary_exists) {
1065110701
printf("Removed %s\n", activation->bin_path);
1065210702
}
10703+
cli_uninstall_report_leftover_installer(activation->bin_path, activation->dry_run);
1065310704
return CLI_OK;
1065410705
}
1065510706

0 commit comments

Comments
 (0)