fix(cli-registry): toggling a CLI off in Settings never hid it anywhere else

window.__codemanCliAvailable — the flag isCliAvailable() reads client-side
to gate the welcome-screen buttons, the Run-menu dropdown and the mobile
overview — was built purely from each CLI's own installed-on-PATH resolver
(isClaudeAvailable() etc.), with no reference to the registry's `enabled`
flag at all. So disabling a CLI via the new Settings UI (or a hand-edited
clis.json) updated the settings row and nothing else: every launch surface
kept offering it, both live and after a full page reload, since even a
fresh render never consulted the registry.

Fixed in two places:

- server.ts: after building `available`, intersect the nine real
  SessionMode ids against `enabledClis()`. git/cloudflared (utility
  binaries, not CLI registry entries) and deepseekBinary (a secondary
  installed-only flag for the "add a profile" affordance) are deliberately
  left alone.
- settings-ui.js: `toggleCliEnabled()` now patches
  `window.__codemanCliAvailable` in place and refreshes the welcome screen,
  the mobile overview and an already-open Run menu, mirroring the existing
  `installDeepSeekProfile()` pattern for the same "injected once, needs an
  explicit patch" reason — without this half, the server-side fix alone
  still left every surface stale until the next reload.

New test in test/render-index-html.test.ts: an installed-but-disabled CLI
(codex, forced via clis.json + reloadCliRegistry()) reads as unavailable,
while an installed-and-enabled one (claude) is unaffected by the override.

Verified on the Debian devbox (codeman-devbox, real tmux — this sandbox has
none and WebServer's constructor hard-requires it): typecheck clean, the
new test passes (17/17 in render-index-html.test.ts), the CLI-registry
suites pass (86/86), and the full CI gate is green (415 test files, 7855
tests, 0 failures).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N6eadpRyqpA9PD3i139cSD
This commit is contained in:
Devvyn
2026-09-22 07:13:57 +08:00
co-authored by Claude Sonnet 5
parent db4557d94b
commit 0c77dd0a18
3 changed files with 64 additions and 1 deletions
+29 -1
View File
@@ -23,8 +23,11 @@ import { isDeepSeekAvailable, isDeepSeekRunnable } from '../src/utils/deepseek-c
import { isOmpAvailable } from '../src/utils/omp-cli-resolver.js';
import { isCloudflaredAvailable } from '../src/utils/cloudflared-resolver.js';
import { isGitAvailable } from '../src/git-clone.js';
import { enabledClis } from '../src/config/cli-registry/registry.js';
import { enabledClis, reloadCliRegistry } from '../src/config/cli-registry/registry.js';
import { STOCK_CLIS } from '../src/config/cli-registry/stock.js';
import { dataPath } from '../src/config/instance.js';
import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname } from 'node:path';
// renderIndexHtml probes the real PATH for every CLI, which would make the
// assertions below depend on whatever happens to be installed on the machine
@@ -197,6 +200,31 @@ describe('WebServer.renderIndexHtml', () => {
});
});
it('reads as unavailable for a CLI disabled via the registry, even though it is installed', async () => {
// The bug this guards: a CLI toggled off in Settings (docs/cli-enable-disable-plan.md)
// still offered itself in the welcome screen / Run menu / mobile overview, because
// window.__codemanCliAvailable was built purely from each resolver's own PATH probe —
// it never consulted the registry's `enabled` flag at all. Installed AND enabled must
// both hold for `isCliAvailable()` (the client-side gate every one of those surfaces
// reads) to read true.
vi.mocked(isCodexAvailable).mockReturnValue(true);
vi.mocked(isClaudeAvailable).mockReturnValue(true);
const path = dataPath('clis.json');
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, JSON.stringify({ clis: { codex: { enabled: false } } }, null, 2), { mode: 0o600 });
reloadCliRegistry();
try {
const { server } = makeServer({});
const html = await render(server);
const flags = JSON.parse(html.match(/window\.__codemanCliAvailable=(\{.*?\});/)![1]);
expect(flags.codex).toBe(false); // installed, but disabled in the registry
expect(flags.claude).toBe(true); // installed and enabled — unaffected by codex's override
} finally {
writeFileSync(path, JSON.stringify({ clis: {} }, null, 2), { mode: 0o600 });
reloadCliRegistry();
}
});
it('reports which run modes the custom-model Run-menu picker may generate an entry for', async () => {
// Read generically off the CLI registry's own capabilities, not a hardcoded id
// list — antigravity (`unsupported`) and shell (`kind !== 'agent'`) must be