diff --git a/.changeset/cli-missing-wrangler.md b/.changeset/cli-missing-wrangler.md new file mode 100644 index 0000000..f8b28eb --- /dev/null +++ b/.changeset/cli-missing-wrangler.md @@ -0,0 +1,5 @@ +--- +"@mieweb/cli": patch +--- + +Don't crash with a stack trace when wrangler isn't installed: wrangler-delegated commands on the cloudflare target now print "wrangler binary not found…" and exit 1, and `mieweb --version` prints the mieweb version and exits 0 on every target (it no longer errors on non-cloudflare targets). diff --git a/packages/cli/src/cloudflare.mjs b/packages/cli/src/cloudflare.mjs index ab64716..2d88f84 100644 --- a/packages/cli/src/cloudflare.mjs +++ b/packages/cli/src/cloudflare.mjs @@ -9,11 +9,19 @@ import { runWrangler } from '@mieweb/deploy-wrangler'; * reference provider does (MIEWEB_REAL_WRANGLER, else the project's wrangler * via the active package manager). * + * Never throws: a missing wrangler (or a failed spawn) is reported on stderr + * and returned as exit code 1. + * * @param {string[]} args arguments to forward to wrangler * @param {{ cwd?: string }} [opts] - * @returns {Promise} wrangler's exit code (1 if killed by a signal) + * @returns {Promise} wrangler's exit code (1 if it couldn't run or was killed) */ export async function delegateToWrangler(args, opts = {}) { - const { code } = await runWrangler(args, { cwd: opts.cwd ?? process.cwd() }); - return code ?? 1; + try { + const { code } = await runWrangler(args, { cwd: opts.cwd ?? process.cwd() }); + return code ?? 1; + } catch (err) { + console.error(`mieweb: ${err instanceof Error ? err.message : String(err)}`); + return 1; + } } diff --git a/packages/cli/src/index.mjs b/packages/cli/src/index.mjs index 84e38bc..2b23f73 100755 --- a/packages/cli/src/index.mjs +++ b/packages/cli/src/index.mjs @@ -69,7 +69,8 @@ async function main(argv) { // Version flags: always show the mieweb version first. On cloudflare we then // let wrangler print its own version below; other targets show mieweb only. - if (args[0] === '--version' || args[0] === '-v' || args[0] === '-V') { + const versionFlag = args[0] === '--version' || args[0] === '-v' || args[0] === '-V'; + if (versionFlag) { process.stdout.write(`mieweb ${miewebVersion()}\n`); } @@ -81,6 +82,9 @@ async function main(argv) { const config = loadConfig({ overrideTarget }); + // Only cloudflare has a second version to show (wrangler's, below). + if (versionFlag && config.target !== 'cloudflare') return 0; + // Container image plumbing (build once, skopeo copy — container-plan.md M2). // `images push` on the cloudflare target delegates to wrangler's managed // registry; every other target goes through skopeo to its own registry. @@ -126,7 +130,9 @@ async function main(argv) { if (config.target === 'cloudflare') { // Reference path for non-provider commands: hand to wrangler untouched. - return delegateToWrangler(args, { cwd: config.root }); + const code = await delegateToWrangler(args, { cwd: config.root }); + // `--version` already printed mieweb's version; wrangler's is a bonus. + return versionFlag ? 0 : code; } // Node "host" targets run the unchanged worker via the host harness. `local` diff --git a/packages/cli/test/provider-core.test.mjs b/packages/cli/test/provider-core.test.mjs index 47f3770..8d7c37e 100644 --- a/packages/cli/test/provider-core.test.mjs +++ b/packages/cli/test/provider-core.test.mjs @@ -65,3 +65,45 @@ test('delegateToWrangler returns wrangler’s exit code', async (t) => { t.after(() => delete process.env.MIEWEB_REAL_WRANGLER); assert.equal(await delegateToWrangler(['d1', 'migrations', 'apply'], { cwd: dir }), 3); }); + +test('delegateToWrangler reports a missing wrangler instead of throwing', async (t) => { + const dir = tmp(t); + writeFileSync(join(dir, 'package.json'), '{"name":"app"}'); + const saved = process.env.MIEWEB_REAL_WRANGLER; + delete process.env.MIEWEB_REAL_WRANGLER; + t.after(() => { + if (saved !== undefined) process.env.MIEWEB_REAL_WRANGLER = saved; + }); + const errors = []; + t.mock.method(console, 'error', (m) => errors.push(m)); + assert.equal(await delegateToWrangler(['d1', 'list'], { cwd: dir }), 1); + assert.match(errors.join('\n'), /wrangler binary not found/); +}); + +test('`mieweb --version` works without wrangler installed', async (t) => { + const { execFileSync } = await import('node:child_process'); + const dir = tmp(t); + writeFileSync(join(dir, 'package.json'), '{"name":"app"}'); + const env = { ...process.env }; + delete env.MIEWEB_REAL_WRANGLER; + const out = execFileSync(process.execPath, [new URL('../src/index.mjs', import.meta.url).pathname, '--version'], { + cwd: dir, + env, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + assert.match(out, /^mieweb \d+\.\d+\.\d+/); +}); + +test('`mieweb --version` on a non-cloudflare target prints only the version', async (t) => { + const { execFileSync } = await import('node:child_process'); + const dir = tmp(t); + writeFileSync(join(dir, 'package.json'), '{"name":"app"}'); + writeFileSync(join(dir, 'mieweb.jsonc'), '{"target":"mieweb"}'); + const out = execFileSync(process.execPath, [new URL('../src/index.mjs', import.meta.url).pathname, '--version'], { + cwd: dir, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + assert.match(out, /^mieweb \d+\.\d+\.\d+\n$/); +});