refactor(commands): add command contract and migrate shell completion

This commit is contained in:
Tam Nhu Tran
2026-02-12 12:59:17 +07:00
parent e68ae5b166
commit fc4b77bc52
4 changed files with 300 additions and 30 deletions
@@ -0,0 +1,30 @@
/**
* Command Execution Contract
*
* Standardized lifecycle: parse -> validate -> execute -> render
* for CLI command handlers.
*/
export interface CommandExecutionContract<TParsedArgs, TExecutionResult> {
parse(rawArgs: string[]): TParsedArgs;
validate(parsedArgs: TParsedArgs): void;
execute(parsedArgs: TParsedArgs): Promise<TExecutionResult> | TExecutionResult;
render(
result: TExecutionResult,
context: { rawArgs: string[]; parsedArgs: TParsedArgs }
): Promise<void> | void;
}
/**
* Run a command through the standard lifecycle.
*/
export async function runCommandWithContract<TParsedArgs, TExecutionResult>(
rawArgs: string[],
contract: CommandExecutionContract<TParsedArgs, TExecutionResult>
): Promise<{ parsedArgs: TParsedArgs; result: TExecutionResult }> {
const parsedArgs = contract.parse(rawArgs);
contract.validate(parsedArgs);
const result = await contract.execute(parsedArgs);
await contract.render(result, { rawArgs, parsedArgs });
return { parsedArgs, result };
}
+71 -30
View File
@@ -5,6 +5,75 @@
*/
import { initUI, header, ok, fail, color } from '../utils/ui';
import {
runCommandWithContract,
type CommandExecutionContract,
} from './command-execution-contract';
type ShellTarget = 'bash' | 'zsh' | 'fish' | 'powershell' | null;
interface ShellCompletionParsedArgs {
targetShell: ShellTarget;
force: boolean;
}
interface ShellCompletionInstallResult {
success: boolean;
alreadyInstalled?: boolean;
message?: string;
reload?: string;
}
interface ShellCompletionInstallerLike {
install(
shell: ShellTarget,
options: { force: boolean }
): ShellCompletionInstallResult;
}
export function parseShellCompletionArgs(args: string[]): ShellCompletionParsedArgs {
let targetShell: ShellTarget = null;
const force = args.includes('--force') || args.includes('-f');
if (args.includes('--bash')) targetShell = 'bash';
else if (args.includes('--zsh')) targetShell = 'zsh';
else if (args.includes('--fish')) targetShell = 'fish';
else if (args.includes('--powershell')) targetShell = 'powershell';
return { targetShell, force };
}
export function createShellCompletionCommandContract(
installer: ShellCompletionInstallerLike
): CommandExecutionContract<ShellCompletionParsedArgs, ShellCompletionInstallResult> {
return {
parse: parseShellCompletionArgs,
validate: () => {
// No validation at this stage to preserve existing behavior exactly.
},
execute: (parsed) => installer.install(parsed.targetShell, { force: parsed.force }),
render: (result, context) => {
if (result.alreadyInstalled && !context.parsedArgs.force) {
console.log(ok('Shell completion already installed'));
console.log(` Use ${color('--force', 'warning')} to reinstall`);
console.log('');
return;
}
console.log(ok('Shell completion installed successfully!'));
console.log('');
console.log(result.message);
console.log('');
console.log(color('To activate:', 'info'));
console.log(` ${result.reload}`);
console.log('');
console.log(color('Then test:', 'info'));
console.log(' ccs <TAB> # See available profiles');
console.log(' ccs auth <TAB> # See auth subcommands');
console.log('');
},
};
}
/**
* Handle shell completion command
@@ -16,38 +85,10 @@ export async function handleShellCompletionCommand(args: string[]): Promise<void
console.log(header('Shell Completion Installer'));
console.log('');
// Parse flags
let targetShell: string | null = null;
const force = args.includes('--force') || args.includes('-f');
if (args.includes('--bash')) targetShell = 'bash';
else if (args.includes('--zsh')) targetShell = 'zsh';
else if (args.includes('--fish')) targetShell = 'fish';
else if (args.includes('--powershell')) targetShell = 'powershell';
try {
const installer = new ShellCompletionInstaller();
const result = installer.install(targetShell as 'bash' | 'zsh' | 'fish' | 'powershell' | null, {
force,
});
if (result.alreadyInstalled && !force) {
console.log(ok('Shell completion already installed'));
console.log(` Use ${color('--force', 'warning')} to reinstall`);
console.log('');
return;
}
console.log(ok('Shell completion installed successfully!'));
console.log('');
console.log(result.message);
console.log('');
console.log(color('To activate:', 'info'));
console.log(` ${result.reload}`);
console.log('');
console.log(color('Then test:', 'info'));
console.log(' ccs <TAB> # See available profiles');
console.log(' ccs auth <TAB> # See auth subcommands');
console.log('');
const contract = createShellCompletionCommandContract(installer);
await runCommandWithContract(args, contract);
} catch (error) {
const err = error as Error;
console.error(fail(`Error: ${err.message}`));
@@ -0,0 +1,59 @@
import { describe, it, expect } from 'bun:test';
import { runCommandWithContract } from '../../../src/commands/command-execution-contract';
describe('runCommandWithContract', () => {
it('runs parse -> validate -> execute -> render in order', async () => {
const lifecycle: string[] = [];
const result = await runCommandWithContract(['--flag'], {
parse: (rawArgs) => {
lifecycle.push('parse');
expect(rawArgs).toEqual(['--flag']);
return { parsed: true, value: rawArgs[0] };
},
validate: (parsedArgs) => {
lifecycle.push('validate');
expect(parsedArgs).toEqual({ parsed: true, value: '--flag' });
},
execute: async (parsedArgs) => {
lifecycle.push('execute');
return { output: parsedArgs.value.toUpperCase() };
},
render: (executionResult, context) => {
lifecycle.push('render');
expect(executionResult).toEqual({ output: '--FLAG' });
expect(context.rawArgs).toEqual(['--flag']);
expect(context.parsedArgs).toEqual({ parsed: true, value: '--flag' });
},
});
expect(lifecycle).toEqual(['parse', 'validate', 'execute', 'render']);
expect(result.parsedArgs).toEqual({ parsed: true, value: '--flag' });
expect(result.result).toEqual({ output: '--FLAG' });
});
it('short-circuits after validate failure', async () => {
const lifecycle: string[] = [];
const promise = runCommandWithContract([], {
parse: () => {
lifecycle.push('parse');
return { valid: false };
},
validate: () => {
lifecycle.push('validate');
throw new Error('validation failed');
},
execute: () => {
lifecycle.push('execute');
return { ok: true };
},
render: () => {
lifecycle.push('render');
},
});
await expect(promise).rejects.toThrow('validation failed');
expect(lifecycle).toEqual(['parse', 'validate']);
});
});
@@ -0,0 +1,140 @@
import { describe, it, expect, beforeAll, beforeEach, afterEach, mock } from 'bun:test';
type ShellTarget = 'bash' | 'zsh' | 'fish' | 'powershell' | null;
interface InstallResult {
success: boolean;
alreadyInstalled?: boolean;
message?: string;
reload?: string;
}
interface InstallCall {
shell: ShellTarget;
options: { force: boolean };
}
const installCalls: InstallCall[] = [];
let installResult: InstallResult = {
success: true,
message: 'Added to ~/.zshrc',
reload: 'source ~/.zshrc',
};
let installError: Error | null = null;
mock.module('../../../src/utils/shell-completion', () => ({
ShellCompletionInstaller: class {
install(shell: ShellTarget, options: { force: boolean }): InstallResult {
installCalls.push({ shell, options });
if (installError) {
throw installError;
}
return installResult;
}
},
}));
mock.module('../../../src/utils/ui', () => ({
initUI: async () => {},
header: (value: string) => value,
ok: (value: string) => value,
fail: (value: string) => value,
color: (value: string) => value,
}));
let handleShellCompletionCommand: (args: string[]) => Promise<void>;
let parseShellCompletionArgs: (args: string[]) => { targetShell: ShellTarget; force: boolean };
let originalConsoleLog: typeof console.log;
let originalConsoleError: typeof console.error;
let originalProcessExit: typeof process.exit;
let logLines: string[] = [];
let errorLines: string[] = [];
beforeAll(async () => {
const mod = await import('../../../src/commands/shell-completion-command');
handleShellCompletionCommand = mod.handleShellCompletionCommand;
parseShellCompletionArgs = mod.parseShellCompletionArgs;
});
beforeEach(() => {
installCalls.length = 0;
installError = null;
installResult = {
success: true,
message: 'Added to ~/.zshrc',
reload: 'source ~/.zshrc',
};
logLines = [];
errorLines = [];
originalConsoleLog = console.log;
originalConsoleError = console.error;
originalProcessExit = process.exit;
console.log = (...args: unknown[]) => {
logLines.push(args.map(String).join(' '));
};
console.error = (...args: unknown[]) => {
errorLines.push(args.map(String).join(' '));
};
process.exit = ((code?: number) => {
throw new Error(`process.exit(${code ?? 0})`);
}) as typeof process.exit;
});
afterEach(() => {
console.log = originalConsoleLog;
console.error = originalConsoleError;
process.exit = originalProcessExit;
});
describe('shell-completion command', () => {
it('parses shell flags and force flag', () => {
const parsed = parseShellCompletionArgs(['--zsh', '--force']);
expect(parsed).toEqual({ targetShell: 'zsh', force: true });
});
it('preserves existing priority when multiple shell flags are present', () => {
const parsed = parseShellCompletionArgs(['--zsh', '--bash']);
expect(parsed).toEqual({ targetShell: 'bash', force: false });
});
it('executes installer with parsed args and renders success output', async () => {
await handleShellCompletionCommand(['--zsh', '--force']);
expect(installCalls).toHaveLength(1);
expect(installCalls[0]).toEqual({
shell: 'zsh',
options: { force: true },
});
expect(logLines.some((line) => line.includes('Shell completion installed successfully!'))).toBe(
true
);
expect(logLines.some((line) => line.includes('source ~/.zshrc'))).toBe(true);
});
it('renders already-installed output without forcing reinstall', async () => {
installResult = {
success: true,
alreadyInstalled: true,
message: 'Updated completion files',
reload: 'source ~/.zshrc',
};
await handleShellCompletionCommand(['--zsh']);
expect(logLines.some((line) => line.includes('Shell completion already installed'))).toBe(true);
expect(logLines.some((line) => line.includes('Use --force to reinstall'))).toBe(true);
expect(logLines.some((line) => line.includes('installed successfully!'))).toBe(false);
});
it('prints usage and exits with code 1 on installer error', async () => {
installError = new Error('boom');
await expect(handleShellCompletionCommand([])).rejects.toThrow('process.exit(1)');
expect(errorLines.some((line) => line.includes('Error: boom'))).toBe(true);
expect(errorLines.some((line) => line.includes('ccs --shell-completion --zsh'))).toBe(true);
});
});