Skip to content

Forward host stdin to wp-cli through the server daemon - #3211

Draft
chubes4 wants to merge 2 commits into
trunkfrom
wp-cli-stdin-forwarding-clean
Draft

Forward host stdin to wp-cli through the server daemon#3211
chubes4 wants to merge 2 commits into
trunkfrom
wp-cli-stdin-forwarding-clean

Conversation

@chubes4

@chubes4 chubes4 commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

What

studio wp <command> now forwards piped host stdin to PHP. Closes #3200.

# Before: silently empty
echo 'hello' | studio wp eval 'echo file_get_contents("php://stdin");'
# ""

# After
echo 'hello' | studio wp eval 'echo file_get_contents("php://stdin");'
# hello

# Real-world usage
cat content.md | studio wp post create - --post_title="Draft"
curl -sL plugin.zip | studio wp plugin install - --activate

Why

studio wp runs in a different process from the WordPress server daemon (or in a worker thread on the in-proc path). Neither has the user's stdin pipe attached, so anything piped on the host never reached PHP. php://stdin always read empty.

How

  1. Drain host stdin up-front in commands/wp.tsdrainHostStdin() reads process.stdin into a Buffer when non-TTY (interactive shells are never blocked). Forwarded to both the daemon path (sendWpCliCommand) and the in-proc path (runWpCliCommand / runGlobalWpCliCommand).

  2. Base64 over IPC — Node's child_process IPC serializes payloads as JSON, so the wp-cli-command IPC message gets an optional stdinBase64 field to preserve binary bytes (gzipped SQL dumps, ZIP files, etc.) untouched.

  3. Daemon decodes + forwards — the daemon child in wordpress-server-child.ts decodes base64 and hands the bytes to server.playground.cli(args, { stdin }).

  4. Disable dedup when stdin is presentsequential() previously deduped concurrent WP-CLI calls by args alone. Two callers piping different bytes into the same command would coincidentally collapse into one execution, with the second caller getting the first caller's result silently. Fixed by returning undefined from deduplicateKey whenever stdin is present. Piped stdin is inherently non-idempotent; dedup is meaningless for it.

Requires

This PR relies on the new stdin option on PHP.cli() added in WordPress/wordpress-playground#3523. Merge order: Playground first → release → Studio bumps the version pin → this PR merges.

Tests

Validated end-to-end against a real Studio site:

  • 100 sequential calls with varying stdin → 100/100
  • 20 parallel with identical length, different content → 20/20 (regression test for the dedup fix)
  • 5× 1 MB parallel with md5 roundtrip → 5/5 byte-perfect
  • 30 alternating stdin / no-stdin → 30/30
  • 10 MB random binary md5 roundtrip → byte-perfect
  • UTF-8, binary with nulls / high bytes, empty stdin, no-trailing-newline, shell-special chars — all preserved
  • Real wp post create - --post_title=... creates posts with piped markdown content
  • Daemon path + in-proc fallback (site stopped) both work
  • No behavior change for commands invoked without piped stdin

No behavior change when stdin is a TTY

drainHostStdin() returns undefined when process.stdin.isTTY, so interactive invocations are unaffected. Existing args-based dedup still applies to no-stdin calls (only disabled when bytes are present).

Files

  • apps/cli/commands/wp.tsdrainHostStdin(), forward to both paths
  • apps/cli/lib/run-wp-cli-command.ts — in-proc paths accept { stdin }, forward to php.cli()
  • apps/cli/lib/types/wordpress-server-ipc.ts — add optional stdinBase64 to IPC schema
  • apps/cli/lib/wordpress-server-manager.tssendWpCliCommand accepts Buffer, base64-encodes it
  • apps/cli/wordpress-server-child.ts — decode base64, forward to server.playground.cli({ stdin }), fix dedup key

AI assistance

  • AI assistance: Yes
  • Tool(s): Claude Code (Opus 4.7)
  • Used for: Initial implementation of the five-file stdin-forwarding stack (host-stdin drain, IPC stdinBase64 schema, daemon decode, runWpCliCommand passthrough), and the sequential() dedup-key fix. The dedup-collision bug was surfaced by a parallel stress test during review and fixed in the same branch. All code was read, understood, and exercised end-to-end (100 sequential varied stdin, 20 parallel same-length-different-content, 5×1MB md5 roundtrip, alternating stdin/no-stdin, binary/UTF-8/empty/huge payloads) before submission.
`echo foo | studio wp eval 'echo file_get_contents("php://stdin");'`
silently dropped input when the site was running (daemon path). Both
paths now drain process.stdin up-front and forward the bytes to
`php.cli({ stdin })`, which — paired with the Playground-side stdin
option — surfaces them to PHP via `php://stdin`.

- commands/wp.ts: `drainHostStdin()` reads a non-TTY process.stdin
  into a Buffer and passes it to both the daemon (`sendWpCliCommand`)
  and in-proc (`runWpCliCommand`/`runGlobalWpCliCommand`) paths.
- lib/types/wordpress-server-ipc.ts: wp-cli-command IPC payload gains
  an optional `stdinBase64` field. Base64 is used because Node's
  child_process IPC is JSON-serialized; we must preserve binary bytes
  (e.g. gzipped SQL dumps) byte-for-byte.
- lib/wordpress-server-manager.ts: `sendWpCliCommand` accepts an
  optional Buffer and base64-encodes it into the payload.
- lib/run-wp-cli-command.ts: in-proc `runWpCliCommand` /
  `runGlobalWpCliCommand` accept an optional `{ stdin }` and
  forward it to `php.cli()`.
- wordpress-server-child.ts: decode the base64 on the daemon side and
  hand the bytes to `server.playground.cli(args, { stdin })`. Also
  disable the sequential() dedup key whenever stdin is present —
  piped stdin is inherently non-idempotent and two callers with
  coincidentally equal byte lengths must not collapse into one
  execution. The previous key included byte length only, which was
  not enough.

No behavior change for commands invoked without piped stdin.
@wpmobilebot

wpmobilebot commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

📊 Performance Test Results

Comparing 2751c58 vs trunk

app-size

Metric trunk 2751c58 Diff Change
App Size (Mac) 1337.93 MB 1337.93 MB +0.00 MB ⚪ 0.0%

site-editor

Metric trunk 2751c58 Diff Change
load 1740 ms 1788 ms +48 ms ⚪ 0.0%

site-startup

Metric trunk 2751c58 Diff Change
siteCreation 9059 ms 9553 ms +494 ms 🔴 5.5%
siteStartup 4398 ms 4432 ms +34 ms ⚪ 0.0%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff)

…ing-clean

# Conflicts:
#	apps/cli/commands/wp.ts
#	apps/cli/lib/run-wp-cli-command.ts
@wojtekn

wojtekn commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Marking as draft until conflicts and tests are fixed.

@wojtekn
wojtekn marked this pull request as draft July 14, 2026 07:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants