mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 06:16:37 +00:00
Implements the user-facing surface for ccsx auth profile management. After `ccsx auth create work` (auto-spawns codex login with CODEX_HOME pinned per D11), users can `eval "$(ccsx auth use work)"` in any shell to scope all subsequent codex invocations to that profile — letting two terminals run two different Codex accounts concurrently. - codex-auth-router: dispatches argv to subcommand handlers - create: idempotent, --force re-links config.toml preserving auth.json (D9), then auto-spawns codex login with CODEX_HOME pinned (D11); filesystem ops happen before registry write to avoid registry orphans on EACCES/ENOSPC - login: standalone re-auth for an existing profile - switch: persistent default in YAML registry - use: STDOUT-DISCIPLINED — emits only shell-evalable exports; bash/zsh/fish/PowerShell/cmd syntaxes via shell-detect; sets CCS_NO_PRE_DISPATCH=1 at module load to suppress recovery/migration banners that would otherwise contaminate eval (C2) - show: list (active(missing) row at top per D14) + detail views - remove: default-profile guard, active-shell warn, --yes / --force - ASCII-only output, NO_COLOR honored, all errors to stderr via exitWithError - pre-dispatch.ts: early-return when CCS_NO_PRE_DISPATCH=1, placed before autoMigrate which is itself a stdout writer 57 unit tests, all green. Help text cross-references the ccsxp/ccsx distinction since the binaries differ by one character (H5).
114 lines
4.0 KiB
TypeScript
114 lines
4.0 KiB
TypeScript
import { describe, expect, it } from 'bun:test';
|
|
import { detectShell, formatExport } from '../../../src/codex-auth/shell-detect';
|
|
import type { Shell } from '../../../src/codex-auth/shell-detect';
|
|
|
|
// ── detectShell ───────────────────────────────────────────────────────────────
|
|
|
|
describe('detectShell — Unix', () => {
|
|
it('returns bash for /bin/bash', () => {
|
|
expect(detectShell({ SHELL: '/bin/bash' }, 'linux')).toBe('bash');
|
|
});
|
|
|
|
it('returns zsh for /usr/bin/zsh', () => {
|
|
expect(detectShell({ SHELL: '/usr/bin/zsh' }, 'darwin')).toBe('zsh');
|
|
});
|
|
|
|
it('returns fish for /usr/local/bin/fish', () => {
|
|
expect(detectShell({ SHELL: '/usr/local/bin/fish' }, 'linux')).toBe('fish');
|
|
});
|
|
|
|
it('returns bash for /bin/sh (generic POSIX)', () => {
|
|
expect(detectShell({ SHELL: '/bin/sh' }, 'linux')).toBe('bash');
|
|
});
|
|
|
|
it('returns bash when SHELL is unset', () => {
|
|
expect(detectShell({}, 'linux')).toBe('bash');
|
|
});
|
|
|
|
it('returns bash for /usr/local/bin/bash (Homebrew)', () => {
|
|
expect(detectShell({ SHELL: '/usr/local/bin/bash' }, 'darwin')).toBe('bash');
|
|
});
|
|
});
|
|
|
|
describe('detectShell — Windows', () => {
|
|
it('returns pwsh when PSModulePath is set', () => {
|
|
expect(detectShell({ PSModulePath: 'C:\\Windows\\system32\\...' }, 'win32')).toBe('pwsh');
|
|
});
|
|
|
|
it('returns cmd when PSModulePath is absent', () => {
|
|
expect(detectShell({}, 'win32')).toBe('cmd');
|
|
});
|
|
|
|
it('ignores SHELL on Windows — uses PSModulePath heuristic', () => {
|
|
expect(detectShell({ SHELL: '/bin/bash', PSModulePath: 'C:\\ps' }, 'win32')).toBe('pwsh');
|
|
});
|
|
});
|
|
|
|
// ── formatExport ──────────────────────────────────────────────────────────────
|
|
|
|
describe('formatExport — bash', () => {
|
|
it('wraps value in single quotes', () => {
|
|
expect(formatExport('bash', 'CODEX_HOME', '/home/user/.ccs/codex-instances/work')).toBe(
|
|
"export CODEX_HOME='/home/user/.ccs/codex-instances/work'"
|
|
);
|
|
});
|
|
|
|
it('escapes single quotes in value', () => {
|
|
const result = formatExport('bash', 'X', "it's");
|
|
expect(result).toBe("export X='it'\\''s'");
|
|
});
|
|
});
|
|
|
|
describe('formatExport — zsh', () => {
|
|
it('uses same syntax as bash', () => {
|
|
expect(formatExport('zsh', 'CCS_CODEX_PROFILE', 'work')).toBe(
|
|
"export CCS_CODEX_PROFILE='work'"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('formatExport — fish', () => {
|
|
it('uses set -gx syntax with semicolon', () => {
|
|
expect(formatExport('fish', 'CODEX_HOME', '/path/to/dir')).toBe(
|
|
"set -gx CODEX_HOME '/path/to/dir';"
|
|
);
|
|
});
|
|
|
|
it('escapes single quotes', () => {
|
|
const result = formatExport('fish', 'X', "a'b");
|
|
expect(result).toContain('set -gx X');
|
|
expect(result).toContain("'a'\\''b'");
|
|
});
|
|
});
|
|
|
|
describe('formatExport — pwsh', () => {
|
|
it('uses $env: assignment with double quotes', () => {
|
|
expect(formatExport('pwsh', 'CODEX_HOME', 'C:\\Users\\foo')).toBe(
|
|
'$env:CODEX_HOME = "C:\\Users\\foo"'
|
|
);
|
|
});
|
|
|
|
it('doubles internal double quotes', () => {
|
|
const result = formatExport('pwsh', 'X', 'say "hello"');
|
|
expect(result).toBe('$env:X = "say ""hello"""');
|
|
});
|
|
});
|
|
|
|
describe('formatExport — cmd', () => {
|
|
it('uses set KEY=VALUE syntax without quotes', () => {
|
|
expect(formatExport('cmd', 'CODEX_HOME', 'C:\\Users\\foo\\.ccs\\codex-instances\\work')).toBe(
|
|
'set CODEX_HOME=C:\\Users\\foo\\.ccs\\codex-instances\\work'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('formatExport — each shell produces distinct syntax', () => {
|
|
const shells: Shell[] = ['bash', 'zsh', 'fish', 'pwsh', 'cmd'];
|
|
it('all shells produce different output for same input', () => {
|
|
const outputs = shells.map((s) => formatExport(s, 'K', 'val'));
|
|
const unique = new Set(outputs);
|
|
// fish and bash differ; pwsh and cmd differ; bash and zsh are identical by design
|
|
expect(unique.size).toBeGreaterThanOrEqual(4);
|
|
});
|
|
});
|