Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions bun.lock
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "@hono/cli",
"dependencies": {
"@hono/node-server": "^1.19.5",
"commander": "^14.0.1",
"@takojs/tako": "^1.4.0",
"esbuild": "^0.25.10",
"hono": "^4.9.12",
},
Expand Down Expand Up @@ -227,6 +228,8 @@

"@sindresorhus/merge-streams": ["@sindresorhus/[email protected]", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="],

"@takojs/tako": ["@takojs/[email protected]", "", {}, "sha512-nV5Y2dSFgTA/KAvCdsXYWGKtI6yox590eBeNomyJYoajkUFYpKJqElsG6VHNxTlBPVrZr9RP93+MBgCjcXTAKg=="],

"@tybys/wasm-util": ["@tybys/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],

"@types/chai": ["@types/[email protected]", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
Expand Down Expand Up @@ -381,7 +384,7 @@

"color-name": ["[email protected]", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],

"commander": ["commander@14.0.1", "", {}, "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A=="],
"commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],

"comment-parser": ["[email protected]", "", {}, "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg=="],

Expand Down Expand Up @@ -1111,8 +1114,6 @@

"strip-ansi-cjs/ansi-regex": ["[email protected]", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],

"sucrase/commander": ["[email protected]", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],

"update-notifier/chalk": ["[email protected]", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],

"widest-line/string-width": ["[email protected]", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "@hono/cli",
"version": "0.1.1",
"description": "CLI for Hono",
"type": "module",
"bin": {
"hono": "dist/cli.js"
Expand Down Expand Up @@ -34,7 +35,7 @@
"homepage": "https://hono.dev",
"dependencies": {
"@hono/node-server": "^1.19.5",
"commander": "^14.0.1",
"@takojs/tako": "^1.4.0",
"esbuild": "^0.25.10",
"hono": "^4.9.12"
},
Expand Down
47 changes: 21 additions & 26 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,26 @@
import { Command } from 'commander'
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { docsCommand } from './commands/docs/index.js'
import { optimizeCommand } from './commands/optimize/index.js'
import { requestCommand } from './commands/request/index.js'
import { searchCommand } from './commands/search/index.js'
import { serveCommand } from './commands/serve/index.js'
import { Tako } from '@takojs/tako'
import pkg from '../package.json' with { type: 'json' }
import { docsArgs, docsCommand, docsValidation } from './commands/docs/index.js'
import { optimizeArgs, optimizeCommand, optimizeValidation } from './commands/optimize/index.js'
import { requestArgs, requestCommand, requestValidation } from './commands/request/index.js'
import { searchArgs, searchCommand, searchValidation } from './commands/search/index.js'
import { serveArgs, serveCommand, serveValidation } from './commands/serve/index.js'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const rootArgs = {
metadata: {
cliName: 'hono',
version: pkg.version,
help: pkg.description,
},
}

// Read version from package.json
const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'))

const program = new Command()

program
.name('hono')
.description('CLI for Hono')
.version(packageJson.version, '-v, --version', 'display version number')
const tako = new Tako()

// Register commands
docsCommand(program)
optimizeCommand(program)
searchCommand(program)
requestCommand(program)
serveCommand(program)
tako.command('docs', docsArgs, docsValidation, docsCommand)
tako.command('optimize', optimizeArgs, optimizeValidation, optimizeCommand)
tako.command('request', requestArgs, requestValidation, requestCommand)
tako.command('search', searchArgs, searchValidation, searchCommand)
tako.command('serve', serveArgs, serveValidation, serveCommand)

program.parse()
await tako.cli(rootArgs)
37 changes: 19 additions & 18 deletions src/commands/docs/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { Command } from 'commander'
import { Tako } from '@takojs/tako'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { Buffer } from 'node:buffer'
import * as process from 'node:process'
import { docsArgs, docsCommand, docsValidation } from './index.js'

// Mock fetch
global.fetch = vi.fn()

import { docsCommand } from './index.js'
globalThis.fetch = vi.fn()

describe('docsCommand', () => {
let program: Command
let tako: Tako
let consoleLogSpy: ReturnType<typeof vi.spyOn>
let consoleErrorSpy: ReturnType<typeof vi.spyOn>
let originalIsTTY: boolean | undefined

beforeEach(() => {
program = new Command()
docsCommand(program)
tako = new Tako()
tako.command('docs', docsArgs, docsValidation, docsCommand)
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})

Expand Down Expand Up @@ -46,7 +47,7 @@ describe('docsCommand', () => {
text: () => Promise.resolve(mockContent),
} as Response)

await program.parseAsync(['node', 'test', 'docs'])
await tako.cli({ config: { args: ['docs'] } })

expect(fetch).toHaveBeenCalledWith('https://hono.dev/llms.txt')
expect(consoleLogSpy).toHaveBeenCalledWith('Fetching Hono documentation...')
Expand All @@ -61,7 +62,7 @@ describe('docsCommand', () => {
text: () => Promise.resolve(mockMarkdown),
} as Response)

await program.parseAsync(['node', 'test', 'docs', '/docs/concepts/stacks'])
await tako.cli({ config: { args: ['docs', '/docs/concepts/stacks'] } })

expect(fetch).toHaveBeenCalledWith(
'https://raw.githubusercontent.com/honojs/website/refs/heads/main/docs/concepts/stacks.md'
Expand All @@ -80,7 +81,7 @@ describe('docsCommand', () => {
text: () => Promise.resolve(mockMarkdown),
} as Response)

await program.parseAsync(['node', 'test', 'docs', '/examples/stytch-auth'])
await tako.cli({ config: { args: ['docs', '/examples/stytch-auth'] } })

expect(fetch).toHaveBeenCalledWith(
'https://raw.githubusercontent.com/honojs/website/refs/heads/main/examples/stytch-auth.md'
Expand All @@ -99,7 +100,7 @@ describe('docsCommand', () => {
text: () => Promise.resolve(mockMarkdown),
} as Response)

await program.parseAsync(['node', 'test', 'docs', 'examples/basic'])
await tako.cli({ config: { args: ['docs', 'examples/basic'] } })

expect(fetch).toHaveBeenCalledWith(
'https://raw.githubusercontent.com/honojs/website/refs/heads/main/examples/basic.md'
Expand All @@ -115,7 +116,7 @@ describe('docsCommand', () => {
statusText: 'Not Found',
} as Response)

await program.parseAsync(['node', 'test', 'docs'])
await tako.cli({ config: { args: ['docs'] } })

expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error fetching documentation:',
Expand All @@ -131,7 +132,7 @@ describe('docsCommand', () => {
statusText: 'Not Found',
} as Response)

await program.parseAsync(['node', 'test', 'docs', '/docs/concepts/motivation'])
await tako.cli({ config: { args: ['docs', '/docs/concepts/motivation'] } })

expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error fetching documentation:',
Expand All @@ -149,7 +150,7 @@ describe('docsCommand', () => {
statusText: 'Not Found',
} as Response)

await program.parseAsync(['node', 'test', 'docs', '/examples/stytch-auth'])
await tako.cli({ config: { args: ['docs', '/examples/stytch-auth'] } })

expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error fetching documentation:',
Expand All @@ -164,7 +165,7 @@ describe('docsCommand', () => {
const networkError = new Error('Network error')
vi.mocked(fetch).mockRejectedValue(networkError)

await program.parseAsync(['node', 'test', 'docs'])
await tako.cli({ config: { args: ['docs'] } })

expect(consoleErrorSpy).toHaveBeenCalledWith('Error fetching documentation:', 'Network error')
expect(consoleLogSpy).toHaveBeenCalledWith('\nPlease visit: https://hono.dev/docs')
Expand All @@ -189,7 +190,7 @@ describe('docsCommand', () => {
text: () => Promise.resolve(mockMarkdown),
} as Response)

await program.parseAsync(['node', 'test', 'docs'])
await tako.cli({ config: { args: ['docs'] } })

expect(fetch).toHaveBeenCalledWith(
'https://raw.githubusercontent.com/honojs/website/refs/heads/main/docs/concepts/middleware.md'
Expand Down Expand Up @@ -222,7 +223,7 @@ describe('docsCommand', () => {
text: () => Promise.resolve(mockMarkdown),
} as Response)

await program.parseAsync(['node', 'test', 'docs'])
await tako.cli({ config: { args: ['docs'] } })

expect(fetch).toHaveBeenCalledWith(
'https://raw.githubusercontent.com/honojs/website/refs/heads/main/docs/api/context.md'
Expand All @@ -246,7 +247,7 @@ describe('docsCommand', () => {
text: () => Promise.resolve(mockContent),
} as Response)

await program.parseAsync(['node', 'test', 'docs'])
await tako.cli({ config: { args: ['docs'] } })

expect(fetch).toHaveBeenCalledWith('https://hono.dev/llms.txt')
expect(consoleLogSpy).toHaveBeenCalledWith('Fetching Hono documentation...')
Expand Down
129 changes: 76 additions & 53 deletions src/commands/docs/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Command } from 'commander'
import type { Tako, TakoArgs, TakoHandler } from '@takojs/tako'
import { Buffer } from 'node:buffer'
import * as process from 'node:process'

async function fetchAndDisplayContent(url: string, fallbackUrl?: string): Promise<void> {
async function fetchAndDisplayContent(c: Tako, url: string, fallbackUrl?: string): Promise<void> {
try {
const response = await fetch(url)

Expand All @@ -9,64 +11,85 @@ async function fetchAndDisplayContent(url: string, fallbackUrl?: string): Promis
}

const content = await response.text()
console.log('\n' + content)
c.print({ message: '\n' + content })
} catch (error) {
console.error(
'Error fetching documentation:',
error instanceof Error ? error.message : String(error)
)
console.log(`\nPlease visit: ${fallbackUrl || 'https://hono.dev/docs'}`)
c.print({
message: [
'Error fetching documentation:',
error instanceof Error ? error.message : String(error),
],
style: 'red',
level: 'error',
})
c.print({ message: `\nPlease visit: ${fallbackUrl || 'https://hono.dev/docs'}` })
}
}

async function getPath(c: Tako): Promise<string | undefined> {
const pathFromArgs = c.scriptArgs.positionals[0]
if (pathFromArgs) {
return pathFromArgs
}

// If no path provided, check for stdin input
// Check if stdin is piped (not a TTY)
if (process.stdin.isTTY) {
return
}

try {
const chunks: Buffer[] = []
for await (const chunk of process.stdin) {
chunks.push(chunk)
}
const stdinInput = Buffer.concat(chunks).toString().trim()
if (!stdinInput) {
return
}
// Remove quotes if present (handles jq output without -r flag)
return stdinInput.replace(/^["'](.*)["']$/, '$1')
} catch (error) {
c.print({
message: [
'Error reading from stdin:',
error instanceof Error ? error.message : String(error),
],
style: 'red',
level: 'error',
})
return
}
}

export function docsCommand(program: Command) {
program
.command('docs')
.argument(
'[path]',
'Documentation path (e.g., /docs/concepts/motivation, /examples/stytch-auth)',
''
)
.description('Display Hono documentation')
.action(async (path: string) => {
let finalPath = path
export const docsArgs: TakoArgs = {
metadata: {
help: 'Display Hono documentation',
placeholder: '[path]',
},
}

// If no path provided, check for stdin input
if (!path) {
// Check if stdin is piped (not a TTY)
if (!process.stdin.isTTY) {
try {
const chunks: Buffer[] = []
for await (const chunk of process.stdin) {
chunks.push(chunk)
}
const stdinInput = Buffer.concat(chunks).toString().trim()
if (stdinInput) {
// Remove quotes if present (handles jq output without -r flag)
finalPath = stdinInput.replace(/^["'](.*)["']$/, '$1')
}
} catch (error) {
console.error('Error reading from stdin:', error)
}
}
export const docsValidation: TakoHandler = async (_c, next) => {
await next()
}

export const docsCommand: TakoHandler = async (c) => {
const finalPath = await getPath(c)

// If still no path, fetch llms.txt
if (!finalPath) {
console.log('Fetching Hono documentation...')
await fetchAndDisplayContent('https://hono.dev/llms.txt')
return
}
}
if (!finalPath) {
// If still no path, fetch llms.txt
c.print({ message: 'Fetching Hono documentation...' })
await fetchAndDisplayContent(c, 'https://hono.dev/llms.txt')
return
}

// Ensure path starts with /
const normalizedPath = finalPath.startsWith('/') ? finalPath : `/${finalPath}`
// Ensure path starts with /
const normalizedPath = finalPath.startsWith('/') ? finalPath : `/${finalPath}`

// Remove leading slash to get the GitHub path
const basePath = normalizedPath.slice(1) // Remove leading slash
const markdownUrl = `https://raw.githubusercontent.com/honojs/website/refs/heads/main/${basePath}.md`
const webUrl = `https://hono.dev${normalizedPath}`
// Remove leading slash to get the GitHub path
const basePath = normalizedPath.slice(1) // Remove leading slash
const markdownUrl = `https://raw.githubusercontent.com/honojs/website/refs/heads/main/${basePath}.md`
const webUrl = `https://hono.dev${normalizedPath}`

console.log(`Fetching Hono documentation for ${finalPath}...`)
await fetchAndDisplayContent(markdownUrl, webUrl)
})
c.print({ message: `Fetching Hono documentation for ${finalPath}...` })
await fetchAndDisplayContent(c, markdownUrl, webUrl)
}
Loading