Skip to content

fix(installer): upgrade over any earlier install without --force - #31

Merged
KageBinary merged 2 commits into
mainfrom
fix/reinstall
Oct 2, 2026
Merged

KageBinary merged 2 commits into
mainfrom
fix/reinstall

Conversation

@KageBinary

Copy link
Copy Markdown
Collaborator

Why

Re-running the installer over an earlier version crashed. Reproduced by installing 2.4.2 into a temp HOME, then running main's installer:

FileExistsError: .../home/.codex/plugins/ix-memory/.codex-plugin/plugin.json already exists. Re-run with --force.

None of the hosted one-line installers pass --force, so upgrading meant a traceback with some files already written.

Related problems:

  • ix mcp install --host codex ran without --force, with no timeout, and its exit code was ignored. So a pre-feat(mcp): serve tools from the Ix CLI instead of shipping a server #24 ix-memory registration pointing at the deleted mcp/server.py was never replaced.
  • Files an older version shipped, but the new one doesn't, were left behind.
  • The old global /tmp/ix-codex-hooks cache was never cleaned up.
  • Windows: Codex 0.155.1 runs hooks with powershell.exe -NoProfile -Command, where the hook command "python" "launcher" name is a parse error.

What changes

  • No half-done installs. Everything is read and checked first, new files are written beside their destinations, then swapped in together. If any swap fails, the old files are put back.
  • Owned files:
    • Plugin files are overwritten, and files no longer shipped are removed. They are tracked by a new files list in ix-plugin-version.json; installs without the list are matched against the names earlier versions shipped.
    • The plugin folder is replaced whole.
    • User files are never touched.
  • hooks.json is merged, not copied. Old plugin entries (-lc, Bash-only matchers, the 2.4.x Windows form) are replaced in place, other tools' hooks keep their positions, and nothing is duplicated. The hook-trust notice prints only when hooks.json actually changed.
  • Unchanged reinstall writes nothing.
  • Marketplace entry: replaced when it points at the plugin's own path; any other ix-memory entry still needs --force.
  • MCP registration:
    • Runs ix mcp install --host codex --format json with a 120s timeout. The exit code and the per-host outcome are reported; the real CLI exits 0 on a conflict, so the JSON outcome is what catches it.
    • A stale mcp/server.py registration gets --force (available since ix 0.9.3, the minimum). If --force is ever rejected, the [mcp_servers.ix-memory] table is rewritten in a checked copy of config.toml.
    • Any other server named ix-memory is left alone unless --force is passed.
    • The old .codex/mcp/*.py files are deleted only once the config no longer names mcp/server.py.
    • A failure or timeout here is a warning, and the installer still exits 0. The plugin and hooks did install, and a non-zero exit would make the hosted one-liners report the whole install as failed.
  • Old cache: $TMPDIR/ix-codex-hooks is removed only if it belongs to the user, is not a symlink, and holds only the old cache files.
  • codex_hooks: config.toml is left alone; a leftover codex_hooks flag gets a hint that it can be deleted.
  • Windows: each hook gets a commandWindows of & '<python>' '<launcher>' <name>, and command keeps the POSIX form.
  • Version 2.4.4.

Tests

  • unittest: 112 → 135 (1 Windows-only skip). The new tests/test_installer_upgrade.py (23 tests) covers:

    • a fresh install, and a reinstall at the same version;
    • an upgrade from a synthesized pre-feat(mcp): serve tools from the Ix CLI instead of shipping a server #24 install (old mcp/server.py registration, codex_hooks flag, -lc hooks, a dropped hook file);
    • an upgrade from 2.4.2/2.4.3;
    • a user's own hooks kept;
    • ix missing, and ix mcp install failing or timing out;
    • an install interrupted partway.

    20 of them fail on main for the intended reason; the other 3 are guards that pass there by design.

  • test-local.sh, the header check and shellcheck pass.

  • Against the real CLI, only ix mcp install --dry-run was run, against a temp CODEX_HOME.

Not verified

  • That PowerShell passes the hook's stdin through to Python.
  • The rare fallback where Codex has no session shell and uses cmd, where & is invalid.
  • The Windows CI leg runs on this PR.

🤖 Generated with Claude Code

KageBinary and others added 2 commits October 1, 2026 18:39
Re-running the installer after any change raised FileExistsError, after
part of the install had been written, and neither hosted one-liner passes
--force. Even with --force, `ix mcp install` ran without it, unbounded and
unchecked, so a 2.4.1 `ix-memory` entry pointing at the deleted
mcp/server.py was never replaced.

The installer now plans every change, checks it, stages the new files
beside their destinations and swaps them in together, restoring the
previous files if any swap fails:

- files the plugin owns are overwritten, and ones it no longer ships are
  removed; ownership is recorded in ix-plugin-version.json (`files`), with
  the historical file list for installs that predate it. The plugin tree
  is replaced whole, so dropped skills go too.
- hooks.json is merged: plugin handlers from any version (`-lc`, `-c`,
  `Bash` matchers, the 2.4.x Windows launcher form) are replaced where the
  first one was, and every other hook is kept in place.
- the marketplace entry is replaced when it points at the plugin's own
  path; another ix-memory entry still needs --force.
- `ix mcp install --host codex --format json` runs with a timeout; its exit
  status and per-host outcome are reported as warnings, never a crash. A
  registration of the old mcp/server.py is replaced with --force (or, for
  a CLI without it, by rewriting that one table in a checked copy of
  config.toml), and the old server files are removed once nothing
  launches them.
- the pre-2.4.2 /tmp/ix-codex-hooks cache is removed when it is the
  user's own and holds only cache files.
- the hook-trust notice is printed only when hooks.json changed.

Windows: Codex 0.155 runs hook commands through the session shell, which
on Windows is `powershell.exe -NoProfile -Command`. The 2.4.x command
`"python" "launcher" name` is a parse error there. Each hook now gets a
`commandWindows` of `& '<python>' '<launcher>' <name>`, and `command`
keeps the POSIX form.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@KageBinary
KageBinary merged commit 1190fbd into main Oct 2, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant