diff --git a/packages/cli/README.md b/packages/cli/README.md index 46c1139..7743382 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -12,15 +12,25 @@ It does not contain local per-tool REST logic or local schema validation. ## Install +**Bun** ```bash bun add -g @youdotcom-oss/cli ``` +**Node** +```bash +npm add -g @youdotcom-oss/cli +``` Or run it without installing: +**Bun** ```bash bunx @youdotcom-oss/cli tools ``` +**Node** +```bash +npx @youdotcom-oss/cli tools +``` ## Commands @@ -52,6 +62,8 @@ echo '{"query":"latest bun release"}' | ydc you-search Prints the resolved URL, tool id, sanitized headers, and JSON arguments. - `--profile free` Supported only for `you-search`. In this mode the CLI routes to `?profile=free` and strips auth headers. +- `-h, --help` + Prints usage, available commands, tools, and flags, then exits. ## Environment diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 9f2779f..739d4b3 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -8,9 +8,35 @@ const BASE_MCP_SERVER_URL = 'https://api.you.com/mcp' type McpToolResult = Awaited> const args = process.argv.slice(2) const command = args[0] -const usage = `Usage: ydc tools - ydc schema [input|output] - ydc [flags]` + +const buildHelp = () => + [ + 'Usage: ydc tools', + ' ydc schema [input|output]', + " ydc '' [flags]", + " echo '' | ydc ", + '', + 'Agent-first CLI bridge for the hosted You.com MCP server.', + '', + 'Commands:', + ' tools List the locally allowlisted tool ids', + ' schema [input|output] Fetch the raw remote schema for a tool', + " '' Execute a remote tool with JSON input", + '', + 'Tools:', + ` ${TOOL_CONTRACT.tools.map(({ name }) => name).join(', ')}`, + '', + 'Flags:', + ' --api-key Use this API key instead of YDC_API_KEY', + ' --dry-run Print resolved URL, tool id, sanitized headers, and JSON arguments', + ' --profile free Route to ?profile=free and strip auth (you-search only)', + ' -h, --help Show this help message', + '', + 'Environment:', + ' YDC_API_KEY Optional default API key', + ].join('\n') + +const isHelpRequest = command === '--help' || command === '-h' if (command === 'tools') { console.log( @@ -156,8 +182,13 @@ if (command && tool) { } } -if (!command || command === '--help') { - console.log(usage) +if (!command) { + console.error(buildHelp()) + process.exit(1) +} + +if (isHelpRequest) { + console.log(buildHelp()) process.exit(0) } diff --git a/packages/cli/src/tests/cli.spec.ts b/packages/cli/src/tests/cli.spec.ts index 146b966..184ed37 100644 --- a/packages/cli/src/tests/cli.spec.ts +++ b/packages/cli/src/tests/cli.spec.ts @@ -124,6 +124,81 @@ describe('ydc help and command validation', () => { expect(stdout).toContain('Usage: ydc tools') }) + test('prints the same help for the -h short flag', async () => { + const longChild = Bun.spawn({ + cmd: ['bun', './src/cli.ts', '--help'], + cwd: `${import.meta.dir}/../..`, + stderr: 'pipe', + stdout: 'pipe', + }) + const shortChild = Bun.spawn({ + cmd: ['bun', './src/cli.ts', '-h'], + cwd: `${import.meta.dir}/../..`, + stderr: 'pipe', + stdout: 'pipe', + }) + + const [longOut, , longExit] = await Promise.all([ + new Response(longChild.stdout).text(), + new Response(longChild.stderr).text(), + longChild.exited, + ]) + const [shortOut, shortErr, shortExit] = await Promise.all([ + new Response(shortChild.stdout).text(), + new Response(shortChild.stderr).text(), + shortChild.exited, + ]) + + expect(longExit).toBe(0) + expect(shortExit).toBe(0) + expect(shortErr).toBe('') + expect(shortOut).toBe(longOut) + }) + + test('lists every allowlisted tool and flag in the help output', async () => { + const child = Bun.spawn({ + cmd: ['bun', './src/cli.ts', '--help'], + cwd: `${import.meta.dir}/../..`, + stderr: 'pipe', + stdout: 'pipe', + }) + + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + child.exited, + ]) + + expect(exitCode).toBe(0) + expect(stderr).toBe('') + for (const { name } of TOOL_CONTRACT.tools) { + expect(stdout).toContain(name) + } + expect(stdout).toContain('--api-key') + expect(stdout).toContain('--dry-run') + expect(stdout).toContain('--profile') + expect(stdout).toContain('-h, --help') + }) + + test('prints help to stderr and exits non-zero when no command is given', async () => { + const child = Bun.spawn({ + cmd: ['bun', './src/cli.ts'], + cwd: `${import.meta.dir}/../..`, + stderr: 'pipe', + stdout: 'pipe', + }) + + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + child.exited, + ]) + + expect(exitCode).toBe(1) + expect(stdout).toBe('') + expect(stderr).toContain('Usage: ydc tools') + }) + test('rejects unknown commands immediately', async () => { const child = Bun.spawn({ cmd: ['bun', './src/cli.ts', 'you-missing'],