mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-20 02:17:05 +00:00
feat(cli): refresh help and shell completion UX
- add a shared command catalog for root help and completion routing - replace shell-local completion logic with a hidden __complete backend - add topic help and parity tests for router, help, and completion coverage
This commit is contained in:
1 parent
3eb55d0e33
commit
24ba2abe10
15 files changed
+1227
-2118
No files matched your search
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import { ROOT_COMMAND_CATALOG, getAllRootCommandTokens } from '../../../src/commands/command-catalog';
|
||||
import { ROOT_COMMAND_ROUTES } from '../../../src/commands/root-command-router';
|
||||
|
||||
describe('command catalog', () => {
|
||||
test('covers every routed root command and alias', () => {
|
||||
const catalogTokens = new Set(getAllRootCommandTokens());
|
||||
|
||||
for (const route of ROOT_COMMAND_ROUTES) {
|
||||
expect(catalogTokens.has(route.name)).toBe(true);
|
||||
for (const alias of route.aliases || []) {
|
||||
expect(catalogTokens.has(alias)).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('keeps hidden operational hooks out of the public help surface', () => {
|
||||
const hiddenCommands = ROOT_COMMAND_CATALOG.filter((entry) => entry.visibility === 'hidden').map(
|
||||
(entry) => entry.name
|
||||
);
|
||||
|
||||
expect(hiddenCommands).toContain('--install');
|
||||
expect(hiddenCommands).toContain('--uninstall');
|
||||
expect(hiddenCommands).toContain('__complete');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { getCompletionSuggestions } from '../../../src/commands/completion-backend';
|
||||
|
||||
const originalHome = process.env.HOME;
|
||||
const originalUserProfile = process.env.USERPROFILE;
|
||||
const originalCcsHome = process.env.CCS_HOME;
|
||||
let tempHome = '';
|
||||
|
||||
function suggestionValues(
|
||||
tokensBeforeCurrent: string[],
|
||||
current = '',
|
||||
shell: 'bash' | 'fish' | 'powershell' | 'zsh' = 'bash'
|
||||
): string[] {
|
||||
return getCompletionSuggestions({ tokensBeforeCurrent, current, shell }).map((entry) => entry.value);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-completion-'));
|
||||
fs.mkdirSync(path.join(tempHome, '.ccs'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(tempHome, '.ccs', 'config.json'),
|
||||
JSON.stringify(
|
||||
{
|
||||
profiles: {
|
||||
localglm: '~/.ccs/localglm.settings.json',
|
||||
},
|
||||
cliproxy: {
|
||||
'my-codex': {
|
||||
provider: 'codex',
|
||||
settings: '~/.ccs/my-codex.settings.json',
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tempHome, '.ccs', 'profiles.json'),
|
||||
JSON.stringify(
|
||||
{
|
||||
profiles: {
|
||||
work: { type: 'account', created: '2026-04-02T00:00:00.000Z' },
|
||||
},
|
||||
default: 'work',
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
process.env.HOME = tempHome;
|
||||
process.env.USERPROFILE = tempHome;
|
||||
process.env.CCS_HOME = tempHome;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = originalHome;
|
||||
if (originalUserProfile === undefined) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = originalUserProfile;
|
||||
if (originalCcsHome === undefined) delete process.env.CCS_HOME;
|
||||
else process.env.CCS_HOME = originalCcsHome;
|
||||
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('completion backend', () => {
|
||||
test('includes current root commands and dynamic profiles at the top level', () => {
|
||||
const values = suggestionValues([]);
|
||||
|
||||
expect(values).toContain('config');
|
||||
expect(values).toContain('docker');
|
||||
expect(values).toContain('tokens');
|
||||
expect(values).toContain('migrate');
|
||||
expect(values).toContain('cursor');
|
||||
expect(values).toContain('copilot');
|
||||
expect(values).toContain('gemini');
|
||||
expect(values).toContain('localglm');
|
||||
expect(values).toContain('work');
|
||||
expect(values).toContain('my-codex');
|
||||
expect(values).not.toContain('__complete');
|
||||
expect(values).not.toContain('--install');
|
||||
expect(values).not.toContain('--uninstall');
|
||||
});
|
||||
|
||||
test('suggests help topics from the help command', () => {
|
||||
const values = suggestionValues(['help']);
|
||||
expect(values).toContain('profiles');
|
||||
expect(values).toContain('providers');
|
||||
expect(values).toContain('completion');
|
||||
expect(values).toContain('targets');
|
||||
expect(values).not.toContain('api');
|
||||
expect(values).not.toContain('__complete');
|
||||
});
|
||||
|
||||
test('suggests lifecycle subcommands for api', () => {
|
||||
const values = suggestionValues(['api']);
|
||||
expect(values).toEqual(expect.arrayContaining(['create', 'discover', 'copy', 'export', 'import', 'remove']));
|
||||
});
|
||||
|
||||
test('suggests account profiles for auth show', () => {
|
||||
const values = suggestionValues(['auth', 'show']);
|
||||
expect(values).toContain('work');
|
||||
expect(values).toContain('--json');
|
||||
});
|
||||
|
||||
test('suggests cliproxy variants for variant-scoped commands', () => {
|
||||
const values = suggestionValues(['cliproxy', 'edit']);
|
||||
expect(values).toContain('my-codex');
|
||||
});
|
||||
|
||||
test('suggests env format values after the format flag', () => {
|
||||
const values = suggestionValues(['env', '--format']);
|
||||
expect(values).toEqual(expect.arrayContaining(['openai', 'anthropic', 'raw', 'claude-extension']));
|
||||
});
|
||||
|
||||
test('includes live doctor and cliproxy flags from the shared catalog', () => {
|
||||
expect(suggestionValues(['doctor'])).toEqual(expect.arrayContaining(['--fix', '-f']));
|
||||
expect(suggestionValues(['cliproxy'])).toEqual(expect.arrayContaining(['remove', '--backend']));
|
||||
});
|
||||
|
||||
test('filters suggestions by the current token prefix', () => {
|
||||
const values = suggestionValues([], 'do');
|
||||
expect(values).toEqual(expect.arrayContaining(['docker', 'doctor']));
|
||||
expect(values).not.toContain('tokens');
|
||||
});
|
||||
});
|
||||
@@ -1,152 +1,77 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import { showApiCommandHelp } from '../../../src/commands/api-command/help';
|
||||
import { handleHelpCommand } from '../../../src/commands/help-command';
|
||||
import {
|
||||
handleHelpCommand,
|
||||
handleHelpRoute,
|
||||
getRootHelpVisibleCommands,
|
||||
} from '../../../src/commands/help-command';
|
||||
|
||||
function stripAnsi(input: string): string {
|
||||
return input.replace(/\u001b\[[0-9;]*m/g, '');
|
||||
}
|
||||
|
||||
async function renderLines(
|
||||
render: (writeLine: (line: string) => void) => Promise<void>
|
||||
): Promise<string> {
|
||||
const lines: string[] = [];
|
||||
await render((line) => lines.push(line));
|
||||
return stripAnsi(lines.join('\n'));
|
||||
}
|
||||
|
||||
describe('help command parity', () => {
|
||||
test('root help documents cliproxy provider filter under quota command', async () => {
|
||||
const lines: string[] = [];
|
||||
await handleHelpCommand((line) => lines.push(line));
|
||||
test('root help stays within the compact line budget', async () => {
|
||||
const rendered = await renderLines((writeLine) => handleHelpCommand(writeLine));
|
||||
const visibleLines = rendered.split('\n').filter((line) => line.trim().length > 0);
|
||||
|
||||
const rendered = stripAnsi(lines.join('\n'));
|
||||
expect(rendered.includes('ccs cliproxy status [provider]')).toBe(false);
|
||||
expect(rendered.includes('ccs cliproxy status')).toBe(true);
|
||||
expect(rendered.includes('ccs cliproxy quota --provider <name>')).toBe(true);
|
||||
expect(rendered.includes('ccs llamacpp')).toBe(true);
|
||||
expect(visibleLines.length).toBeLessThanOrEqual(90);
|
||||
expect(rendered.includes('ccs help <topic>')).toBe(true);
|
||||
expect(rendered.includes('ccs help completion')).toBe(true);
|
||||
});
|
||||
|
||||
test('root help documents llama.cpp as a local API profile', async () => {
|
||||
const lines: string[] = [];
|
||||
await handleHelpCommand((line) => lines.push(line));
|
||||
test('root help covers every public root command once the catalog is updated', async () => {
|
||||
const rendered = await renderLines((writeLine) => handleHelpCommand(writeLine));
|
||||
|
||||
const rendered = stripAnsi(lines.join('\n'));
|
||||
expect(rendered.includes('ccs llamacpp')).toBe(true);
|
||||
expect(rendered.includes('http://127.0.0.1:8080')).toBe(true);
|
||||
for (const command of getRootHelpVisibleCommands()) {
|
||||
expect(rendered.includes(command)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('root help no longer markets glmt as a supported profile', async () => {
|
||||
const lines: string[] = [];
|
||||
await handleHelpCommand((line) => lines.push(line));
|
||||
|
||||
const rendered = stripAnsi(lines.join('\n'));
|
||||
test('root help no longer markets deprecated glmt directly', async () => {
|
||||
const rendered = await renderLines((writeLine) => handleHelpCommand(writeLine));
|
||||
expect(rendered.includes('ccs glmt')).toBe(false);
|
||||
expect(rendered.includes('ccs glm')).toBe(true);
|
||||
});
|
||||
|
||||
test('root help documents Claude IDE extension setup surfaces', async () => {
|
||||
const lines: string[] = [];
|
||||
await handleHelpCommand((line) => lines.push(line));
|
||||
test('providers topic lists built-in OAuth provider shortcuts', async () => {
|
||||
const rendered = await renderLines((writeLine) => handleHelpRoute(['providers'], writeLine));
|
||||
|
||||
const rendered = stripAnsi(lines.join('\n'));
|
||||
expect(rendered.includes('Claude IDE Extension setup page')).toBe(true);
|
||||
expect(rendered.includes('ccs env <profile> --format claude-extension --ide vscode')).toBe(
|
||||
true
|
||||
);
|
||||
expect(rendered.includes('ccs env <profile> --format claude-extension --ide windsurf')).toBe(
|
||||
true
|
||||
);
|
||||
expect(rendered.includes('Built-in OAuth Providers')).toBe(true);
|
||||
expect(rendered.includes('ccs cliproxy --help')).toBe(true);
|
||||
expect(rendered.includes('gemini')).toBe(true);
|
||||
expect(rendered.includes('codex')).toBe(true);
|
||||
expect(rendered.includes('ghcp')).toBe(true);
|
||||
});
|
||||
|
||||
test('root help documents dashboard host binding example', async () => {
|
||||
const lines: string[] = [];
|
||||
await handleHelpCommand((line) => lines.push(line));
|
||||
test('completion topic documents install and verification paths', async () => {
|
||||
const rendered = await renderLines((writeLine) => handleHelpRoute(['completion'], writeLine));
|
||||
|
||||
const rendered = stripAnsi(lines.join('\n'));
|
||||
expect(rendered.includes('ccs config --host 0.0.0.0')).toBe(true);
|
||||
expect(rendered.includes('Force all-interface binding for remote devices')).toBe(true);
|
||||
expect(rendered.includes('ccs --shell-completion')).toBe(true);
|
||||
expect(rendered.includes('ccs help <TAB>')).toBe(true);
|
||||
expect(rendered.includes('--force')).toBe(true);
|
||||
});
|
||||
|
||||
test('root help documents docker deployment commands', async () => {
|
||||
const lines: string[] = [];
|
||||
await handleHelpCommand((line) => lines.push(line));
|
||||
test('api topic delegates to command-specific help', async () => {
|
||||
const rendered = await renderLines((writeLine) => handleHelpRoute(['api'], writeLine));
|
||||
|
||||
const rendered = stripAnsi(lines.join('\n'));
|
||||
expect(rendered.includes('ccs docker --help')).toBe(true);
|
||||
expect(rendered.includes('ccs docker up --host <ssh>')).toBe(true);
|
||||
expect(rendered.includes('ccs docker update')).toBe(true);
|
||||
});
|
||||
|
||||
test('root help documents official channels native-only scope and process-env tokens', async () => {
|
||||
const lines: string[] = [];
|
||||
await handleHelpCommand((line) => lines.push(line));
|
||||
|
||||
const rendered = stripAnsi(lines.join('\n'));
|
||||
expect(rendered.includes('Dashboard -> Settings -> Channels (fastest path)')).toBe(true);
|
||||
expect(
|
||||
rendered.includes(
|
||||
'Fastest path: turn on the channel, save the token if needed, then run ccs.'
|
||||
)
|
||||
).toBe(true);
|
||||
expect(rendered.includes('ccs codex')).toBe(true);
|
||||
expect(rendered.includes('ccs --target codex')).toBe(true);
|
||||
expect(
|
||||
rendered.includes('Current-process TELEGRAM_BOT_TOKEN / DISCORD_BOT_TOKEN also work')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('root help documents bare cursor as the runtime entrypoint', async () => {
|
||||
const lines: string[] = [];
|
||||
await handleHelpCommand((line) => lines.push(line));
|
||||
|
||||
const rendered = stripAnsi(lines.join('\n'));
|
||||
expect(rendered.includes('ccs cursor <cmd>')).toBe(false);
|
||||
expect(rendered.includes('Run Claude via Cursor local proxy')).toBe(true);
|
||||
});
|
||||
|
||||
test('root help explains Claude [1m] as an explicit CCS suffix with upstream limits', async () => {
|
||||
const lines: string[] = [];
|
||||
await handleHelpCommand((line) => lines.push(line));
|
||||
|
||||
const rendered = stripAnsi(lines.join('\n'));
|
||||
expect(
|
||||
rendered.includes('Claude models: plain by default, opt-in with --1m or saved [1m]')
|
||||
).toBe(true);
|
||||
expect(rendered.includes('CCS only controls the saved [1m] suffix.')).toBe(true);
|
||||
expect(rendered.includes('return 429 extra-usage errors for long-context requests')).toBe(true);
|
||||
});
|
||||
|
||||
test('root help documents native Codex runtime alias and runtime-only scope', async () => {
|
||||
const lines: string[] = [];
|
||||
await handleHelpCommand((line) => lines.push(line));
|
||||
|
||||
const rendered = stripAnsi(lines.join('\n'));
|
||||
expect(rendered.includes('ccs-codex <profile> [args]')).toBe(true);
|
||||
expect(rendered.includes('ccsx <profile> [args]')).toBe(true);
|
||||
expect(rendered.includes('ccsxp [args]')).toBe(true);
|
||||
expect(rendered.includes('ccs --target codex')).toBe(true);
|
||||
expect(rendered.includes('ccsxp "your prompt"')).toBe(true);
|
||||
expect(rendered.includes('ccs codex-api --target codex')).toBe(true);
|
||||
expect(rendered.includes('Compatible -> Codex CLI')).toBe(true);
|
||||
expect(rendered.includes('CLIPROXY_API_KEY')).toBe(true);
|
||||
expect(rendered.includes('codex (runtime-only)')).toBe(true);
|
||||
});
|
||||
|
||||
test('api help documents create-time Claude [1m] flags and entitlement warning', async () => {
|
||||
const lines: string[] = [];
|
||||
await showApiCommandHelp((line) => lines.push(line));
|
||||
|
||||
const rendered = stripAnsi(lines.join('\n'));
|
||||
expect(rendered.includes('--1m / --no-1m')).toBe(true);
|
||||
expect(rendered.includes('CCS API Management')).toBe(true);
|
||||
expect(rendered.includes('ccs api create --preset anthropic --1m')).toBe(true);
|
||||
expect(rendered.includes('Plain Claude model IDs stay on standard context by default.')).toBe(
|
||||
true
|
||||
);
|
||||
expect(rendered.includes('some accounts can still return 429 for long-context requests')).toBe(
|
||||
true
|
||||
);
|
||||
expect(rendered.includes('ccs api discover --register')).toBe(true);
|
||||
});
|
||||
|
||||
test('api help documents Codex bridge runtime launch separately from persisted targets', async () => {
|
||||
const lines: string[] = [];
|
||||
await showApiCommandHelp((line) => lines.push(line));
|
||||
test('unknown help target shows an actionable fallback', async () => {
|
||||
const rendered = await renderLines((writeLine) => handleHelpRoute(['unknown-topic'], writeLine));
|
||||
|
||||
const rendered = stripAnsi(lines.join('\n'));
|
||||
expect(rendered.includes('ccs api create codex-api --cliproxy-provider codex')).toBe(true);
|
||||
expect(rendered.includes('ccs codex-api --target codex')).toBe(true);
|
||||
expect(rendered.includes('Default target: claude or droid (create)')).toBe(true);
|
||||
expect(rendered.includes('Unknown help topic or command: unknown-topic')).toBe(true);
|
||||
expect(rendered.includes('Available help topics:')).toBe(true);
|
||||
process.exitCode = 0;
|
||||
});
|
||||
});
|
||||
@@ -27,7 +27,7 @@ beforeEach(() => {
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('../../../src/commands/api-command', () => ({
|
||||
mock.module('../../../src/commands/api-command/index', () => ({
|
||||
handleApiCommand: async (args: string[]) => {
|
||||
calls.push(`api:${args.join(' ')}`);
|
||||
},
|
||||
@@ -42,6 +42,12 @@ beforeEach(() => {
|
||||
mock.module('../../../src/commands/tokens-command', () => ({
|
||||
handleTokensCommand: async () => 37,
|
||||
}));
|
||||
|
||||
mock.module('../../../src/commands/completion-backend', () => ({
|
||||
handleCompletionCommand: async (args: string[]) => {
|
||||
calls.push(`complete:${args.join(' ')}`);
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -120,4 +126,12 @@ describe('root-command-router', () => {
|
||||
|
||||
await expect(tryHandleRootCommand(['tokens', 'list'])).rejects.toThrow('process.exit(37)');
|
||||
});
|
||||
|
||||
it('routes hidden completion queries through the completion backend', async () => {
|
||||
const tryHandleRootCommand = await loadTryHandleRootCommand();
|
||||
|
||||
await expect(tryHandleRootCommand(['__complete', '--shell', 'bash', '--current', 'do'])).resolves.toBe(true);
|
||||
|
||||
expect(calls).toEqual(['complete:--shell bash --current do']);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user