From c0835dfde127e287d2eea6d401f441fea9362fe2 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Tue, 18 Aug 2026 20:11:31 +0900 Subject: [PATCH 1/4] feat: add agent-context command --- README.md | 28 ++++--- src/cli.ts | 2 + src/commands/agent-context/index.test.ts | 62 ++++++++++++++ src/commands/agent-context/index.ts | 101 +++++++++++++++++++++++ src/commands/build/index.ts | 12 +++ src/commands/request/index.ts | 16 ++++ src/commands/routes/index.ts | 12 +++ src/utils/agent-context.ts | 14 ++++ 8 files changed, 236 insertions(+), 11 deletions(-) create mode 100644 src/commands/agent-context/index.test.ts create mode 100644 src/commands/agent-context/index.ts create mode 100644 src/utils/agent-context.ts diff --git a/README.md b/README.md index f78d84b..8ed4a77 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,9 @@ hono build # Show routes of your Hono app hono routes + +# Show how to use Hono CLI, for coding agents +hono agent-context ``` ## Commands @@ -33,6 +36,7 @@ hono routes - `request [file]` - Send request to Hono app using `app.request()` - `build [entry]` - Build your Hono app - `routes [file]` - Show routes of your Hono app +- `agent-context` - Show how to use Hono CLI, for coding agents ### `request` @@ -217,23 +221,25 @@ hono routes [file] [options] } ``` -## Tips +### `agent-context` -### Using Hono CLI with AI Code Agents +Show how to use Hono CLI, as Markdown for coding agents. The content is generated from the command definitions, so it always matches the installed version. + +```bash +hono agent-context +``` -When working with AI code agents like Claude Code, you can configure them to use the `hono` CLI for testing. Add the following to your project's `CLAUDE.md` or similar configuration: +## Tips -````markdown -## Hono Development +### Using Hono CLI with AI Code Agents -Use the `hono` CLI for efficient development. View all commands with `hono --help`. +Add one line to your project's `AGENTS.md` or `CLAUDE.md`: -```bash -# Test your app without starting a server -hono request -P /api/users src/index.ts -hono request -P /api/users -X POST -d '{"name":"Alice"}' src/index.ts +```markdown +Working on this Hono app? Run `hono agent-context` first and follow it. ``` -```` + +The agent reads the output and learns the whole workflow: `routes` to see the app, `request` to test it, and `build` to bundle it. ## Authors diff --git a/src/cli.ts b/src/cli.ts index d4ac5b9..6345125 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,6 +2,7 @@ import { Command } from 'commander' import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' +import { agentContextCommand } from './commands/agent-context/index.js' import { buildCommand } from './commands/build/index.js' import { requestCommand } from './commands/request/index.js' import { routesCommand } from './commands/routes/index.js' @@ -23,5 +24,6 @@ program buildCommand(program) requestCommand(program) routesCommand(program) +agentContextCommand(program) program.parse() diff --git a/src/commands/agent-context/index.test.ts b/src/commands/agent-context/index.test.ts new file mode 100644 index 0000000..a44ce9a --- /dev/null +++ b/src/commands/agent-context/index.test.ts @@ -0,0 +1,62 @@ +import { Command } from 'commander' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { buildCommand } from '../build/index.js' +import { requestCommand } from '../request/index.js' +import { routesCommand } from '../routes/index.js' +import { agentContextCommand } from './index.js' + +describe('agentContextCommand', () => { + let program: Command + const spyOnLog = () => vi.spyOn(console, 'log').mockImplementation(() => {}) + let consoleLogSpy: ReturnType + + beforeEach(() => { + program = new Command() + buildCommand(program) + requestCommand(program) + routesCommand(program) + agentContextCommand(program) + consoleLogSpy = spyOnLog() + }) + + afterEach(() => { + consoleLogSpy.mockRestore() + vi.restoreAllMocks() + }) + + const getOutput = async (): Promise => { + await program.parseAsync(['node', 'hono', 'agent-context']) + return consoleLogSpy.mock.calls[0][0] + } + + it('should print Markdown with the output contract and workflow', async () => { + const output = await getOutput() + expect(output).toContain('# Hono CLI') + expect(output).toContain('## Output contract') + expect(output).toContain('"ok": true') + expect(output).toContain('## Recommended workflow') + }) + + it('should document every command except itself', async () => { + const output = await getOutput() + expect(output).toContain('### hono build [entry]') + expect(output).toContain('### hono request [file]') + expect(output).toContain('### hono routes [file]') + expect(output).not.toContain('### hono agent-context') + }) + + it('should include options from the command definitions', async () => { + const output = await getOutput() + expect(output).toContain('`--optimize`') + expect(output).toContain('`-P, --path `') + expect(output).toContain('`--verbose`') + }) + + it('should include declared output shapes, error codes, and examples', async () => { + const output = await getOutput() + expect(output).toContain('`ENTRY_NOT_FOUND`') + expect(output).toContain('`INVALID_APP`') + expect(output).toContain('"router": "SmartRouter + RegExpRouter"') + expect(output).toContain('hono build --optimize') + }) +}) diff --git a/src/commands/agent-context/index.ts b/src/commands/agent-context/index.ts new file mode 100644 index 0000000..bf0fe04 --- /dev/null +++ b/src/commands/agent-context/index.ts @@ -0,0 +1,101 @@ +import type { Command } from 'commander' +import type { CommandAgentContext } from '../../utils/agent-context.js' +import { agentContext as buildContext } from '../build/index.js' +import { agentContext as requestContext } from '../request/index.js' +import { agentContext as routesContext } from '../routes/index.js' + +const contexts: Record = { + routes: routesContext, + request: requestContext, + build: buildContext, +} + +const INTRO = `# Hono CLI + +Hono CLI (\`hono\`) is a command-line tool for coding agents working on a +[Hono](https://hono.dev) app. It loads the app directly, so you can +inspect and test it without starting a server. + +## Output contract + +Every command prints JSON to stdout: + +- Success: \`{ "ok": true, "data": ... }\` with exit code 0 +- Failure: \`{ "ok": false, "error": { "code", "message", "hint" } }\` with exit code 1 + +Follow \`error.hint\` when a command fails. Logs go to stderr. Add +\`--plain\` when a human wants to read the output. + +## Recommended workflow + +1. \`hono routes\` — get all routes of the app without reading the source +2. \`hono request -P \` — send a request to the app without a server +3. After you change the app, run them again to verify +4. \`hono build\` — bundle the app (\`--optimize\` to reduce size)` + +export function agentContextCommand(program: Command) { + program + .command('agent-context') + .description('Show how to use Hono CLI, for coding agents') + .action(async () => { + console.log(renderAgentContext(program)) + }) +} + +export const renderAgentContext = (program: Command): string => { + const sections: string[] = [INTRO, '## Commands'] + + for (const command of program.commands) { + const name = command.name() + if (name === 'agent-context') { + continue + } + + const args = command.registeredArguments + .map((arg) => (arg.required ? `<${arg.name()}>` : `[${arg.name()}]`)) + .join(' ') + const lines: string[] = [] + lines.push(`### hono ${name}${args ? ` ${args}` : ''}`) + lines.push('') + lines.push(command.description()) + + if (command.options.length > 0) { + lines.push('') + lines.push('Options:') + lines.push('') + for (const option of command.options) { + const defaultValue = + option.defaultValue === undefined || option.defaultValue === false + ? '' + : ` (default: ${JSON.stringify(option.defaultValue)})` + lines.push(`- \`${option.flags}\` — ${option.description}${defaultValue}`) + } + } + + const context = contexts[name] + if (context?.output) { + lines.push('') + lines.push(`Output \`data\`: \`${context.output}\``) + } + if (context?.errors && context.errors.length > 0) { + lines.push('') + lines.push(`Error codes: ${context.errors.map((e) => `\`${e}\``).join(', ')}`) + } + if (context?.examples && context.examples.length > 0) { + lines.push('') + lines.push('Examples:') + lines.push('') + lines.push('```bash') + lines.push(...context.examples) + lines.push('```') + } + if (context?.notes && context.notes.length > 0) { + lines.push('') + lines.push(...context.notes.map((note) => `- ${note}`)) + } + + sections.push(lines.join('\n')) + } + + return sections.join('\n\n') +} diff --git a/src/commands/build/index.ts b/src/commands/build/index.ts index d67f9f3..8403975 100644 --- a/src/commands/build/index.ts +++ b/src/commands/build/index.ts @@ -5,10 +5,22 @@ import { buildInitParams, serializeInitParams } from 'hono/router/reg-exp-router import { execFile } from 'node:child_process' import { existsSync, realpathSync, statSync, readFileSync } from 'node:fs' import { dirname, join, resolve } from 'node:path' +import type { CommandAgentContext } from '../../utils/agent-context.js' import { buildAndImportApp } from '../../utils/build.js' import { CliError, handleErrors, printResult } from '../../utils/output.js' import { removeApis } from './remove-apis.js' +export const agentContext: CommandAgentContext = { + output: + '{ "optimized": true, "router": "PreparedRegExpRouter", "removed": { "requestBodyApis": true, "contextResponseApis": ["html"], "honoApis": ["route"] }, "output": "dist/index.js", "size": 34124 }', + errors: ['ENTRY_NOT_FOUND', 'INVALID_OPTION'], + examples: ['hono build', 'hono build --optimize', 'hono build --optimize -m -o dist/app.js'], + notes: [ + 'Without --optimize it just bundles the app.', + 'With --optimize, request body APIs are removed only when every route method is strictly GET/HEAD/OPTIONS. If you have checked the app never reads request bodies, pass --request-body-api-removal force.', + ], +} + const DEFAULT_ENTRY_CANDIDATES = ['src/index.ts', 'src/index.tsx', 'src/index.js', 'src/index.jsx'] const HONO_REMOVAL_METHODS = ['route', 'mount', 'fire'] diff --git a/src/commands/request/index.ts b/src/commands/request/index.ts index d1fc4f8..87957dc 100644 --- a/src/commands/request/index.ts +++ b/src/commands/request/index.ts @@ -1,9 +1,25 @@ import type { Command } from 'commander' import type { Hono } from 'hono' +import type { CommandAgentContext } from '../../utils/agent-context.js' import { getFilenameFromPath, saveFile } from '../../utils/file.js' import { getBuildIterator } from '../../utils/load-app.js' import { handleErrors, printResult } from '../../utils/output.js' +export const agentContext: CommandAgentContext = { + output: + '{ "status": 200, "headers": { "content-type": "application/json" }, "body": { "message": "Hello" } }', + errors: ['ENTRY_NOT_FOUND'], + examples: [ + 'hono request -P /api/users', + `hono request -P /api/users -X POST -d '{"name":"Alice"}'`, + 'hono request -P /image.png -o image.png', + ], + notes: [ + 'No server needed. The request goes directly to app.request().', + 'A JSON response body is embedded as an object. A binary body becomes null with "binary": true — save it with -o.', + ], +} + interface RequestOptions { method?: string data?: string diff --git a/src/commands/routes/index.ts b/src/commands/routes/index.ts index 7af45b1..a6282b1 100644 --- a/src/commands/routes/index.ts +++ b/src/commands/routes/index.ts @@ -1,8 +1,20 @@ import type { Command } from 'commander' import { getRouterName, inspectRoutes } from 'hono/dev' +import type { CommandAgentContext } from '../../utils/agent-context.js' import { getBuildIterator } from '../../utils/load-app.js' import { CliError, handleErrors, printResult } from '../../utils/output.js' +export const agentContext: CommandAgentContext = { + output: + '{ "router": "SmartRouter + RegExpRouter", "routes": [{ "method": "GET", "path": "/", "name": "[handler]", "isMiddleware": false }] }', + errors: ['ENTRY_NOT_FOUND', 'INVALID_APP'], + examples: ['hono routes', 'hono routes --verbose src/app.ts'], + notes: [ + 'Routes are resolved from the real app instance, so mounted sub-apps and basePath are all expanded.', + 'Run it first to get the full picture of an app without reading the source.', + ], +} + interface RoutesOptions { verbose: boolean plain: boolean diff --git a/src/utils/agent-context.ts b/src/utils/agent-context.ts new file mode 100644 index 0000000..f6d1eec --- /dev/null +++ b/src/utils/agent-context.ts @@ -0,0 +1,14 @@ +/** + * Extra information a command declares for `hono agent-context`. + * Keep it next to the command so it stays correct. + */ +export interface CommandAgentContext { + /** Shape of `data` in the JSON output, as a compact sample */ + output?: string + /** Error codes this command can return */ + errors?: string[] + /** Usage examples, one command line each */ + examples?: string[] + /** Extra notes for agents */ + notes?: string[] +} From 9ff2f182b3dc28b8180d9265b2f023333338b121 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Tue, 18 Aug 2026 20:18:24 +0900 Subject: [PATCH 2/4] chore: write agent-context document with hono/jsx --- src/commands/agent-context/document.tsx | 117 ++++++++++++++++++++++++ src/commands/agent-context/index.ts | 92 +------------------ 2 files changed, 118 insertions(+), 91 deletions(-) create mode 100644 src/commands/agent-context/document.tsx diff --git a/src/commands/agent-context/document.tsx b/src/commands/agent-context/document.tsx new file mode 100644 index 0000000..fdde0d8 --- /dev/null +++ b/src/commands/agent-context/document.tsx @@ -0,0 +1,117 @@ +import type { Command } from 'commander' +import { raw } from 'hono/html' +import type { FC, PropsWithChildren } from 'hono/jsx' +import type { CommandAgentContext } from '../../utils/agent-context.js' +import { agentContext as buildContext } from '../build/index.js' +import { agentContext as requestContext } from '../request/index.js' +import { agentContext as routesContext } from '../routes/index.js' + +const contexts: Record = { + routes: routesContext, + request: requestContext, + build: buildContext, +} + +// Markdown primitives. They emit raw text, so JSX never escapes it. + +const asArray = (children: unknown): unknown[] => + (Array.isArray(children) ? children : [children]).flat() + +const blocks = (children: unknown): string => + asArray(children) + .filter((child) => child !== null && child !== undefined && child !== false) + .map((child) => String(child).trim()) + .filter((text) => text.length > 0) + .join('\n\n') + +const Section: FC> = ({ + level, + title, + children, +}) => raw(`${'#'.repeat(level)} ${title}\n\n${blocks(children)}`) + +const P: FC = ({ children }) => raw(asArray(children).map(String).join('')) + +const Bullets: FC<{ items: string[] }> = ({ items }) => + raw(items.map((item) => `- ${item}`).join('\n')) + +const Steps: FC<{ items: string[] }> = ({ items }) => + raw(items.map((item, index) => `${index + 1}. ${item}`).join('\n')) + +const CodeBlock: FC<{ lang: string; lines: string[] }> = ({ lang, lines }) => + raw(['```' + lang, ...lines, '```'].join('\n')) + +const CommandDoc: FC<{ command: Command; context?: CommandAgentContext }> = ({ + command, + context, +}) => { + const args = command.registeredArguments + .map((arg) => (arg.required ? `<${arg.name()}>` : `[${arg.name()}]`)) + .join(' ') + + const optionLines = command.options.map((option) => { + const defaultValue = + option.defaultValue === undefined || option.defaultValue === false + ? '' + : ` (default: ${JSON.stringify(option.defaultValue)})` + return `\`${option.flags}\` — ${option.description}${defaultValue}` + }) + + return ( +
+

{command.description()}

+ {optionLines.length > 0 ?

Options:

: null} + {optionLines.length > 0 ? : null} + {context?.output ?

{`Output \`data\`: \`${context.output}\``}

: null} + {context?.errors?.length ? ( +

{`Error codes: ${context.errors.map((code) => `\`${code}\``).join(', ')}`}

+ ) : null} + {context?.examples?.length ?

Examples:

: null} + {context?.examples?.length ? : null} + {context?.notes?.length ? : null} +
+ ) +} + +const AgentContextDocument: FC<{ program: Command }> = ({ program }) => ( +
+

+ Hono CLI (`hono`) is a command-line tool for coding agents working on a + [Hono](https://hono.dev) app. It loads the app directly, so you can inspect and test it + without starting a server. +

+
+

Every command prints JSON to stdout:

+ +

+ Follow `error.hint` when a command fails. Logs go to stderr. Add `--plain` when a human + wants to read the output. +

+
+
+ ` — send a request to the app without a server', + 'After you change the app, run them again to verify', + '`hono build` — bundle the app (`--optimize` to reduce size)', + ]} + /> +
+
+ {program.commands + .filter((command) => command.name() !== 'agent-context') + .map((command) => ( + + ))} +
+
+) + +export const renderAgentContext = (program: Command): string => + String() diff --git a/src/commands/agent-context/index.ts b/src/commands/agent-context/index.ts index bf0fe04..8ae874a 100644 --- a/src/commands/agent-context/index.ts +++ b/src/commands/agent-context/index.ts @@ -1,37 +1,5 @@ import type { Command } from 'commander' -import type { CommandAgentContext } from '../../utils/agent-context.js' -import { agentContext as buildContext } from '../build/index.js' -import { agentContext as requestContext } from '../request/index.js' -import { agentContext as routesContext } from '../routes/index.js' - -const contexts: Record = { - routes: routesContext, - request: requestContext, - build: buildContext, -} - -const INTRO = `# Hono CLI - -Hono CLI (\`hono\`) is a command-line tool for coding agents working on a -[Hono](https://hono.dev) app. It loads the app directly, so you can -inspect and test it without starting a server. - -## Output contract - -Every command prints JSON to stdout: - -- Success: \`{ "ok": true, "data": ... }\` with exit code 0 -- Failure: \`{ "ok": false, "error": { "code", "message", "hint" } }\` with exit code 1 - -Follow \`error.hint\` when a command fails. Logs go to stderr. Add -\`--plain\` when a human wants to read the output. - -## Recommended workflow - -1. \`hono routes\` — get all routes of the app without reading the source -2. \`hono request -P \` — send a request to the app without a server -3. After you change the app, run them again to verify -4. \`hono build\` — bundle the app (\`--optimize\` to reduce size)` +import { renderAgentContext } from './document.js' export function agentContextCommand(program: Command) { program @@ -41,61 +9,3 @@ export function agentContextCommand(program: Command) { console.log(renderAgentContext(program)) }) } - -export const renderAgentContext = (program: Command): string => { - const sections: string[] = [INTRO, '## Commands'] - - for (const command of program.commands) { - const name = command.name() - if (name === 'agent-context') { - continue - } - - const args = command.registeredArguments - .map((arg) => (arg.required ? `<${arg.name()}>` : `[${arg.name()}]`)) - .join(' ') - const lines: string[] = [] - lines.push(`### hono ${name}${args ? ` ${args}` : ''}`) - lines.push('') - lines.push(command.description()) - - if (command.options.length > 0) { - lines.push('') - lines.push('Options:') - lines.push('') - for (const option of command.options) { - const defaultValue = - option.defaultValue === undefined || option.defaultValue === false - ? '' - : ` (default: ${JSON.stringify(option.defaultValue)})` - lines.push(`- \`${option.flags}\` — ${option.description}${defaultValue}`) - } - } - - const context = contexts[name] - if (context?.output) { - lines.push('') - lines.push(`Output \`data\`: \`${context.output}\``) - } - if (context?.errors && context.errors.length > 0) { - lines.push('') - lines.push(`Error codes: ${context.errors.map((e) => `\`${e}\``).join(', ')}`) - } - if (context?.examples && context.examples.length > 0) { - lines.push('') - lines.push('Examples:') - lines.push('') - lines.push('```bash') - lines.push(...context.examples) - lines.push('```') - } - if (context?.notes && context.notes.length > 0) { - lines.push('') - lines.push(...context.notes.map((note) => `- ${note}`)) - } - - sections.push(lines.join('\n')) - } - - return sections.join('\n\n') -} From 286b5b17cde4aa655a2e60dd67887ca381adfba9 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Tue, 18 Aug 2026 20:21:39 +0900 Subject: [PATCH 3/4] chore: explain the raw() usage in document.tsx --- src/commands/agent-context/document.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/commands/agent-context/document.tsx b/src/commands/agent-context/document.tsx index fdde0d8..49a5689 100644 --- a/src/commands/agent-context/document.tsx +++ b/src/commands/agent-context/document.tsx @@ -12,7 +12,9 @@ const contexts: Record = { build: buildContext, } -// Markdown primitives. They emit raw text, so JSX never escapes it. +// Markdown primitives. hono/jsx is an HTML renderer, but these components +// borrow only its composition. raw() keeps the text as-is, so Markdown +// characters are never HTML-escaped. const asArray = (children: unknown): unknown[] => (Array.isArray(children) ? children : [children]).flat() From 7a9402dfb568f2b915dd5cdd6f291d3de556db86 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Tue, 18 Aug 2026 20:25:37 +0900 Subject: [PATCH 4/4] refactor: build agent-context with markdown utils --- src/commands/agent-context/document.ts | 80 ++++++++++++++++ src/commands/agent-context/document.tsx | 119 ------------------------ src/utils/markdown.test.ts | 20 ++++ src/utils/markdown.ts | 23 +++++ 4 files changed, 123 insertions(+), 119 deletions(-) create mode 100644 src/commands/agent-context/document.ts delete mode 100644 src/commands/agent-context/document.tsx create mode 100644 src/utils/markdown.test.ts create mode 100644 src/utils/markdown.ts diff --git a/src/commands/agent-context/document.ts b/src/commands/agent-context/document.ts new file mode 100644 index 0000000..a0b070a --- /dev/null +++ b/src/commands/agent-context/document.ts @@ -0,0 +1,80 @@ +import type { Command } from 'commander' +import type { CommandAgentContext } from '../../utils/agent-context.js' +import { bullets, codeBlock, section, steps } from '../../utils/markdown.js' +import { agentContext as buildContext } from '../build/index.js' +import { agentContext as requestContext } from '../request/index.js' +import { agentContext as routesContext } from '../routes/index.js' + +const contexts: Record = { + routes: routesContext, + request: requestContext, + build: buildContext, +} + +const commandDoc = (command: Command, context?: CommandAgentContext): string => { + const args = command.registeredArguments + .map((arg) => (arg.required ? `<${arg.name()}>` : `[${arg.name()}]`)) + .join(' ') + + const optionLines = command.options.map((option) => { + const defaultValue = + option.defaultValue === undefined || option.defaultValue === false + ? '' + : ` (default: ${JSON.stringify(option.defaultValue)})` + return `\`${option.flags}\` — ${option.description}${defaultValue}` + }) + + const errors = context?.errors ?? [] + const examples = context?.examples ?? [] + const notes = context?.notes ?? [] + + return section( + 3, + `hono ${command.name()}${args ? ` ${args}` : ''}`, + command.description(), + optionLines.length > 0 && 'Options:', + optionLines.length > 0 && bullets(optionLines), + context?.output && `Output \`data\`: \`${context.output}\``, + errors.length > 0 && `Error codes: ${errors.map((code) => `\`${code}\``).join(', ')}`, + examples.length > 0 && 'Examples:', + examples.length > 0 && codeBlock('bash', examples), + notes.length > 0 && bullets(notes) + ) +} + +export const renderAgentContext = (program: Command): string => + section( + 1, + 'Hono CLI', + 'Hono CLI (`hono`) is a command-line tool for coding agents working on a ' + + '[Hono](https://hono.dev) app. It loads the app directly, so you can inspect ' + + 'and test it without starting a server.', + section( + 2, + 'Output contract', + 'Every command prints JSON to stdout:', + bullets([ + 'Success: `{ "ok": true, "data": ... }` with exit code 0', + 'Failure: `{ "ok": false, "error": { "code", "message", "hint" } }` with exit code 1', + ]), + 'Follow `error.hint` when a command fails. Logs go to stderr. Add `--plain` ' + + 'when a human wants to read the output.' + ), + section( + 2, + 'Recommended workflow', + steps([ + '`hono routes` — get all routes of the app without reading the source', + '`hono request -P ` — send a request to the app without a server', + 'After you change the app, run them again to verify', + '`hono build` — bundle the app (`--optimize` to reduce size)', + ]) + ), + section( + 2, + 'Commands', + ...program.commands + .filter((command) => command.name() !== 'agent-context') + .map((command) => commandDoc(command, contexts[command.name()])) + ) + ) diff --git a/src/commands/agent-context/document.tsx b/src/commands/agent-context/document.tsx deleted file mode 100644 index 49a5689..0000000 --- a/src/commands/agent-context/document.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import type { Command } from 'commander' -import { raw } from 'hono/html' -import type { FC, PropsWithChildren } from 'hono/jsx' -import type { CommandAgentContext } from '../../utils/agent-context.js' -import { agentContext as buildContext } from '../build/index.js' -import { agentContext as requestContext } from '../request/index.js' -import { agentContext as routesContext } from '../routes/index.js' - -const contexts: Record = { - routes: routesContext, - request: requestContext, - build: buildContext, -} - -// Markdown primitives. hono/jsx is an HTML renderer, but these components -// borrow only its composition. raw() keeps the text as-is, so Markdown -// characters are never HTML-escaped. - -const asArray = (children: unknown): unknown[] => - (Array.isArray(children) ? children : [children]).flat() - -const blocks = (children: unknown): string => - asArray(children) - .filter((child) => child !== null && child !== undefined && child !== false) - .map((child) => String(child).trim()) - .filter((text) => text.length > 0) - .join('\n\n') - -const Section: FC> = ({ - level, - title, - children, -}) => raw(`${'#'.repeat(level)} ${title}\n\n${blocks(children)}`) - -const P: FC = ({ children }) => raw(asArray(children).map(String).join('')) - -const Bullets: FC<{ items: string[] }> = ({ items }) => - raw(items.map((item) => `- ${item}`).join('\n')) - -const Steps: FC<{ items: string[] }> = ({ items }) => - raw(items.map((item, index) => `${index + 1}. ${item}`).join('\n')) - -const CodeBlock: FC<{ lang: string; lines: string[] }> = ({ lang, lines }) => - raw(['```' + lang, ...lines, '```'].join('\n')) - -const CommandDoc: FC<{ command: Command; context?: CommandAgentContext }> = ({ - command, - context, -}) => { - const args = command.registeredArguments - .map((arg) => (arg.required ? `<${arg.name()}>` : `[${arg.name()}]`)) - .join(' ') - - const optionLines = command.options.map((option) => { - const defaultValue = - option.defaultValue === undefined || option.defaultValue === false - ? '' - : ` (default: ${JSON.stringify(option.defaultValue)})` - return `\`${option.flags}\` — ${option.description}${defaultValue}` - }) - - return ( -
-

{command.description()}

- {optionLines.length > 0 ?

Options:

: null} - {optionLines.length > 0 ? : null} - {context?.output ?

{`Output \`data\`: \`${context.output}\``}

: null} - {context?.errors?.length ? ( -

{`Error codes: ${context.errors.map((code) => `\`${code}\``).join(', ')}`}

- ) : null} - {context?.examples?.length ?

Examples:

: null} - {context?.examples?.length ? : null} - {context?.notes?.length ? : null} -
- ) -} - -const AgentContextDocument: FC<{ program: Command }> = ({ program }) => ( -
-

- Hono CLI (`hono`) is a command-line tool for coding agents working on a - [Hono](https://hono.dev) app. It loads the app directly, so you can inspect and test it - without starting a server. -

-
-

Every command prints JSON to stdout:

- -

- Follow `error.hint` when a command fails. Logs go to stderr. Add `--plain` when a human - wants to read the output. -

-
-
- ` — send a request to the app without a server', - 'After you change the app, run them again to verify', - '`hono build` — bundle the app (`--optimize` to reduce size)', - ]} - /> -
-
- {program.commands - .filter((command) => command.name() !== 'agent-context') - .map((command) => ( - - ))} -
-
-) - -export const renderAgentContext = (program: Command): string => - String() diff --git a/src/utils/markdown.test.ts b/src/utils/markdown.test.ts new file mode 100644 index 0000000..57b0278 --- /dev/null +++ b/src/utils/markdown.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect } from 'vitest' +import { section, bullets, steps, codeBlock } from './markdown' + +describe('markdown', () => { + it('should join blocks with blank lines and skip conditional blocks', () => { + const result = section(1, 'Title', 'First.', false, null, undefined, 'Second.') + expect(result).toBe('# Title\n\nFirst.\n\nSecond.') + }) + + it('should nest sections', () => { + const result = section(1, 'Top', section(2, 'Sub', 'Body.')) + expect(result).toBe('# Top\n\n## Sub\n\nBody.') + }) + + it('should render bullets, steps, and code blocks', () => { + expect(bullets(['a', 'b'])).toBe('- a\n- b') + expect(steps(['a', 'b'])).toBe('1. a\n2. b') + expect(codeBlock('bash', ['echo hi'])).toBe('```bash\necho hi\n```') + }) +}) diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts new file mode 100644 index 0000000..cae482c --- /dev/null +++ b/src/utils/markdown.ts @@ -0,0 +1,23 @@ +// Small helpers to build a Markdown document. A block is a string; +// false/null/undefined blocks are skipped, so conditional blocks can be +// written inline. + +export type Block = string | false | null | undefined + +const joinBlocks = (blocks: Block[]): string => + blocks + .filter((block): block is string => typeof block === 'string') + .map((block) => block.trim()) + .filter((block) => block.length > 0) + .join('\n\n') + +export const section = (level: 1 | 2 | 3, title: string, ...blocks: Block[]): string => + `${'#'.repeat(level)} ${title}\n\n${joinBlocks(blocks)}` + +export const bullets = (items: string[]): string => items.map((item) => `- ${item}`).join('\n') + +export const steps = (items: string[]): string => + items.map((item, index) => `${index + 1}. ${item}`).join('\n') + +export const codeBlock = (lang: string, lines: string[]): string => + ['```' + lang, ...lines, '```'].join('\n')