From 4fc34441baab4eea078ffa130a3a776f0aef3430 Mon Sep 17 00:00:00 2001 From: kjellbergzoey Date: Sun, 7 Jun 2026 17:14:30 +0000 Subject: [PATCH] fix: correct wpcli invocation (--profile cli + safe arg passing in kiqr wp) (a) buildWpCliArgs now inserts `--profile cli` before the `run` subcommand. The wpcli compose service declares `profiles: ['cli']` (BitnamiRuntimeProvider), and on Docker Compose v2 a profiled service targeted by `run` may not start unless its profile is enabled. The flag is positioned before `run` so it takes effect, and is safe regardless of the Compose version. This also covers `kiqr db dump`/`db restore`, which go through buildWpCliArgs via spawnWpCli. (b) `kiqr wp` previously built a shell string and passed it to execSync, which mangled arguments containing spaces/quotes/special characters (e.g. `--post_title="Hello World"`). It now uses a new array-based, shell-free helper (runWpCliInherit) that spawns docker with an args array and inherits stdio so output still streams to the terminal. The raw process args are forwarded verbatim, and the no-args `--help` path is preserved. The readProjectConfig() call is wrapped in try/catch so an invalid config surfaces a clear message instead of crashing. Tests for buildWpCliArgs are updated to expect `--profile cli`, assert the flag precedes `run`, and confirm args are passed verbatim. Co-Authored-By: Claude Opus 4.8 --- src/commands/wp.tsx | 34 ++++++++++++++++------------------ src/lib/wpcli.ts | 26 +++++++++++++++++++++++++- tests/lib/wpcli.test.ts | 23 ++++++++++++++++++++++- 3 files changed, 63 insertions(+), 20 deletions(-) diff --git a/src/commands/wp.tsx b/src/commands/wp.tsx index 5167df5..59f83a7 100644 --- a/src/commands/wp.tsx +++ b/src/commands/wp.tsx @@ -1,4 +1,3 @@ -import {execSync} from 'node:child_process'; import path from 'node:path'; import {Box, Text, useApp} from 'ink'; import {argument} from 'pastel'; @@ -6,6 +5,7 @@ import {useEffect} from 'react'; import zod from 'zod'; import {readProjectConfig} from '../lib/config.js'; import {getProjectRuntimeDir} from '../lib/paths.js'; +import {runWpCliInherit} from '../lib/wpcli.js'; export const description = 'Run a WP-CLI command'; @@ -26,7 +26,14 @@ export default function Wp(_props: Props) { const {exit} = useApp(); useEffect(() => { - const pc = readProjectConfig(); + let pc: ReturnType; + try { + pc = readProjectConfig(); + } catch (err) { + console.error(err instanceof Error ? err.message : String(err)); + exit(new Error()); + return; + } if (!pc) { console.error('This project is not initialized. Run "kiqr init" first.'); exit(new Error()); @@ -36,25 +43,16 @@ export default function Wp(_props: Props) { const runtimeDir = getProjectRuntimeDir(pc.project_id); const composePath = path.join(runtimeDir, 'compose.yaml'); - // Grab everything after "wp" from the raw process args + // Grab everything after "wp" from the raw process args. Each element is a + // single argument, so values with spaces/quotes (e.g. + // --post_title="Hello World") are preserved when passed as an array. const wpIndex = process.argv.indexOf('wp'); const rawArgs = wpIndex >= 0 ? process.argv.slice(wpIndex + 1) : []; + const wpArgs = rawArgs.length === 0 ? ['--help'] : rawArgs; - if (rawArgs.length === 0) { - execSync(`docker compose -f "${composePath}" run --rm wpcli --help`, { - stdio: 'inherit', - }); - exit(); - return; - } - - try { - execSync(`docker compose -f "${composePath}" run --rm wpcli ${rawArgs.join(' ')}`, { - stdio: 'inherit', - }); - } catch { - // WP-CLI exits with non-zero for errors — already printed to stderr - } + // Use the array-based, shell-free spawn so arguments are passed verbatim. + // WP-CLI exits non-zero on errors, which it has already printed to stderr. + runWpCliInherit(composePath, wpArgs); exit(); }, []); diff --git a/src/lib/wpcli.ts b/src/lib/wpcli.ts index 3a182a4..bcd4533 100644 --- a/src/lib/wpcli.ts +++ b/src/lib/wpcli.ts @@ -2,7 +2,18 @@ import {type ChildProcessByStdio, spawn, spawnSync} from 'node:child_process'; import type {Readable, Writable} from 'node:stream'; export function buildWpCliArgs(composePath: string, wpArgs: string[]): string[] { - return ['compose', '-f', composePath, 'run', '--rm', '-T', 'wpcli', ...wpArgs]; + return [ + 'compose', + '-f', + composePath, + '--profile', + 'cli', + 'run', + '--rm', + '-T', + 'wpcli', + ...wpArgs, + ]; } export type WpCliProcess = ChildProcessByStdio; @@ -13,6 +24,19 @@ export function spawnWpCli(composePath: string, wpArgs: string[]): WpCliProcess }) as WpCliProcess; } +/** + * Run WP-CLI with the parent process's stdio inherited so output streams + * straight to the terminal. Uses spawnSync with an args array (no shell), so + * arguments containing spaces, quotes, or other special characters are passed + * through verbatim. Returns the child's exit code (or 1 if it was signalled). + */ +export function runWpCliInherit(composePath: string, wpArgs: string[]): number { + const result = spawnSync('docker', buildWpCliArgs(composePath, wpArgs), { + stdio: 'inherit', + }); + return result.status ?? 1; +} + export function isWordPressInstalled(composePath: string): boolean { const result = spawnSync( 'docker', diff --git a/tests/lib/wpcli.test.ts b/tests/lib/wpcli.test.ts index 2816bf7..48df996 100644 --- a/tests/lib/wpcli.test.ts +++ b/tests/lib/wpcli.test.ts @@ -2,12 +2,14 @@ import {describe, expect, it} from 'vitest'; import {buildWpCliArgs} from '../../src/lib/wpcli.js'; describe('buildWpCliArgs', () => { - it('produces a docker compose run invocation with -T to disable TTY', () => { + it('produces a docker compose run invocation with --profile cli and -T to disable TTY', () => { const args = buildWpCliArgs('/path/compose.yaml', ['wp', 'db', 'export', '-']); expect(args).toEqual([ 'compose', '-f', '/path/compose.yaml', + '--profile', + 'cli', 'run', '--rm', '-T', @@ -18,4 +20,23 @@ describe('buildWpCliArgs', () => { '-', ]); }); + + it('enables the cli profile so the profiled wpcli service can start', () => { + const args = buildWpCliArgs('/path/compose.yaml', ['wp', 'core', 'is-installed']); + const profileIdx = args.indexOf('--profile'); + expect(profileIdx).toBeGreaterThanOrEqual(0); + expect(args[profileIdx + 1]).toBe('cli'); + // --profile must come before the `run` subcommand to take effect. + expect(profileIdx).toBeLessThan(args.indexOf('run')); + }); + + it('passes WP-CLI args verbatim as separate array elements (no shell joining)', () => { + const args = buildWpCliArgs('/path/compose.yaml', [ + 'wp', + 'post', + 'create', + '--post_title=Hello World', + ]); + expect(args).toContain('--post_title=Hello World'); + }); });