mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
feat(statusline): delegate to the statusline the exporter shadows
Claude Code ranks a repo's .claude/settings.local.json above ~/.claude/settings.json, so the statusLine Codeman injects for the Plan Usage chip shadows whatever statusline the user configured globally. The inline exporter then printed Codeman's own footer in its place, and running `claude` by hand in a managed repo rendered the bare word `codeman` — the response the server returns for an unknown session id. The exporter becomes a generated shim, following the deepseek-status-shim pattern: a versioned .mjs in the data dir, refreshed on a marker change, written through temp-and-rename so a live render cannot read a half-written file. It forwards the same payload to /api/status-telemetry and, concurrently, resolves the statusline it shadows and prints that. Codeman's footer still appears when there is nothing to shadow, so the exporter keeps its value on a machine with no statusline of its own. The delegate resolves at render time, walking the settings files Claude Code consults from the render directory upward and then the home ones, skipping Codeman's own entry in either form. Late resolution means editing a global statusline needs no reinjection. Ownership now keys on the version-free `codeman-statusline-shim` token, and applyStatusLineConfig still reads the old /api/status-telemetry command as ours, so managed repos upgrade in place instead of being mistaken for hand-authored. A hand-authored statusLine is left alone exactly as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit 641795821fdbb171f9f47c4909c6e8945465d05b)
This commit is contained in:
committed by
Codeman maintainer
parent
c4b74415ee
commit
c375268879
@@ -23,6 +23,7 @@ import {
|
||||
updateCaseModel,
|
||||
writeHooksConfig,
|
||||
} from '../src/hooks-config.js';
|
||||
import { LEGACY_STATUSLINE_MARKER, STATUSLINE_SHIM_TOKEN } from '../src/statusline-shim.js';
|
||||
|
||||
describe('generateHooksConfig', () => {
|
||||
it('should return an object with hooks key', () => {
|
||||
@@ -1305,3 +1306,81 @@ describe('Hook Config Generation - Extended', () => {
|
||||
expect(stopHooks[0].hooks[0].command).toContain('stop');
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyStatusLineConfig', () => {
|
||||
const testDir = join(tmpdir(), 'codeman-statusline-config-' + Date.now());
|
||||
const settingsFile = join(testDir, '.claude', 'settings.local.json');
|
||||
|
||||
const read = () => JSON.parse(readFileSync(settingsFile, 'utf-8'));
|
||||
const write = (value: object) => {
|
||||
mkdirSync(join(testDir, '.claude'), { recursive: true });
|
||||
writeFileSync(settingsFile, JSON.stringify(value, null, 2));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('injects the delegating shim rather than an inline exporter', async () => {
|
||||
await applyStatusLineConfig(testDir, true);
|
||||
const { statusLine } = read();
|
||||
expect(statusLine.type).toBe('command');
|
||||
expect(statusLine.command).toContain(STATUSLINE_SHIM_TOKEN);
|
||||
// The inline form SHADOWS the user's statusline, which is the whole reason
|
||||
// the shim exists. It may never be the command we inject by choice.
|
||||
expect(statusLine.command).not.toContain(LEGACY_STATUSLINE_MARKER);
|
||||
});
|
||||
|
||||
it('upgrades a pre-shim inline exporter in place', async () => {
|
||||
// Every repo a previous Codeman managed still holds this command. If the
|
||||
// ownership check missed it, the upgrade would read it as hand-authored,
|
||||
// refuse to touch it, and leave the user shadowed forever.
|
||||
write({
|
||||
statusLine: { type: 'command', command: `curl -X POST "$CODEMAN_API_URL${LEGACY_STATUSLINE_MARKER}"` },
|
||||
permissions: { allow: ['Read'] },
|
||||
});
|
||||
|
||||
await applyStatusLineConfig(testDir, true);
|
||||
|
||||
const settings = read();
|
||||
expect(settings.statusLine.command).toContain(STATUSLINE_SHIM_TOKEN);
|
||||
expect(settings.permissions).toEqual({ allow: ['Read'] });
|
||||
});
|
||||
|
||||
it('removes a pre-shim inline exporter on the disable path', async () => {
|
||||
write({ statusLine: { type: 'command', command: `curl "$CODEMAN_API_URL${LEGACY_STATUSLINE_MARKER}"` } });
|
||||
await applyStatusLineConfig(testDir, false);
|
||||
expect(read().statusLine).toBeUndefined();
|
||||
});
|
||||
|
||||
it('removes its own shim entry on the disable path', async () => {
|
||||
await applyStatusLineConfig(testDir, true);
|
||||
await applyStatusLineConfig(testDir, false);
|
||||
expect(read().statusLine).toBeUndefined();
|
||||
});
|
||||
|
||||
it('never touches a statusLine the user wrote themselves', async () => {
|
||||
// Unchanged contract: a hand-authored entry in the repo's own file stops
|
||||
// Codeman cold, so it never owns an entry it would have to restore later.
|
||||
const mine = { type: 'command', command: 'bash ~/.claude/my-statusline.sh' };
|
||||
write({ statusLine: mine });
|
||||
|
||||
await applyStatusLineConfig(testDir, true);
|
||||
expect(read().statusLine).toEqual(mine);
|
||||
|
||||
await applyStatusLineConfig(testDir, false);
|
||||
expect(read().statusLine).toEqual(mine);
|
||||
});
|
||||
|
||||
it('rewrites nothing when the shim command is already current', async () => {
|
||||
await applyStatusLineConfig(testDir, true);
|
||||
const before = readFileSync(settingsFile, 'utf-8');
|
||||
await applyStatusLineConfig(testDir, true);
|
||||
expect(readFileSync(settingsFile, 'utf-8')).toBe(before);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user