mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 15:09:42 +02:00
feat(statusline): wrap the user's own real statusline instead of skipping it
Now that the exporter no longer lives in a fixed per-case file, it can compose with the user's actual configured statusline rather than just backing off when one is found. findEffectiveUserStatusLineCommand() walks Claude Code's own settings precedence for a workspace: project-local .claude/settings.local.json > project-shared .claude/settings.json > the user's global ~/.claude/settings.json. A legacy Codeman-marked entry left behind in the project's own settings.local.json is never treated as a real user command — it's skipped and precedence continues to the next layer. The shared exporter script (bumped to a V2 marker so stale copies self-heal) now fires the telemetry POST in a background subshell — its own stdout/stderr discarded so nothing leaks into the visible statusline, and confirmed non-blocking (~4ms, even against an unreachable endpoint) — then, if the pane's environment carries CODEMAN_USER_STATUSLINE_CMD, feeds it the same stdin blob and relays its stdout as ours. Otherwise it falls back to the plain "codeman" marker as before. The discovered command is threaded to the pane via `tmux setenv CODEMAN_USER_STATUSLINE_CMD` (_configureStatusLineUserCommand) rather than embedded in the spawn command line, for the same premature-shell-expansion reason as the parent commit: tmux stores a setenv value verbatim and never re-parses it, so once shellescape()d for that one command, the command's own $/quotes survive untouched into the pane's environment. Verified live via direct shell execution of the generated script (both branches: fallback and user-command wrapping) before deploy. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GyMnFWnUzc41TDeHg9juW
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d4aa3c8cca
commit
e15e8e43e8
@@ -18,11 +18,12 @@ import {
|
||||
statSync,
|
||||
} from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { tmpdir, homedir } from 'node:os';
|
||||
import { spawn } from 'node:child_process';
|
||||
import {
|
||||
applyStatusLineConfig,
|
||||
ensureCodemanHooks,
|
||||
findEffectiveUserStatusLineCommand,
|
||||
generateBackgroundWakeScript,
|
||||
generateHooksConfig,
|
||||
generateStatusLineCommand,
|
||||
@@ -1387,3 +1388,75 @@ describe('resolveStatusLineCliCommand', () => {
|
||||
expect(JSON.parse(readFileSync(settingsPath, 'utf-8')).statusLine).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findEffectiveUserStatusLineCommand', () => {
|
||||
const testDir = join(tmpdir(), 'codeman-statusline-precedence-test-' + Date.now());
|
||||
const userSettingsPath = join(homedir(), '.claude', 'settings.json');
|
||||
|
||||
beforeEach(() => {
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
rmSync(userSettingsPath, { force: true }); // don't leak into other tests sharing this HOME
|
||||
});
|
||||
|
||||
it('returns undefined when nothing is configured anywhere', async () => {
|
||||
expect(await findEffectiveUserStatusLineCommand(testDir)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('finds the user global ~/.claude/settings.json when nothing else is set', async () => {
|
||||
const userClaudeDir = join(homedir(), '.claude');
|
||||
mkdirSync(userClaudeDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(userClaudeDir, 'settings.json'),
|
||||
JSON.stringify({ statusLine: { type: 'command', command: 'echo user-global' } })
|
||||
);
|
||||
|
||||
expect(await findEffectiveUserStatusLineCommand(testDir)).toBe('echo user-global');
|
||||
});
|
||||
|
||||
it('project-SHARED settings.json wins over user-global', async () => {
|
||||
const userClaudeDir = join(homedir(), '.claude');
|
||||
mkdirSync(userClaudeDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(userClaudeDir, 'settings.json'),
|
||||
JSON.stringify({ statusLine: { type: 'command', command: 'echo user-global' } })
|
||||
);
|
||||
const projectClaudeDir = join(testDir, '.claude');
|
||||
mkdirSync(projectClaudeDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(projectClaudeDir, 'settings.json'),
|
||||
JSON.stringify({ statusLine: { type: 'command', command: 'echo project-shared' } })
|
||||
);
|
||||
|
||||
expect(await findEffectiveUserStatusLineCommand(testDir)).toBe('echo project-shared');
|
||||
});
|
||||
|
||||
it('project-LOCAL settings.local.json wins over everything', async () => {
|
||||
const projectClaudeDir = join(testDir, '.claude');
|
||||
mkdirSync(projectClaudeDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(projectClaudeDir, 'settings.json'),
|
||||
JSON.stringify({ statusLine: { type: 'command', command: 'echo project-shared' } })
|
||||
);
|
||||
writeFileSync(
|
||||
join(projectClaudeDir, 'settings.local.json'),
|
||||
JSON.stringify({ statusLine: { type: 'command', command: 'echo project-local' } })
|
||||
);
|
||||
|
||||
expect(await findEffectiveUserStatusLineCommand(testDir)).toBe('echo project-local');
|
||||
});
|
||||
|
||||
it('skips a legacy Codeman-marked entry in project settings.local.json and falls through', async () => {
|
||||
await applyStatusLineConfig(testDir, true); // simulates a pre-fix disk-written exporter
|
||||
const projectClaudeDir = join(testDir, '.claude');
|
||||
writeFileSync(
|
||||
join(projectClaudeDir, 'settings.json'),
|
||||
JSON.stringify({ statusLine: { type: 'command', command: 'echo project-shared' } })
|
||||
);
|
||||
|
||||
expect(await findEffectiveUserStatusLineCommand(testDir)).toBe('echo project-shared');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user