mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-11 01:39:41 +02:00
refactor(cli): one credential reader for every client of the API
`readCodemanEnv()` in cli.ts and `parseEnvFile()`/`readCodemanCredentials()` in tui-client.ts were two copies of the same `.env` reader (the TUI's said so in a comment), and `codeman agent` was about to need a third. They move into `src/codeman-credentials.ts`; `codeman attach` and the TUI read from it, and the TUI keeps re-exporting its names so nothing that imports them changes. The lookup order is one pure function, `credentialsFrom(env, fileEnv)`: each field from the environment, then the file, username defaulting to `admin` — the order attach and the TUI already used. `readCodemanCredentials` takes the environment as a parameter so a caller with its own (the agent CLI's guard) gets the same answer. The parser now also tolerates an `export ` prefix, which the agent skill's preamble already accepted in the same file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3a0cee6b90
commit
6d862d5323
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @fileoverview The one credential reader `codeman attach`, `codeman tui` and
|
||||
* `codeman agent` share: env first, the data dir's `.env` as the fallback.
|
||||
*/
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { basicAuthHeader, readCodemanCredentials, readCodemanEnvFile } from '../src/codeman-credentials.js';
|
||||
|
||||
describe('readCodemanCredentials', () => {
|
||||
let dir: string;
|
||||
const saved = { user: process.env.CODEMAN_USERNAME, pass: process.env.CODEMAN_PASSWORD };
|
||||
|
||||
beforeEach(() => {
|
||||
dir = mkdtempSync(join(tmpdir(), 'codeman-cred-'));
|
||||
delete process.env.CODEMAN_USERNAME;
|
||||
delete process.env.CODEMAN_PASSWORD;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
if (saved.user === undefined) delete process.env.CODEMAN_USERNAME;
|
||||
else process.env.CODEMAN_USERNAME = saved.user;
|
||||
if (saved.pass === undefined) delete process.env.CODEMAN_PASSWORD;
|
||||
else process.env.CODEMAN_PASSWORD = saved.pass;
|
||||
});
|
||||
|
||||
it('falls back to the .env file, quotes and an export prefix stripped, default user admin', () => {
|
||||
const env = join(dir, '.env');
|
||||
writeFileSync(env, '# a comment\nnot an assignment\nexport CODEMAN_PASSWORD="hunter2"\n');
|
||||
expect(readCodemanCredentials(env)).toEqual({ username: 'admin', password: 'hunter2' });
|
||||
});
|
||||
|
||||
it('prefers the environment over the file', () => {
|
||||
const env = join(dir, '.env');
|
||||
writeFileSync(env, 'CODEMAN_USERNAME=file\nCODEMAN_PASSWORD=file-pass\n');
|
||||
process.env.CODEMAN_USERNAME = 'envuser';
|
||||
process.env.CODEMAN_PASSWORD = 'env-pass';
|
||||
expect(readCodemanCredentials(env)).toEqual({ username: 'envuser', password: 'env-pass' });
|
||||
});
|
||||
|
||||
it('an absent file means no password, and no header to send', () => {
|
||||
const creds = readCodemanCredentials(join(dir, 'missing'));
|
||||
expect(creds).toEqual({ username: 'admin' });
|
||||
expect(basicAuthHeader(creds)).toBeUndefined();
|
||||
expect(readCodemanEnvFile(join(dir, 'missing'))).toEqual({});
|
||||
});
|
||||
|
||||
it('takes an explicit environment, field by field', () => {
|
||||
const env = join(dir, '.env');
|
||||
writeFileSync(env, 'CODEMAN_USERNAME=joe\n');
|
||||
expect(readCodemanCredentials(env, { CODEMAN_PASSWORD: 'pw' })).toEqual({ username: 'joe', password: 'pw' });
|
||||
});
|
||||
|
||||
it('builds a Basic header from a password', () => {
|
||||
expect(basicAuthHeader({ username: 'joe', password: 'pw' })).toBe(
|
||||
`Basic ${Buffer.from('joe:pw').toString('base64')}`
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user