Skip to content
Merged
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,7 @@ It writes a "Hono CLI" block to `AGENTS.md`, or to `CLAUDE.md` when only that ex

The block has extra lines for the framework: `void` (`package.json` depends on `void`: use the `hono-void` skill from [honojs/skills](https://github.com/honojs/skills)), `cloudflare-workers` (a wrangler config or `cloudflare.config.ts`), or `hono`.

Without `@hono/cli` in `package.json`, it runs `<package manager> add -D @hono/cli`. The package manager comes from the lockfile, as in `init`.
Without `@hono/cli` in `package.json`, it runs `<package manager> add -D @hono/cli@<this version>`. The package manager comes from the lockfile, as in `init`, and the commands in the block use it too: `pnpm exec hono` in a pnpm project, `npx hono` with npm. The exact version lets pnpm add a release candidate that is younger than its `minimumReleaseAge`.

**Output:**

Expand All @@ -566,7 +566,7 @@ Without `@hono/cli` in `package.json`, it runs `<package manager> add -D @hono/c
"framework": "void",
"file": "AGENTS.md",
"action": "appended",
"added": "@hono/cli@^1.0.0"
"added": "@hono/cli@1.0.0"
}
}
```
Expand Down
34 changes: 27 additions & 7 deletions src/commands/agent-init/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,44 @@ export type Framework = 'void' | 'cloudflare-workers' | 'hono'
const START = '<!-- hono-cli:start -->'
const END = '<!-- hono-cli:end -->'

// {run} runs a package of the project, {dlx} one from the registry
const COMMON = [
'Verify changes with Hono CLI instead of a browser or curl: `npx hono request <path>`, `npx hono batch -` for flows. Run `npx hono --help` first.',
'Verify changes with Hono CLI instead of a browser or curl: `{run} hono request <path>`, `{run} hono batch -` for flows. Run `{run} hono --help` first.',
]

const FRAMEWORK_LINES: Record<Framework, string[]> = {
void: [
'This is a Void app. To check pages and page actions, use the hono-void skill. If it is missing, install it: `npx skills add honojs/skills --skill hono-void -y`.',
'This is a Void app. To check pages and page actions, use the hono-void skill. If it is missing, install it: `{dlx} skills add honojs/skills --skill hono-void -y`.',
],
'cloudflare-workers': [
'`c.env` has the local bindings (KV, D1, R2, vars). Add `--runtime workerd` to run in the real Workers runtime.',
],
hono: [],
}

export const renderBlock = (framework: Framework): string =>
[
/**
* The commands of each package manager. A project that names pnpm in
* devEngines makes npm (and so npx) refuse to run.
*/
const RUNNERS: Record<string, { run: string; dlx: string }> = {
npm: { run: 'npx', dlx: 'npx' },
pnpm: { run: 'pnpm exec', dlx: 'pnpm dlx' },
yarn: { run: 'yarn', dlx: 'yarn dlx' },
bun: { run: 'bunx', dlx: 'bunx' },
}

export const renderBlock = (framework: Framework, packageManager = 'npm'): string => {
const { run, dlx } = RUNNERS[packageManager] ?? RUNNERS.npm
return [
START,
'## Hono CLI',
'',
...[...COMMON, ...FRAMEWORK_LINES[framework]].map((line) => `- ${line}`),
...[...COMMON, ...FRAMEWORK_LINES[framework]].map(
(line) => `- ${line.replaceAll('{run}', run).replaceAll('{dlx}', dlx)}`
),
END,
].join('\n')
}

export interface WriteResult {
file: string
Expand All @@ -38,13 +54,17 @@ export interface WriteResult {
* Write the block to AGENTS.md, or to CLAUDE.md when only that exists.
* A block from an earlier run is replaced in place.
*/
export const writeBlock = (dir: string, framework: Framework): WriteResult => {
export const writeBlock = (
dir: string,
framework: Framework,
packageManager = 'npm'
): WriteResult => {
const file =
!existsSync(join(dir, 'AGENTS.md')) && existsSync(join(dir, 'CLAUDE.md'))
? 'CLAUDE.md'
: 'AGENTS.md'
const path = join(dir, file)
const block = renderBlock(framework)
const block = renderBlock(framework, packageManager)
if (!existsSync(path)) {
writeFileSync(path, `${block}\n`)
return { file, action: 'created' }
Expand Down
16 changes: 9 additions & 7 deletions src/commands/agent-init/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,18 @@ describe('agentInitCommand', () => {
framework: 'void',
file: 'AGENTS.md',
action: 'created',
added: '@hono/cli@^1.0.0',
added: '@hono/cli@1.0.0',
})
expect(runPackageManager).toHaveBeenCalledWith(
'pnpm',
['add', '-D', '@hono/cli@^1.0.0'],
['add', '-D', '@hono/cli@1.0.0'],
process.cwd()
)
expect(readFileSync('AGENTS.md', 'utf-8')).toContain(
'npx skills add honojs/skills --skill hono-void -y'
)
// pnpm project: no npx, which npm refuses when devEngines names pnpm
const agents = readFileSync('AGENTS.md', 'utf-8')
expect(agents).toContain('`pnpm exec hono request <path>`')
expect(agents).toContain('`pnpm dlx skills add honojs/skills --skill hono-void -y`')
expect(agents).not.toContain('npx')
})

it('should detect a Cloudflare Workers app', async () => {
Expand All @@ -75,15 +77,15 @@ describe('agentInitCommand', () => {
packageJson({})
const output = await run('--no-install')
expect(runPackageManager).not.toHaveBeenCalled()
expect(output.data.suggestions).toEqual(['Add Hono CLI: npm add -D @hono/cli@^1.0.0'])
expect(output.data.suggestions).toEqual(['Add Hono CLI: npm add -D @hono/cli@1.0.0'])
})

it('should keep the file and say how to add when adding fails', async () => {
packageJson({})
vi.mocked(runPackageManager).mockRejectedValueOnce(new Error('exited with 1'))
const output = await run()
expect(output.error.code).toBe('INSTALL_FAILED')
expect(output.error.suggestions).toEqual(['Run it yourself: npm add -D @hono/cli@^1.0.0'])
expect(output.error.suggestions).toEqual(['Run it yourself: npm add -D @hono/cli@1.0.0'])
expect(readFileSync('AGENTS.md', 'utf-8')).toContain('## Hono CLI')
})
})
12 changes: 8 additions & 4 deletions src/commands/agent-init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import { writeBlock } from './block.js'

const help: CommandHelp = {
output:
'{ "framework": "void", "file": "AGENTS.md", "action": "created", "added": "@hono/cli@^1.0.0" }',
'{ "framework": "void", "file": "AGENTS.md", "action": "created", "added": "@hono/cli@1.0.0" }',
examples: ['npx @hono/cli@next agent-init', 'hono agent-init --no-install'],
notes: [
'Tells coding agents to verify changes with Hono CLI: writes a "Hono CLI" block to AGENTS.md (CLAUDE.md when only that exists).',
'Running it again replaces the block in place.',
'The block has extra lines for the framework: void (package.json depends on void), cloudflare-workers (a wrangler config or cloudflare.config.ts), or hono.',
'Adds @hono/cli to devDependencies with the package manager of the lockfile. Skip it with --no-install.',
'The commands in the block use the same package manager: pnpm exec hono in a pnpm project, npx hono with npm.',
],
}

Expand Down Expand Up @@ -58,11 +59,14 @@ export function agentInitCommand(program: Command) {
handleErrors(async (options: AgentInitOptions, command: Command) => {
const dir = process.cwd()
const framework = detectFramework(dir)
const written = writeBlock(dir, framework)

const version = command.parent?.version()
const spec = version ? `@hono/cli@^${version}` : '@hono/cli'
const packageManager = detectPackageManager(dir, process.env.npm_config_user_agent)
const written = writeBlock(dir, framework, packageManager)

// The exact version: pnpm skips a release younger than its
// minimumReleaseAge for a range, but adds an exclude for an exact one
const version = command.parent?.version()
const spec = version ? `@hono/cli@${version}` : '@hono/cli'
const addCommand = [packageManager, 'add', '-D', spec]
const needsCli = !hasCli(dir)
if (needsCli && options.install) {
Expand Down
Loading