diff --git a/.changeset/cli9a1d.md b/.changeset/cli9a1d.md new file mode 100644 index 00000000..0ecbf8df --- /dev/null +++ b/.changeset/cli9a1d.md @@ -0,0 +1,5 @@ +--- +"aicodeman": patch +--- + +Docker Compose: CLIs installed from Settings (DeepSeek, Pi and any other npm-based CLI) survive `Update-Codeman.sh`. The image's `NPM_CONFIG_PREFIX` (`/opt/codeman-cli`) is image content and was discarded when the container was recreated; `POST /api/clis/:id/install` now installs into `~/.local` on the persistent home mount when running in the container, and `~/.local/bin` is on the image PATH. diff --git a/docker/server.Dockerfile b/docker/server.Dockerfile index 99142abc..305fc392 100644 --- a/docker/server.Dockerfile +++ b/docker/server.Dockerfile @@ -219,6 +219,9 @@ RUN set -eux; \ COPY --from=ghcr.io/astral-sh/uv:0.9 /uv /uvx /usr/local/bin/ ENV NPM_CONFIG_PREFIX=/opt/codeman-cli ENV PATH=$PATH:/opt/codeman-cli/bin +# CLIs installed at runtime (Settings -> CLIs, npm redirected to ~/.local by installEnv()) live on the +# persistent home mount, so they survive a container recreate. Appended for the same reason as above. +ENV PATH=$PATH:/home/${CODEMAN_RUNTIME_USER}/.local/bin # pnpm is not an agent CLI: it is here because `dsh plugin` (DeepSeek Harness, which # this image leaves to be installed at runtime, see SERVER_INTENTIONAL_OMISSIONS in # test/docker-agent-image-coverage.test.ts) spawns a literal `pnpm` with no npm diff --git a/src/web/routes/cli-registry-routes.ts b/src/web/routes/cli-registry-routes.ts index 5b79b428..62a54e4f 100644 --- a/src/web/routes/cli-registry-routes.ts +++ b/src/web/routes/cli-registry-routes.ts @@ -217,6 +217,13 @@ export function installEnv(source: NodeJS.ProcessEnv = process.env): NodeJS.Proc for (const [key, value] of Object.entries(source)) { if (!key.startsWith('CODEMAN_')) env[key] = value; } + // ⚠️ In the Docker Compose deployment the image sets NPM_CONFIG_PREFIX=/opt/codeman-cli, which is IMAGE + // content: `Update-Codeman.sh` recreates the container and every CLI installed there (dsh, pi, ...) + // vanishes. HOME is the persistent bind mount and `~/.local/bin` is already on every resolver's search + // list, so npm-based installs are redirected there. curl|bash installers already target HOME. + if (source.CODEMAN_IN_CONTAINER === '1' && source.HOME) { + env.NPM_CONFIG_PREFIX = `${source.HOME}/.local`; + } return env; } diff --git a/test/routes/cli-registry-routes.test.ts b/test/routes/cli-registry-routes.test.ts index 34d7cda0..bc33b1f8 100644 --- a/test/routes/cli-registry-routes.test.ts +++ b/test/routes/cli-registry-routes.test.ts @@ -713,6 +713,12 @@ describe('registry writes are serialized and never clobber a file the reader wou } expect(installEnv({ CODEMAN_PASSWORD: 'x', HOME: '/h' })).toEqual({ HOME: '/h' }); }); + + it('redirects npm installs to the persistent HOME inside the Compose container', () => { + expect( + installEnv({ CODEMAN_IN_CONTAINER: '1', HOME: '/home/codeman', NPM_CONFIG_PREFIX: '/opt/codeman-cli' }) + ).toEqual({ HOME: '/home/codeman', NPM_CONFIG_PREFIX: '/home/codeman/.local' }); + }); }); /**