From 1cc56261d2df530519736f2b0fdc10b522e52222 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Fri, 16 Oct 2026 02:35:14 +0900 Subject: [PATCH] fix(agent-init): use the project's package manager and pin the version --- README.md | 4 ++-- src/commands/agent-init/block.ts | 34 +++++++++++++++++++++------ src/commands/agent-init/index.test.ts | 16 +++++++------ src/commands/agent-init/index.ts | 12 ++++++---- 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 51c30d6..f88a2fd 100644 --- a/README.md +++ b/README.md @@ -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 ` add -D @hono/cli`. The package manager comes from the lockfile, as in `init`. +Without `@hono/cli` in `package.json`, it runs ` add -D @hono/cli@`. 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:** @@ -566,7 +566,7 @@ Without `@hono/cli` in `package.json`, it runs ` add -D @hono/c "framework": "void", "file": "AGENTS.md", "action": "appended", - "added": "@hono/cli@^1.0.0" + "added": "@hono/cli@1.0.0" } } ``` diff --git a/src/commands/agent-init/block.ts b/src/commands/agent-init/block.ts index 8981b74..7e9afb4 100644 --- a/src/commands/agent-init/block.ts +++ b/src/commands/agent-init/block.ts @@ -6,13 +6,14 @@ export type Framework = 'void' | 'cloudflare-workers' | 'hono' const START = '' const 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 `, `npx hono batch -` for flows. Run `npx hono --help` first.', + 'Verify changes with Hono CLI instead of a browser or curl: `{run} hono request `, `{run} hono batch -` for flows. Run `{run} hono --help` first.', ] const FRAMEWORK_LINES: Record = { 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.', @@ -20,14 +21,29 @@ const FRAMEWORK_LINES: Record = { 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 = { + 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 @@ -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' } diff --git a/src/commands/agent-init/index.test.ts b/src/commands/agent-init/index.test.ts index b0c38d8..0f5afda 100644 --- a/src/commands/agent-init/index.test.ts +++ b/src/commands/agent-init/index.test.ts @@ -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 `') + 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 () => { @@ -75,7 +77,7 @@ 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 () => { @@ -83,7 +85,7 @@ describe('agentInitCommand', () => { 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') }) }) diff --git a/src/commands/agent-init/index.ts b/src/commands/agent-init/index.ts index c66d9ba..a47a6ee 100644 --- a/src/commands/agent-init/index.ts +++ b/src/commands/agent-init/index.ts @@ -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.', ], } @@ -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) {